Compare commits
1004 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c85d0f35a | ||
|
|
331fcf0714 | ||
|
|
0ec9e8932e | ||
|
|
67eb7fdbd4 | ||
|
|
a1e7da8d84 | ||
|
|
f53b0ec9af | ||
|
|
57d0e8eea3 | ||
|
|
7403ef60c1 | ||
|
|
42ee461f56 | ||
|
|
77e8952e8a | ||
|
|
2be365c839 | ||
|
|
c0c08b3354 | ||
|
|
b0d8462a60 | ||
|
|
f246c96b28 | ||
|
|
aa7b3d985f | ||
|
|
5ed6e9842c | ||
|
|
eaf8b02aa9 | ||
|
|
7aa0aed7c9 | ||
|
|
562ba42b1b | ||
|
|
c081234021 | ||
|
|
bba0208361 | ||
|
|
b19efcebb9 | ||
|
|
4d6ce6b917 | ||
|
|
92d8027640 | ||
|
|
c9935fa45c | ||
|
|
a140deae54 | ||
|
|
9e5cc57441 | ||
|
|
8bab346cbb | ||
|
|
0a52cafefe | ||
|
|
83b3fdff21 | ||
|
|
97dfbe9e35 | ||
|
|
e4e6b5426e | ||
|
|
58815a3910 | ||
|
|
b17f173e03 | ||
|
|
9535fea964 | ||
|
|
4134e43f6e | ||
|
|
274b3bcb7e | ||
|
|
8800397ba4 | ||
|
|
0a90218d2a | ||
|
|
06598e0665 | ||
|
|
03e5d86b73 | ||
|
|
8533240407 | ||
|
|
2753005386 | ||
|
|
9178d061ff | ||
|
|
934c77cee9 | ||
|
|
2ec2239124 | ||
|
|
257262e3cc | ||
|
|
e85975308f | ||
|
|
323ccf3b25 | ||
|
|
be892516d5 | ||
|
|
a6e956472f | ||
|
|
392ce35efa | ||
|
|
ef35991a8c | ||
|
|
1c2992c887 | ||
|
|
1eaeebf2fe | ||
|
|
ffc027a309 | ||
|
|
6821d84f9b | ||
|
|
dcb25d8a40 | ||
|
|
2f20eeb016 | ||
|
|
98e40e2b49 | ||
|
|
2d10394690 | ||
|
|
ebe76aacf3 | ||
|
|
b144f0c1d7 | ||
|
|
8ec8b2c66d | ||
|
|
5bb5af2b5e | ||
|
|
2229686057 | ||
|
|
1a419f7e28 | ||
|
|
b8973d63c0 | ||
|
|
b1932dc8e4 | ||
|
|
de54bd4817 | ||
|
|
424bc9fa6e | ||
|
|
9282bdc09d | ||
|
|
2cb34c6535 | ||
|
|
5329f21896 | ||
|
|
6d73f2eb9f | ||
|
|
52a777aae6 | ||
|
|
e44bbae07a | ||
|
|
f8abca6053 | ||
|
|
156e697042 | ||
|
|
39d338e5bf | ||
|
|
8fb25646bd | ||
|
|
04cd53397a | ||
|
|
c16588578b | ||
|
|
fc000af68c | ||
|
|
b3414b23ce | ||
|
|
3143c4e51c | ||
|
|
bf7d43768b | ||
|
|
ea5a96f87f | ||
|
|
50f7dec026 | ||
|
|
98de9f1d7f | ||
|
|
4dc3d5dcf7 | ||
|
|
49566756cd | ||
|
|
5de89949b3 | ||
|
|
7b49f66ab7 | ||
|
|
412193f1d7 | ||
|
|
4c52c1b188 | ||
|
|
66152c52ca | ||
|
|
ca7b758dd4 | ||
|
|
8ea2780a44 | ||
|
|
a679c615ed | ||
|
|
7cc7bd4786 | ||
|
|
99761f0a93 | ||
|
|
b87f57cd25 | ||
|
|
14ff9aeae5 | ||
|
|
6ce37db3dc | ||
|
|
90dcbdfd02 | ||
|
|
ba7976c5c6 | ||
|
|
6520d3fd76 | ||
|
|
aafe9724d4 | ||
|
|
72207f9222 | ||
|
|
f1be8c88f2 | ||
|
|
927345b131 | ||
|
|
bb68421474 | ||
|
|
7699e1e79a | ||
|
|
59d08a7414 | ||
|
|
41c2d2a3d7 | ||
|
|
0c0be58e65 | ||
|
|
42afbd3e70 | ||
|
|
da9c0712a8 | ||
|
|
143d0f294a | ||
|
|
e197ab7bc3 | ||
|
|
d3bb075c47 | ||
|
|
afeb797c78 | ||
|
|
9ab14a9fd8 | ||
|
|
55a5546f25 | ||
|
|
938f0a028b | ||
|
|
c2b9a19c12 | ||
|
|
29c32f03ae | ||
|
|
950e03e321 | ||
|
|
a1876f16da | ||
|
|
c4b0dd5aa6 | ||
|
|
fa60258edc | ||
|
|
6846d5c6f0 | ||
|
|
30867960ce | ||
|
|
ee201ae6d8 | ||
|
|
6e5b4ca6e0 | ||
|
|
cbc3c36dc3 | ||
|
|
9675b1cf0b | ||
|
|
31967428ca | ||
|
|
9f60fe49ab | ||
|
|
d51340649b | ||
|
|
6907ec3a2e | ||
|
|
30b1aef8ba | ||
|
|
e1bbd76208 | ||
|
|
d38bf3b450 | ||
|
|
63e1849854 | ||
|
|
0b7fee0cc5 | ||
|
|
402301f201 | ||
|
|
c41141fe10 | ||
|
|
eccf5e15b1 | ||
|
|
6299e87569 | ||
|
|
0611db1bdf | ||
|
|
660004e348 | ||
|
|
ac5ce90eba | ||
|
|
0a41e7a4af | ||
|
|
5170b991b4 | ||
|
|
cd27933f98 | ||
|
|
6985f69caa | ||
|
|
aaf36a29a7 | ||
|
|
790420b320 | ||
|
|
2f011ebb24 | ||
|
|
191adf0a7c | ||
|
|
2ab95d908b | ||
|
|
62f505f982 | ||
|
|
eaafce0517 | ||
|
|
10f529c6b6 | ||
|
|
8424e51592 | ||
|
|
45e391b273 | ||
|
|
78e042cb9a | ||
|
|
0c41fdee6f | ||
|
|
11bbf1b8fa | ||
|
|
f89f686097 | ||
|
|
d93e4c7458 | ||
|
|
6bf4fc6cf8 | ||
|
|
35a8f7dd98 | ||
|
|
d0ec8ba22f | ||
|
|
57d3db5322 | ||
|
|
e43ee7ddba | ||
|
|
7a7bffab24 | ||
|
|
1001e57249 | ||
|
|
ccbeca2cad | ||
|
|
d7aa4afa60 | ||
|
|
e4c2ce6a8c | ||
|
|
de115c5113 | ||
|
|
4e0ab4b393 | ||
|
|
050bc472d0 | ||
|
|
48da1da5ee | ||
|
|
1b5f408bef | ||
|
|
b1869a33f9 | ||
|
|
bb07cdaa9f | ||
|
|
db0e4fcc0c | ||
|
|
ba8a2af87d | ||
|
|
85e7071d6c | ||
|
|
ece6df023a | ||
|
|
4c08f4a6e1 | ||
|
|
039e4dd4e5 | ||
|
|
041abf1220 | ||
|
|
56fc23f23a | ||
|
|
87c28c76f3 | ||
|
|
235f878dba | ||
|
|
b2ec0d4a0c | ||
|
|
02560d82ab | ||
|
|
22b80ffbb2 | ||
|
|
2db127d805 | ||
|
|
c2ca9f8f10 | ||
|
|
e614fbc5a1 | ||
|
|
7f001ee391 | ||
|
|
9432304be5 | ||
|
|
38c0527d35 | ||
|
|
6fc176e616 | ||
|
|
588bcebc87 | ||
|
|
4fd085b684 | ||
|
|
d2250cdabb | ||
|
|
179d5e6312 | ||
|
|
12d0f69e9c | ||
|
|
788ff65283 | ||
|
|
d7d93c8975 | ||
|
|
eb07fd7c38 | ||
|
|
c09b87482a | ||
|
|
19562a2445 | ||
|
|
543d65f43f | ||
|
|
fc1af353f3 | ||
|
|
9c57450330 | ||
|
|
0573084ffd | ||
|
|
6cb39709c4 | ||
|
|
4ea2fc34f0 | ||
|
|
a0e2bcb8e4 | ||
|
|
015644453b | ||
|
|
199c7d4e02 | ||
|
|
1d71e96421 | ||
|
|
8d0dbac882 | ||
|
|
6c1b4b1839 | ||
|
|
d4a4e7d139 | ||
|
|
0dfe18cd18 | ||
|
|
8895b44be9 | ||
|
|
279352377b | ||
|
|
51cf363c84 | ||
|
|
bdd554851d | ||
|
|
f611a36089 | ||
|
|
0861e1ed29 | ||
|
|
4070c53112 | ||
|
|
ac8ad98939 | ||
|
|
e315c29620 | ||
|
|
e84cd4fe8b | ||
|
|
2d85638d7d | ||
|
|
1daa700a1a | ||
|
|
c06176b3bf | ||
|
|
85f93c7861 | ||
|
|
e1457b5308 | ||
|
|
fc6fd00fe9 | ||
|
|
3ccc36f87a | ||
|
|
ceae540aa0 | ||
|
|
a5cab7005e | ||
|
|
e48e8cd05b | ||
|
|
e74800916e | ||
|
|
34f154d09d | ||
|
|
1bcdd6bc38 | ||
|
|
dd82ee68f0 | ||
|
|
564486f6d0 | ||
|
|
bf632a8584 | ||
|
|
506cf78dac | ||
|
|
0dbb5f18ba | ||
|
|
85e9c57ee2 | ||
|
|
257c266a2e | ||
|
|
605ea2aa62 | ||
|
|
896d9ae2c7 | ||
|
|
0beabc6c0c | ||
|
|
834a4c13a7 | ||
|
|
eca12aae82 | ||
|
|
00158c1894 | ||
|
|
77f5e3e835 | ||
|
|
48a5c12ab7 | ||
|
|
28793e06fc | ||
|
|
ba74b5aa13 | ||
|
|
01bceca7df | ||
|
|
ccca313a15 | ||
|
|
c463eedc50 | ||
|
|
7f49d6f08b | ||
|
|
78d4487c58 | ||
|
|
8a3f52b704 | ||
|
|
6e04535eff | ||
|
|
5bb580147a | ||
|
|
1adaa9f5c4 | ||
|
|
7b19bbb1a7 | ||
|
|
e3d4165fa4 | ||
|
|
ec54434427 | ||
|
|
67bf69fc21 | ||
|
|
c60e287cbe | ||
|
|
de4b2861b6 | ||
|
|
eb4234d814 | ||
|
|
4da7b8bd17 | ||
|
|
c0f5f224bf | ||
|
|
9b89814056 | ||
|
|
12d0baf5d3 | ||
|
|
8a695c14d7 | ||
|
|
f253bdf954 | ||
|
|
38d8764f15 | ||
|
|
01f6d106a2 | ||
|
|
9119860012 | ||
|
|
eeaaa0e6c9 | ||
|
|
db5987a466 | ||
|
|
b5d5cb085b | ||
|
|
dcf17e3b72 | ||
|
|
5b746c0d9c | ||
|
|
41bc6ca444 | ||
|
|
aba2f58fc4 | ||
|
|
fb1d947e97 | ||
|
|
f02bda2c52 | ||
|
|
aa4de67e90 | ||
|
|
b1a9fbe3ca | ||
|
|
603cf2c0ab | ||
|
|
0bc27e83ed | ||
|
|
25c1d03cca | ||
|
|
e2b4151ab4 | ||
|
|
889d307372 | ||
|
|
fd23396b95 | ||
|
|
fa5266626a | ||
|
|
cfe9bcdfe6 | ||
|
|
0555427805 | ||
|
|
cebc74009d | ||
|
|
61af0842e6 | ||
|
|
7d58ce6e00 | ||
|
|
d4c5ac8110 | ||
|
|
2dbfd1cc67 | ||
|
|
6d461155e8 | ||
|
|
e198a8931a | ||
|
|
e981fe04f9 | ||
|
|
6233a72b8a | ||
|
|
4e013d218f | ||
|
|
31b95db10b | ||
|
|
90708c3ca9 | ||
|
|
ef1074d169 | ||
|
|
63fa174814 | ||
|
|
7942f05961 | ||
|
|
5456290692 | ||
|
|
7ead0d02db | ||
|
|
9c5f0e5140 | ||
|
|
91735b3e19 | ||
|
|
ad5fb5ee56 | ||
|
|
b9040226e4 | ||
|
|
2c2579ae2b | ||
|
|
53fcdd8d7a | ||
|
|
87bf6301dc | ||
|
|
98f6ba6045 | ||
|
|
0ea40499e9 | ||
|
|
84f3519dbe | ||
|
|
61cfef445b | ||
|
|
b60bc2996b | ||
|
|
65c9a2cba0 | ||
|
|
f1c4910993 | ||
|
|
cd18692a53 | ||
|
|
220316ec7e | ||
|
|
bbf6f7fb06 | ||
|
|
82fc581125 | ||
|
|
6be4fcefdb | ||
|
|
f6eab47ab8 | ||
|
|
dd887e9a4f | ||
|
|
a8f5f71b32 | ||
|
|
dd93c4cdd4 | ||
|
|
7eded57d79 | ||
|
|
3de1d3afb0 | ||
|
|
b817148d1c | ||
|
|
902f91e25f | ||
|
|
12cb457c60 | ||
|
|
cb3cfd44ee | ||
|
|
0a335cefbf | ||
|
|
fe9998c6e4 | ||
|
|
634a98c2cb | ||
|
|
d8b7e299fd | ||
|
|
53ad7ecd57 | ||
|
|
69a3e283f8 | ||
|
|
58bb33cdcc | ||
|
|
1bbacc974b | ||
|
|
b6e29c8a61 | ||
|
|
f56d576a1e | ||
|
|
da4dc9eb7e | ||
|
|
e2b2b38e5b | ||
|
|
09f64e018e | ||
|
|
3762c278c4 | ||
|
|
8acd42fcbe | ||
|
|
23deef7a9a | ||
|
|
782e8dc495 | ||
|
|
0049137932 | ||
|
|
dcf4fe10cd | ||
|
|
5cd45a1413 | ||
|
|
65bac929ca | ||
|
|
d4b26cc4c4 | ||
|
|
a88443563e | ||
|
|
1865e56b04 | ||
|
|
4fefc1e4d2 | ||
|
|
8092192210 | ||
|
|
a30e150ade | ||
|
|
9da1bfc606 | ||
|
|
28f29ac49e | ||
|
|
97f079311d | ||
|
|
2586a871e1 | ||
|
|
6a73136176 | ||
|
|
76553d1e65 | ||
|
|
20e1fb406f | ||
|
|
fadfe7d091 | ||
|
|
db3980a716 | ||
|
|
25c36425be | ||
|
|
806a60e356 | ||
|
|
06def81b0a | ||
|
|
ce5058538d | ||
|
|
b5b6eba5da | ||
|
|
e4162f3716 | ||
|
|
4000ec546a | ||
|
|
46b64b8001 | ||
|
|
49bc2cb32a | ||
|
|
85c221e9bd | ||
|
|
9e7239cfef | ||
|
|
640bc33719 | ||
|
|
5ffdd022c2 | ||
|
|
9618e802d1 | ||
|
|
2e5d1b3b55 | ||
|
|
fe15ae07a1 | ||
|
|
ef433bbbe2 | ||
|
|
1bc68a9e85 | ||
|
|
be75019afd | ||
|
|
be88a0fde8 | ||
|
|
a1f9459ee9 | ||
|
|
77089719c0 | ||
|
|
161db209e6 | ||
|
|
04d285164a | ||
|
|
7cda37c5e2 | ||
|
|
63259ef8f4 | ||
|
|
372d33271d | ||
|
|
7997698bcd | ||
|
|
833f08d245 | ||
|
|
09f9e1d398 | ||
|
|
f40b4b2f30 | ||
|
|
f72d8f0ef0 | ||
|
|
f0608441fc | ||
|
|
8b13528661 | ||
|
|
2b2c4c15f5 | ||
|
|
0d685acfca | ||
|
|
6471ee0577 | ||
|
|
a890e25e3f | ||
|
|
acfd0a2a6b | ||
|
|
c945984d88 | ||
|
|
de8f15d726 | ||
|
|
1c648e5c2d | ||
|
|
aa009f8854 | ||
|
|
ebe76cbb0e | ||
|
|
9319b887c2 | ||
|
|
11aa6cccb8 | ||
|
|
ecfda87262 | ||
|
|
c8fab7f1d2 | ||
|
|
920c8846c8 | ||
|
|
fc324babf0 | ||
|
|
258dca8569 | ||
|
|
2ea6610c57 | ||
|
|
1ec1ddc80c | ||
|
|
b67dc1621b | ||
|
|
602ff67f3c | ||
|
|
278a4d35c7 | ||
|
|
ea68e4778e | ||
|
|
2d30ac21a7 | ||
|
|
5fbae3271f | ||
|
|
da6b00918d | ||
|
|
52d7e6892b | ||
|
|
9f05aae876 | ||
|
|
cac9e208df | ||
|
|
c86e8c51cb | ||
|
|
3abb399045 | ||
|
|
fb52618ce9 | ||
|
|
f8a2ffa4fa | ||
|
|
ff521f64a6 | ||
|
|
edb8f28098 | ||
|
|
b06dedfa4a | ||
|
|
f3d0d3e2c9 | ||
|
|
3e54f6e6e1 | ||
|
|
796bd22000 | ||
|
|
403e5050e8 | ||
|
|
c3e24ef4c5 | ||
|
|
eec6e66543 | ||
|
|
b17c2dc341 | ||
|
|
9141b93a6b | ||
|
|
1fb63f8be3 | ||
|
|
34bc242b76 | ||
|
|
f4529594a2 | ||
|
|
3a4cf4f2dd | ||
|
|
f422b22af1 | ||
|
|
6cd56dfd62 | ||
|
|
fbcd4036f5 | ||
|
|
fe708e0a0b | ||
|
|
4fc0bbc9d7 | ||
|
|
fd0eb4ca7d | ||
|
|
3528503604 | ||
|
|
954fb6064e | ||
|
|
2948a9ffc3 | ||
|
|
e54649bf66 | ||
|
|
dcb99fff80 | ||
|
|
a851469ae1 | ||
|
|
c0b9f4488f | ||
|
|
594aebaf8f | ||
|
|
34c8646477 | ||
|
|
5bc3de1e0a | ||
|
|
bd5f3c74e7 | ||
|
|
68341bf6a5 | ||
|
|
95f1561f0d | ||
|
|
056024372b | ||
|
|
cd8962f68b | ||
|
|
bade8ad32f | ||
|
|
4714074b12 | ||
|
|
785b859d63 | ||
|
|
de39ef938a | ||
|
|
6295b7561e | ||
|
|
defae3cc3a | ||
|
|
47c2b4bbde | ||
|
|
029077b92b | ||
|
|
ed042a66a4 | ||
|
|
7499a0d9ab | ||
|
|
09ca2a4fd9 | ||
|
|
a4b2fe2b91 | ||
|
|
8b219ba38a | ||
|
|
6160d3ddd1 | ||
|
|
20d7193fb2 | ||
|
|
3ce1ea610d | ||
|
|
8722791419 | ||
|
|
55c175d9dd | ||
|
|
da2dfd3736 | ||
|
|
bf489513dc | ||
|
|
56054e2607 | ||
|
|
732021f5a5 | ||
|
|
34293bcc1d | ||
|
|
ddb12ffbe2 | ||
|
|
47b99a24fb | ||
|
|
a4e0768105 | ||
|
|
11f7e3b8fc | ||
|
|
e42c346ebc | ||
|
|
23799b8d41 | ||
|
|
9515e13ce5 | ||
|
|
e1d7a363ef | ||
|
|
63d659ff49 | ||
|
|
dbf6020194 | ||
|
|
b5ad75bcf2 | ||
|
|
67d3c44017 | ||
|
|
3b37cacea2 | ||
|
|
a2a6a3d3f6 | ||
|
|
e9b9c87188 | ||
|
|
fec6aacee5 | ||
|
|
941159425b | ||
|
|
f33cd39300 | ||
|
|
8983960ca8 | ||
|
|
9dd885e7eb | ||
|
|
ab2900cadf | ||
|
|
bfcb29ff9c | ||
|
|
9bc52b412c | ||
|
|
606a3c843d | ||
|
|
5b3953094e | ||
|
|
aec8133046 | ||
|
|
97cad9f52c | ||
|
|
44d4198f69 | ||
|
|
432cce4ccf | ||
|
|
7eb43b7c61 | ||
|
|
feb8fcadd6 | ||
|
|
872125515e | ||
|
|
cb9bb37234 | ||
|
|
d223f5e21d | ||
|
|
b9891d1c08 | ||
|
|
a695540f60 | ||
|
|
414ae1b7e9 | ||
|
|
2b2869dc47 | ||
|
|
8244b59b57 | ||
|
|
e1a3d8c303 | ||
|
|
9add142edf | ||
|
|
1ff8308647 | ||
|
|
b6739483ee | ||
|
|
2d0569ed22 | ||
|
|
7cba892778 | ||
|
|
b2814947df | ||
|
|
b65295b1df | ||
|
|
a31ad79eec | ||
|
|
8fffdc83cf | ||
|
|
0a41f72d37 | ||
|
|
7086524cf3 | ||
|
|
9f1f7f272a | ||
|
|
9900e3194e | ||
|
|
c1d0bdbf03 | ||
|
|
f90f956364 | ||
|
|
088702f4d6 | ||
|
|
d4fedf62de | ||
|
|
1f4ffa6785 | ||
|
|
749f4ca6aa | ||
|
|
2511b1d832 | ||
|
|
f3c29f4c24 | ||
|
|
950c78fda2 | ||
|
|
6910a2b2ad | ||
|
|
bcea9875d5 | ||
|
|
9d839a037c | ||
|
|
063f71037d | ||
|
|
56037a2dca | ||
|
|
0cf3d3c883 | ||
|
|
3f21e40e62 | ||
|
|
64cb0ba146 | ||
|
|
67931dfcf9 | ||
|
|
c8e27d209c | ||
|
|
96af13b71f | ||
|
|
fbabae8793 | ||
|
|
99d2648901 | ||
|
|
700f84adec | ||
|
|
09b1543660 | ||
|
|
213a469dd0 | ||
|
|
45101b7c09 | ||
|
|
c9f9b87a6d | ||
|
|
dd09aab191 | ||
|
|
7bab65c138 | ||
|
|
3da56f28f8 | ||
|
|
83630e1fde | ||
|
|
4a222ad16d | ||
|
|
d9893e29ff | ||
|
|
bdfc6bc1fa | ||
|
|
41c1b04f0e | ||
|
|
f69f73fcda | ||
|
|
b24c3527ca | ||
|
|
3bf5d7a2db | ||
|
|
68f9b0d8ff | ||
|
|
e14c5442e0 | ||
|
|
643eb5a5c7 | ||
|
|
2401993018 | ||
|
|
9e61bab336 | ||
|
|
fc60ac3fb0 | ||
|
|
9e99c1729d | ||
|
|
39f745639f | ||
|
|
40a75b9b27 | ||
|
|
63c59a223a | ||
|
|
6717692187 | ||
|
|
f78a9f9112 | ||
|
|
41609f90ea | ||
|
|
cb0f5217fe | ||
|
|
ba9413eae5 | ||
|
|
b3e37dd2c1 | ||
|
|
beb3c7ec89 | ||
|
|
c10bdabee0 | ||
|
|
4e24f0dd4b | ||
|
|
723232659f | ||
|
|
eb7340434e | ||
|
|
3c1ecf1292 | ||
|
|
5f11607358 | ||
|
|
7e727ada94 | ||
|
|
bbad4b9e8a | ||
|
|
ec2c74f093 | ||
|
|
c20fa90c3f | ||
|
|
8135ee53ba | ||
|
|
61d4b7fcec | ||
|
|
2ea5148c4a | ||
|
|
8084786718 | ||
|
|
2ab010acad | ||
|
|
b10bd6ac12 | ||
|
|
b4a658cac5 | ||
|
|
508eee3f95 | ||
|
|
6e38b673ac | ||
|
|
f168bf94e1 | ||
|
|
875e8b59a6 | ||
|
|
f25d8e13c2 | ||
|
|
356c062ce5 | ||
|
|
80ea598ec2 | ||
|
|
837cacc992 | ||
|
|
1c88eb80c0 | ||
|
|
0de0c22e61 | ||
|
|
b8df5d083f | ||
|
|
3eee00bea7 | ||
|
|
a39a2898bb | ||
|
|
c73a2184a0 | ||
|
|
aff764d91c | ||
|
|
8e7fc47e2b | ||
|
|
1f2809a913 | ||
|
|
f7b5cfc623 | ||
|
|
74e746ace9 | ||
|
|
3b64cb43c1 | ||
|
|
ca491067f1 | ||
|
|
0ff5af5e0b | ||
|
|
85650cdfb3 | ||
|
|
0e4c830435 | ||
|
|
570c1a9b5f | ||
|
|
22e54636d4 | ||
|
|
3e8f9b18d0 | ||
|
|
8f0b3ff569 | ||
|
|
c1dadff525 | ||
|
|
bb87281057 | ||
|
|
1690c3b977 | ||
|
|
470de383bd | ||
|
|
239a384281 | ||
|
|
ed6a0ef29b | ||
|
|
d1ffab5487 | ||
|
|
d0cddcfb91 | ||
|
|
07729a7529 | ||
|
|
d002485636 | ||
|
|
2192c91acb | ||
|
|
2ea8d8e152 | ||
|
|
7a176c494f | ||
|
|
0541b5baad | ||
|
|
5b0e3d9cdb | ||
|
|
a3b50efe78 | ||
|
|
82b9f825d5 | ||
|
|
eee343c197 | ||
|
|
bec2a7c77a | ||
|
|
cd0b659653 | ||
|
|
da02622547 | ||
|
|
61b851fd3e | ||
|
|
d821baee4d | ||
|
|
4606c50f75 | ||
|
|
39eae73978 | ||
|
|
389ad1cf17 | ||
|
|
6237df953e | ||
|
|
2497fbbc74 | ||
|
|
79c1563b01 | ||
|
|
2badd39968 | ||
|
|
ccd30f7e80 | ||
|
|
673dc58051 | ||
|
|
fbf1bbbf99 | ||
|
|
494311aee3 | ||
|
|
e070601b28 | ||
|
|
bb65739886 | ||
|
|
56aa2a9104 | ||
|
|
3891531d1c | ||
|
|
0aaa400a87 | ||
|
|
f435d65db7 | ||
|
|
d4ff87395f | ||
|
|
5d42196297 | ||
|
|
4fae5332fc | ||
|
|
f118e94257 | ||
|
|
7a44e37970 | ||
|
|
57df6aa321 | ||
|
|
3f3aa6edd1 | ||
|
|
7dcd335630 | ||
|
|
08c845ff00 | ||
|
|
5445f950c5 | ||
|
|
85447e0b6a | ||
|
|
16076d1481 | ||
|
|
7313e326a0 | ||
|
|
aa9f07e0b9 | ||
|
|
6d6beb23b1 | ||
|
|
ae75eb07b7 | ||
|
|
ad5d7d2097 | ||
|
|
3063337eb2 | ||
|
|
cf5a1cee24 | ||
|
|
36845c021c | ||
|
|
f0d82b2751 | ||
|
|
0610c9fe98 | ||
|
|
1320f5c6c6 | ||
|
|
8e1706532b | ||
|
|
1770323690 | ||
|
|
177173d599 | ||
|
|
a15ef8489c | ||
|
|
59fd2454a4 | ||
|
|
bf146a8c0b | ||
|
|
c178d189c9 | ||
|
|
1abd151c67 | ||
|
|
2f9c08ac49 | ||
|
|
f5057119da | ||
|
|
44172dc5b1 | ||
|
|
1096c80b17 | ||
|
|
7f0029d8a4 | ||
|
|
43d18191f9 | ||
|
|
1a71ba0eb2 | ||
|
|
e14c8c5e91 | ||
|
|
13ba5ebcc8 | ||
|
|
800c714b9e | ||
|
|
493c31b244 | ||
|
|
ec49429810 | ||
|
|
21dfaf6a5a | ||
|
|
03eef94232 | ||
|
|
0e5ed35b6c | ||
|
|
ac8ef4608a | ||
|
|
a669cd5d86 | ||
|
|
d41a868f07 | ||
|
|
56be8931bb | ||
|
|
c541fd551e | ||
|
|
2b89efc923 | ||
|
|
0d9981b3c6 | ||
|
|
2b9362f7bf | ||
|
|
6b1b9bdce2 | ||
|
|
22fb84ca32 | ||
|
|
539b52ecbd | ||
|
|
5f837f7b3c | ||
|
|
04b2421793 | ||
|
|
e5cfa98bbd | ||
|
|
9d5130154b | ||
|
|
b4825e085e | ||
|
|
4ce915100b | ||
|
|
9b180a1c50 | ||
|
|
1e8c285eef | ||
|
|
c09d0940d4 | ||
|
|
a7005d779a | ||
|
|
dc65980dcb | ||
|
|
50a8468995 | ||
|
|
1ed5c219ec | ||
|
|
4971c40e23 | ||
|
|
ceb4667193 | ||
|
|
a1df68ed20 | ||
|
|
941bb94190 | ||
|
|
8e841c6825 | ||
|
|
85841d22f5 | ||
|
|
2403d0a18a | ||
|
|
a3354e9614 | ||
|
|
755d8591aa | ||
|
|
ea493ac1a5 | ||
|
|
9af41b2dc4 | ||
|
|
57940bc994 | ||
|
|
9673a9a0f6 | ||
|
|
e9939e7a0d | ||
|
|
7a74fc03fe | ||
|
|
050a1d45fd | ||
|
|
b61f4e935a | ||
|
|
3233b7c23a | ||
|
|
039db01b31 | ||
|
|
3de95a4f95 | ||
|
|
9c6d875524 | ||
|
|
653fd513ad | ||
|
|
ecdf4aee50 | ||
|
|
9cdfbc7459 | ||
|
|
31c348baff | ||
|
|
e80e4827a8 | ||
|
|
b11d130393 | ||
|
|
af8ac832fd | ||
|
|
043ade6e34 | ||
|
|
519d102a6e | ||
|
|
5f388ed41f | ||
|
|
a834c79b49 | ||
|
|
cb8c3a016a | ||
|
|
7a081e1147 | ||
|
|
72360be3e9 | ||
|
|
a97fd4f47f | ||
|
|
0db37bc204 | ||
|
|
bc7911b0bc | ||
|
|
c33083814e | ||
|
|
ff3320c8dc | ||
|
|
bd368c4c64 | ||
|
|
c051ec19f2 | ||
|
|
2be0d82a35 | ||
|
|
bdfcde7661 | ||
|
|
f609962d44 | ||
|
|
15f4aef7ef | ||
|
|
4218078502 | ||
|
|
18cd851674 | ||
|
|
2c6c148da8 | ||
|
|
42da4b4c43 | ||
|
|
ee006b6b16 | ||
|
|
cfb98986cd | ||
|
|
0fd7921a50 | ||
|
|
e0e86dff7c | ||
|
|
7e30fb19d4 | ||
|
|
df07e0401a | ||
|
|
a908c5f5d5 | ||
|
|
cdaa0b3ac2 | ||
|
|
6573ada881 | ||
|
|
2a7bde7e44 | ||
|
|
eca58bb27e | ||
|
|
2e68cd77fa | ||
|
|
f1d19d2d63 | ||
|
|
a13759130d | ||
|
|
a99cf75b2e | ||
|
|
3b7a52a60f | ||
|
|
6489a29436 | ||
|
|
c907d32779 | ||
|
|
1885a8d0cc | ||
|
|
5c46ecbebd | ||
|
|
f993a7022c | ||
|
|
0d2624bf3b | ||
|
|
801703a7a5 | ||
|
|
64b62c5e98 | ||
|
|
425d03f6b5 | ||
|
|
7977e869c3 | ||
|
|
1b7729ca01 | ||
|
|
0773dd24ab | ||
|
|
76a6c9c2d3 | ||
|
|
3a2477949b | ||
|
|
1edc62d023 | ||
|
|
7b0f5cec97 | ||
|
|
f0a0c0c11d | ||
|
|
3489a76a1d | ||
|
|
3cf05e551f | ||
|
|
501b356344 | ||
|
|
3f395ad4f3 | ||
|
|
ec92a0307b | ||
|
|
edce4e5bbc | ||
|
|
a7e6d0a513 | ||
|
|
2874bcc5f7 | ||
|
|
521c393b74 | ||
|
|
d853643874 | ||
|
|
28884d6774 | ||
|
|
8f7d6a3eb5 | ||
|
|
4653fcd782 | ||
|
|
84698aa68f | ||
|
|
143d390895 | ||
|
|
58568468f6 | ||
|
|
53de59940f | ||
|
|
4588e02faf | ||
|
|
bd5b3fa6e9 | ||
|
|
9dc6d2532a | ||
|
|
8c03b453b2 | ||
|
|
727a58f56d | ||
|
|
b89c10a298 | ||
|
|
5258e9f0e6 | ||
|
|
6c68502d03 | ||
|
|
88c485ffe5 | ||
|
|
562bf6d4ac | ||
|
|
807f865d8b | ||
|
|
50c07a5c8e | ||
|
|
3545bdc586 | ||
|
|
3d9f8ea142 | ||
|
|
9726fb5666 | ||
|
|
db22725687 | ||
|
|
69a69bbb82 | ||
|
|
0075b0836a | ||
|
|
77d447c0a3 | ||
|
|
af8ca7141d | ||
|
|
a48c74b2e7 | ||
|
|
a63949636e | ||
|
|
9a44cc04b1 | ||
|
|
cefa3147fc | ||
|
|
51116efba7 | ||
|
|
21058331cf | ||
|
|
4b001d9890 | ||
|
|
c9e1d7ba5c | ||
|
|
c8997cee68 | ||
|
|
ee343ad06f | ||
|
|
79fa0dbe77 | ||
|
|
9120c1d0eb | ||
|
|
a46370b81e | ||
|
|
2a428100c5 | ||
|
|
9f519b469d | ||
|
|
927cc2e9b5 | ||
|
|
8bda68d684 | ||
|
|
e108e30821 | ||
|
|
779426dbb1 | ||
|
|
a6aaa93389 | ||
|
|
72b18e4266 | ||
|
|
479f9af08c | ||
|
|
0ddfb6e4e7 | ||
|
|
120e99959a | ||
|
|
3cf5dc74bb | ||
|
|
7021715543 | ||
|
|
a351e2a118 | ||
|
|
4a2b9bd662 | ||
|
|
82355f0175 | ||
|
|
965645f1e6 | ||
|
|
5d4bc23c84 | ||
|
|
79e05b1665 | ||
|
|
e3285b5ca4 | ||
|
|
2c954c398c | ||
|
|
33b8f2002b | ||
|
|
e3da09cb0e | ||
|
|
cba3282541 | ||
|
|
621d34954a | ||
|
|
5fc45ad22f | ||
|
|
9ca8afb3ba | ||
|
|
175b2f8664 | ||
|
|
d6fbfe75bc | ||
|
|
bddc670eea | ||
|
|
08cf83de2a | ||
|
|
86184f8595 | ||
|
|
6775960241 | ||
|
|
2119d755ee | ||
|
|
ed9ca74b4f | ||
|
|
e2280dca39 | ||
|
|
deeaa90667 | ||
|
|
7d79d29e7e | ||
|
|
37d11ec303 | ||
|
|
4ed1a4bddb | ||
|
|
1324bc05e8 | ||
|
|
ca9a8173bd | ||
|
|
b98daed19c | ||
|
|
64e5444c06 | ||
|
|
dc1a77ee38 | ||
|
|
9ea068bf64 | ||
|
|
4940d8523c | ||
|
|
a1e78f1d17 | ||
|
|
fd8020dcc4 | ||
|
|
e31e8dec98 | ||
|
|
8103d7d31f | ||
|
|
f23b661a4b | ||
|
|
20691f8ab5 | ||
|
|
e1bd40dea3 | ||
|
|
17926775e7 | ||
|
|
79fac17bf7 | ||
|
|
303de4ae8a | ||
|
|
5caeafe2c3 | ||
|
|
481919bc03 | ||
|
|
558664760b | ||
|
|
f4e8c6ca51 | ||
|
|
01c5d50957 | ||
|
|
36c9c2616e | ||
|
|
2ab0c9cbeb | ||
|
|
d763f2de2f | ||
|
|
152f387939 | ||
|
|
9e5451b940 | ||
|
|
e1e7264bfc | ||
|
|
afb26fdb6f | ||
|
|
b6f7dc048f | ||
|
|
4efd89627d | ||
|
|
e257776852 | ||
|
|
502b0c4cc5 | ||
|
|
0d8c3a1e60 | ||
|
|
7254fbcd74 | ||
|
|
4d61670f38 | ||
|
|
00f90d1084 | ||
|
|
4c389a4077 | ||
|
|
e6ebdd5be3 | ||
|
|
22eb7de7ea | ||
|
|
5c4da77357 |
@@ -2,17 +2,23 @@
|
|||||||
#
|
#
|
||||||
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
|
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
|
||||||
#
|
#
|
||||||
version: 2
|
version: 2.1
|
||||||
|
|
||||||
|
orbs:
|
||||||
|
node: circleci/node@3.0.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:12.16.3
|
- image: cimg/node:16.11.0
|
||||||
- image: circleci/mongo:3.4-jessie
|
- image: mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/repo
|
working_directory: ~/homebrewery
|
||||||
|
executor: node/default
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout:
|
||||||
|
path: ~/homebrewery
|
||||||
|
|
||||||
# Download and cache dependencies
|
# Download and cache dependencies
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
@@ -21,12 +27,48 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- run: npm install
|
- node/install-npm
|
||||||
|
- node/install-packages:
|
||||||
|
app-dir: ~/homebrewery
|
||||||
|
cache-path: node_modules
|
||||||
|
override-ci-command: npm i
|
||||||
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
paths:
|
paths:
|
||||||
- node_modules
|
- node_modules
|
||||||
key: v1-dependencies-{{ checksum "package.json" }}
|
key: v1-dependencies-{{ checksum "package.json" }}
|
||||||
|
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- .
|
||||||
|
|
||||||
|
test:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:16.11.0
|
||||||
|
|
||||||
|
working_directory: ~/homebrewery
|
||||||
|
parallelism: 4
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
|
||||||
# run tests!
|
# run tests!
|
||||||
- run: npm run circleci
|
- run:
|
||||||
|
name: Test - Basic
|
||||||
|
command: npm run test:basic
|
||||||
|
- run:
|
||||||
|
name: Test - Mustache Spans
|
||||||
|
command: npm run test:mustache-span
|
||||||
|
- run:
|
||||||
|
name: Test - Routes
|
||||||
|
command: npm run test:route
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
build_and_test:
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
- test:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
@@ -32,7 +32,7 @@ module.exports = {
|
|||||||
skipBlankLines : true,
|
skipBlankLines : true,
|
||||||
}],
|
}],
|
||||||
'max-depth' : ['warn', { max: 4 }],
|
'max-depth' : ['warn', { max: 4 }],
|
||||||
'max-params' : ['warn', { max: 4 }],
|
'max-params' : ['warn', { max: 5 }],
|
||||||
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
|
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
|
||||||
'no-unused-vars' : ['warn', {
|
'no-unused-vars' : ['warn', {
|
||||||
vars : 'all',
|
vars : 'all',
|
||||||
|
|||||||
7
.gitignore
vendored
@@ -6,7 +6,8 @@ storage
|
|||||||
*.log
|
*.log
|
||||||
build/*
|
build/*
|
||||||
config/local.*
|
config/local.*
|
||||||
|
config/docker.*
|
||||||
|
|
||||||
todo.md
|
todo.md
|
||||||
startDB.bat
|
startDB.bat
|
||||||
startMViewer.bat
|
startMViewer.bat
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM node:14.15
|
FROM node:16.11-alpine
|
||||||
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
|
|
||||||
|
|||||||
57
README.md
@@ -9,37 +9,37 @@ using [Markdown][markdown-url]. It is distributed under the terms of the [MIT Li
|
|||||||
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
|
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
The easiest way to get started using the Homebrewery is to use it
|
The easiest way to get started using The Homebrewery is to use it
|
||||||
[on our website][homebrewery-url]. The code is open source, so feel free to
|
[on our website][homebrewery-url]. The code is open source, so feel free to
|
||||||
clone it, tinker with it. If you want to make changes to the code, you can run
|
clone it and tinker with it. If you want to make changes to the code, you can run
|
||||||
your own local version for testing by following the installation instructions
|
your own local version for testing by following the installation instructions
|
||||||
below.
|
below.
|
||||||
|
|
||||||
[homebrewery-url]: https://homebrewery.naturalcrit.com
|
[homebrewery-url]: https://homebrewery.naturalcrit.com
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
First, install three programs that the Homebrewery requires to run and retrieve
|
First, install three programs that The Homebrewery requires to run and retrieve
|
||||||
updates:
|
updates:
|
||||||
|
|
||||||
1. install [node](https://nodejs.org/en/)
|
1. install [node](https://nodejs.org/en/)
|
||||||
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
||||||
|
|
||||||
For easiest installation, follow these steps:
|
For the easiest installation, follow these steps:
|
||||||
1. In the installer, uncheck the option to run as a service
|
1. In the installer, uncheck the option to run as a service.
|
||||||
1. You can install MongoDB Compass if you want a GUI to view your database documents
|
1. You can install MongoDB Compass if you want a GUI to view your database documents.
|
||||||
1. Go to the C drive and create a folder called "data"
|
1. Go to the C:\ drive and create a folder called "data".
|
||||||
1. Inside the "data" folder, create a new folder called "db"
|
1. Inside the "data" folder, create a new folder called "db".
|
||||||
1. Open a command prompt or other terminal and navigate to your mongodb install folder (c:program files\mongo\server\4.4\bin)
|
1. Open a command prompt or other terminal and navigate to your MongoDB install folder (C:\Program Files\Mongo\Server\4.4\bin).
|
||||||
1. In the command prompt, run "mongod", which will start up your local database server
|
1. In the command prompt, run "mongod", which will start up your local database server.
|
||||||
1. While MongoD is running, open a second command prompt and navigate to the mongodb install folder
|
1. While MongoD is running, open a second command prompt and navigate to the MongoDB install folder.
|
||||||
1. In the second command prompt, run "mongo", which allows you to edit the database
|
1. In the second command prompt, run "mongo", which allows you to edit the database.
|
||||||
1. Type `use homebrewery` to create the homebrewery database. You should see `switched to db homebrewery`
|
1. Type `use homebrewery` to create The Homebrewery database. You should see `switched to db homebrewery`.
|
||||||
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`
|
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`.
|
||||||
1. Search in Windows for "Advanced system settings" and open it
|
1. Search in Windows for "Advanced system settings" and open it.
|
||||||
1. Click "Environment variables", find the "path" variable, and double-click to open it
|
1. Click "Environment variables", find the "path" variable, and double-click to open it.
|
||||||
1. Click "New" and paste in the path to the mongodb "bin" folder
|
1. Click "New" and paste in the path to the MongoDB "bin" folder.
|
||||||
1. Click "OK", "OK", "OK" to close all the windows
|
1. Click "OK" three times to close all the windows.
|
||||||
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt)
|
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt).
|
||||||
|
|
||||||
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
||||||
```
|
```
|
||||||
@@ -54,7 +54,7 @@ the project to run locally.
|
|||||||
You can set this temporarily in your shell of choice:
|
You can set this temporarily in your shell of choice:
|
||||||
* Windows Powershell: `$env:NODE_ENV="local"`
|
* Windows Powershell: `$env:NODE_ENV="local"`
|
||||||
* Windows CMD: `set NODE_ENV=local`
|
* Windows CMD: `set NODE_ENV=local`
|
||||||
* Linux / OSX: `export NODE_ENV=local`
|
* Linux / macOS: `export NODE_ENV=local`
|
||||||
|
|
||||||
Third, you will need to install the Node dependencies, compile the app, and run
|
Third, you will need to install the Node dependencies, compile the app, and run
|
||||||
it using the two commands:
|
it using the two commands:
|
||||||
@@ -63,7 +63,7 @@ it using the two commands:
|
|||||||
1. `npm start`
|
1. `npm start`
|
||||||
|
|
||||||
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
||||||
in your browser and use the Homebrewery offline.
|
in your browser and use The Homebrewery offline.
|
||||||
|
|
||||||
### Running the application via Docker
|
### Running the application via Docker
|
||||||
|
|
||||||
@@ -95,11 +95,11 @@ You can check out the [changelog](./changelog.md).
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the [MIT license](./license). Which means you
|
This project is licensed under the [MIT license](./license), which means you
|
||||||
are free to use The Homebrewery in any way that you want, except for claiming
|
are free to use The Homebrewery in any way that you want, except for claiming
|
||||||
that you made it yourself.
|
that you made it yourself.
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what's created on this site,
|
If you wish to sell, or in some way gain profit for, what's created on this site,
|
||||||
it's your responsibility to ensure you have the proper licenses/rights for any
|
it's your responsibility to ensure you have the proper licenses/rights for any
|
||||||
images or resources used.
|
images or resources used.
|
||||||
|
|
||||||
@@ -108,13 +108,12 @@ images or resources used.
|
|||||||
You are welcome to contribute to the development and maintenance of the
|
You are welcome to contribute to the development and maintenance of the
|
||||||
project! There are several ways of doing that:
|
project! There are several ways of doing that:
|
||||||
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some
|
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some
|
||||||
of them are outdated, duplicates or doesn't contain any useful info. In order
|
of them are outdated, duplicates, or don't contain any useful info. To help, you can [mark duplicates][github-mark-duplicate-url], try to
|
||||||
to help you can [mark duplicates][github-mark-duplicate-url], try to
|
reproduce some complex or weird issues, try finding a workaround for a
|
||||||
reproduce some complex or weird issues, try with finding a workaround for a
|
reported bug, or just mention our issue managers team to let them know about
|
||||||
reported bug or just mention issue managers team to let them know about
|
outdated issues via `@naturalcrit/issue-managers`.
|
||||||
outdated issue via `@naturalcrit/issue-managers`.
|
|
||||||
- Our [subreddit][subreddit-url] is constantly growing and there are number of
|
- Our [subreddit][subreddit-url] is constantly growing and there are number of
|
||||||
bug reports: any help with sorting them out is very welcome.
|
bug reports. Any help with sorting them out is very welcome.
|
||||||
- And of course you can contribute by fixing a bug or implementing a new
|
- And of course you can contribute by fixing a bug or implementing a new
|
||||||
feature by yourself, we are waiting for your
|
feature by yourself, we are waiting for your
|
||||||
[pull requests][github-pr-docs-url]!
|
[pull requests][github-pr-docs-url]!
|
||||||
|
|||||||
496
changelog.md
@@ -1,10 +1,472 @@
|
|||||||
<style>
|
```css
|
||||||
h5 {
|
h5 {
|
||||||
font-size: .35cm !important;
|
font-size: .35cm !important;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
|
||||||
# changelog
|
.page ul ul {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskList li input {
|
||||||
|
list-style-type : none;
|
||||||
|
margin-left : -0.52cm;
|
||||||
|
transform: translateY(.05cm);
|
||||||
|
filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskList li input[checked] {
|
||||||
|
filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre + * {
|
||||||
|
margin-top: 0.17cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin-top: 0.17cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p + pre {
|
||||||
|
margin-top : 0.1cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .openSans {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## changelog
|
||||||
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Wednesday 27/03/2022 - v3.0.8
|
||||||
|
{{taskList
|
||||||
|
* [x] Style updates to user page.
|
||||||
|
|
||||||
|
* [x] Added a logout button (finally)! You can find it under {{openSans **USERNAME {{fa,fa-user}} → LOGOUT {{fas,fa-power-off}}**}}
|
||||||
|
|
||||||
|
Fixes issues: [#303](https://github.com/naturalcrit/homebrewery/issues/303)
|
||||||
|
|
||||||
|
* [x] Clarified the default text when submitting an issue via Reddit post.
|
||||||
|
|
||||||
|
* [x] Fixed broken Table of Contents links in PDFs. (Thanks lucastucious!)
|
||||||
|
|
||||||
|
Fixes issues: [#1749](https://github.com/naturalcrit/homebrewery/issues/1749)
|
||||||
|
|
||||||
|
* [x] Fixed window resizing causing the edit page divider to get lost off of the edge of the page.
|
||||||
|
|
||||||
|
Fixes issues: [#2053](https://github.com/naturalcrit/homebrewery/issues/2053)
|
||||||
|
|
||||||
|
* [x] Fixed Class Table decorations overlapping main text.
|
||||||
|
|
||||||
|
Fixes issues: [#1985](https://github.com/naturalcrit/homebrewery/issues/1985)
|
||||||
|
|
||||||
|
* [x] Updated {{openSans **STYLE EDITOR {{fa,fa-pencil-alt}} → REMOVE DROP CAP {{fas,fa-remove-format}}**}} snippet to also remove small-caps first line font.
|
||||||
|
|
||||||
|
* [x] Background work in preparation for brew themes.
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 02/02/2022 - v3.0.7
|
||||||
|
{{taskList
|
||||||
|
* [x] Revert active line highlighting.
|
||||||
|
|
||||||
|
Fixes issues: [#1913](https://github.com/naturalcrit/homebrewery/issues/1913)
|
||||||
|
|
||||||
|
* [x] Added install steps for Ubuntu. [HERE](https://github.com/naturalcrit/homebrewery/blob/master/install/README.UBUNTU.md)
|
||||||
|
|
||||||
|
Fixes issues: [#1900](https://github.com/naturalcrit/homebrewery/issues/1900)
|
||||||
|
|
||||||
|
* [x] Added social media links to home page.
|
||||||
|
|
||||||
|
* [x] Increase brews visible on the user page to 1,000.
|
||||||
|
|
||||||
|
Fixes issues: [#1943](https://github.com/naturalcrit/homebrewery/issues/1943)
|
||||||
|
|
||||||
|
* [x] Added a Legacy to V3 migration guide under {{openSans **NEED HELP? {{fa,fa-question-circle}} → MIGRATE {{fas,fa-file-import}}**}}
|
||||||
|
|
||||||
|
* [x] Background refactoring and unit tests.
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Saturday 18/12/2021 - v3.0.6
|
||||||
|
{{taskList
|
||||||
|
* [x] Fixed text wrapping for long strings in code blocks.
|
||||||
|
|
||||||
|
Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736)
|
||||||
|
|
||||||
|
* [x] Code search/replace PC: `CTRL F / CTRL SHIFT F` / Mac: `CMD F / OPTION CMD F`
|
||||||
|
|
||||||
|
Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201)
|
||||||
|
|
||||||
|
* [x] Auto-closing HTML tags and curly braces `{{ }}`
|
||||||
|
* [x] Highlight current active line
|
||||||
|
|
||||||
|
Fixes issues: [#1202](https://github.com/naturalcrit/homebrewery/issues/1202)
|
||||||
|
|
||||||
|
* [x] Display tabs and trailing spaces
|
||||||
|
|
||||||
|
Fixes issues: [#1622](https://github.com/naturalcrit/homebrewery/issues/1622)
|
||||||
|
|
||||||
|
* [x] Make columns even in V3 Table of Contents.
|
||||||
|
|
||||||
|
Fixes issues: [#1671](https://github.com/naturalcrit/homebrewery/issues/1671)
|
||||||
|
|
||||||
|
* [x] Fix `CTRL P` failing to print from `/new` pages.
|
||||||
|
|
||||||
|
Fixes issues: [#1815](https://github.com/naturalcrit/homebrewery/issues/1815)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Tuesday 07/12/2021 - v3.0.5
|
||||||
|
{{taskList
|
||||||
|
* [x] Fixed paragraph spacing for **note** and **descriptive** boxes in V3.
|
||||||
|
|
||||||
|
Fixes issues: [#1836](https://github.com/naturalcrit/homebrewery/issues/1836)
|
||||||
|
|
||||||
|
* [x] Added a whole bunch of hotkeys:
|
||||||
|
|
||||||
|
* Page Break `CTRL + ENTER`
|
||||||
|
* Column Break `CTRL + SHIFT + ENTER`
|
||||||
|
* Bulleted Lists `CTRL + L`
|
||||||
|
* Numbered Lists `CTRL + SHIFT + L`
|
||||||
|
* Headers `CTRL + SHIFT + (1-6)`
|
||||||
|
* Underline `CTRL + U`
|
||||||
|
* Link `CTRL + K`
|
||||||
|
* Non-breaking space (\ ) `CTRL + .`
|
||||||
|
* Add Horizontal Space `CTRL + SHIFT + .`
|
||||||
|
* Remove Horizontal Space `CTRL + SHIFT + ,`
|
||||||
|
* Curly Span `CTRL + M`
|
||||||
|
* Curly Div `CTRL + SHIFT + M`
|
||||||
|
|
||||||
|
* [x] Fixed page numbers in the editor panel getting scrambled when scrolling up and down.
|
||||||
|
|
||||||
|
* [x] Faster swapping between tabs on long brews.
|
||||||
|
|
||||||
|
* [x] Better error messages for common issue with Google Drive credentials expiring.
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 17/11/2021 - v3.0.4
|
||||||
|
{{taskList
|
||||||
|
* [x] Fixed incorrect sorting of Google brews by page count and views on the user page.
|
||||||
|
|
||||||
|
Fixes issues: [#1793](https://github.com/naturalcrit/homebrewery/issues/1793)
|
||||||
|
|
||||||
|
* [x] Added code folding! Only on a page-level for now. Hotkeys `CTRL + [` and `CTRL + ]` to fold/unfold all pages. (Thanks jeddai, new contributor!)
|
||||||
|
|
||||||
|
Fixes issues: [#629](https://github.com/naturalcrit/homebrewery/issues/629)
|
||||||
|
|
||||||
|
* [x] Fixed rendering issues due to the latest Chrome update to version 96. (Also thanks to jeddai!)
|
||||||
|
|
||||||
|
Fixes issues: [#1828](https://github.com/naturalcrit/homebrewery/issues/1828)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 27/10/2021 - v3.0.3
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
* [x] Moved **Post To Reddit** button from {{fa,fa-info-circle}} **Properties** menu to the **SHARE** {{fa,fa-share-alt}} button as a dropdown.
|
||||||
|
|
||||||
|
* [x] Added a **Copy URL** button to the **SHARE** {{fa,fa-share-alt}} button as a dropdown.
|
||||||
|
|
||||||
|
* [x] Fixed pages being printed directly from `/new` not recognizing the V3 renderer.
|
||||||
|
|
||||||
|
Fixes issues: [#1702](https://github.com/naturalcrit/homebrewery/issues/1702)
|
||||||
|
|
||||||
|
* [x] Updated links to [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) on home page.
|
||||||
|
|
||||||
|
Fixes issues: [#1744](https://github.com/naturalcrit/homebrewery/issues/1744)
|
||||||
|
|
||||||
|
* [x] Added a [FAQ page](https://homebrewery.naturalcrit.com/faq).
|
||||||
|
|
||||||
|
Fixes issues: [#810](https://github.com/naturalcrit/homebrewery/issues/810)
|
||||||
|
|
||||||
|
* [x] Added {{fa,fa-undo}} **Undo** and {{fa,fa-redo}} **Redo** buttons to the snippet bar.
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
* [x] Switching between the {{fa,fa-beer}} **Brew** and {{fa,fa-paint-brush}} **Style** tabs no longer loses your scroll position or undo history.
|
||||||
|
|
||||||
|
Fixes issues: [#1735](https://github.com/naturalcrit/homebrewery/issues/1735)
|
||||||
|
|
||||||
|
* [x] Divider bar between editor and preview panels can no longer be dragged off the edge of the screen.
|
||||||
|
|
||||||
|
Fixes issues: [#1674](https://github.com/naturalcrit/homebrewery/issues/1674)
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Wednesday 06/10/2021 - v3.0.2
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
* [x] Fixed V3 **EDITOR → QR Code** snippet not working on `/new` (unsaved) pages.
|
||||||
|
|
||||||
|
Fixes issues: [#1710](https://github.com/naturalcrit/homebrewery/issues/1710)
|
||||||
|
|
||||||
|
* [x] Reorganized several snippets from the **Brew Editor** panel into the **Style Editor** panel.
|
||||||
|
|
||||||
|
Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pm6ki7/two_version_of_class_features_making_it_look_more/)
|
||||||
|
|
||||||
|
* [x] Added a page counter to the right of each `\page` line in V3 to help navigate your brews. Starts counting from page 2.
|
||||||
|
|
||||||
|
Fixes issues: [#846](https://github.com/naturalcrit/homebrewery/issues/846)
|
||||||
|
|
||||||
|
* [x] Moved the changelog to be accessible by clicking on the Homebrewery version number.
|
||||||
|
|
||||||
|
Fixes issues: [#1166](https://github.com/naturalcrit/homebrewery/issues/1166)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Friday, 17/09/2021 - v3.0.1
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
* [x] Updated V3 **PHB → Class Feature** snippet to use V3 syntax.
|
||||||
|
|
||||||
|
Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pm6ki7/two_version_of_class_features_making_it_look_more/)
|
||||||
|
|
||||||
|
* [x] Improved V3 **PHB → Monster Stat Block** snippet and styling to allow for easier control of paragraph indentation in the Abilities text.
|
||||||
|
|
||||||
|
Fixes issues: [#181](https://github.com/naturalcrit/homebrewery/issues/181)
|
||||||
|
|
||||||
|
* [x] Improved Legacy **TABLES → Split Table** snippet by removing unneeded column-break backticks.
|
||||||
|
|
||||||
|
Fixes issues: [#844](https://github.com/naturalcrit/homebrewery/issues/844)
|
||||||
|
|
||||||
|
* [x] Changed block elements to use CSS `width` instead of `min-width`. This should make custom styles behave more predictably when trying to resize items.
|
||||||
|
|
||||||
|
Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pohoy3/looking_for_help_with_basic_stuff_in_v3/)
|
||||||
|
|
||||||
|
* [x] Fixed Partial Page Rendering in V3 for large brews
|
||||||
|
|
||||||
|
Fixes issues: [Reported on Reddit](https://www.reddit.com/r/homebrewery/comments/pori3a/weird_behaviour_of_the_brew_after_page_50/)
|
||||||
|
|
||||||
|
* [x] Fixed HTML validation to handle tags starting with 'a', as in `<aside>`.
|
||||||
|
|
||||||
|
Fixes issues: [#230](https://github.com/naturalcrit/homebrewery/issues/230)
|
||||||
|
|
||||||
|
* [x] Fixed page footers switching side when printing.
|
||||||
|
|
||||||
|
Fixes issues: [#1612](https://github.com/naturalcrit/homebrewery/issues/1612)
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Saturday, 11/09/2021 - v3.0.0
|
||||||
|
|
||||||
|
We have been working on v3 for a *very* long time. We want to thank everyone for being paitent.
|
||||||
|
|
||||||
|
|
||||||
|
Some features planned for V3 have actually been released over the recent months as part of V2, and some are still on the way. But at its core, V3 provides brand new Markdown-to-Brew rendering system, which was no simple task. This has opened up access to all sorts of bugfixes, tweaks, and potential for new features that just wouldn't be possible on the old system.
|
||||||
|
|
||||||
|
***BE WARNED:*** As we continue to develop V3, expect small tweaks in the styling, fonts, and snippets; your brews may look slightly different from day-to-day; some things might break completely while we tackle any bugs in this early stage. All of your old documents will continue to work as normal. We are not touching them. If you don't want to deal With the possibility of slight formatting changes, you may choose to stick with the Legacy renderer on any of your brews for as long as you like. However, most new features added from now on will only be available for brews using the V3 renderer.
|
||||||
|
|
||||||
|
Massive changelog incoming:
|
||||||
|
|
||||||
|
#### Markdown+
|
||||||
|
With the latest major update to *The Homebrewery*, we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div`, and `span` in most cases. This should hopefully aid non-coders with readability, and also allows us a few tricks in the background to fix some old issues. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||||
|
|
||||||
|
All brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew. Much of the syntax and styling has changed in V3, so code in one version may be broken in the other.
|
||||||
|
|
||||||
|
Visit [this page](/v3_preview) for brief examples of the new syntax!
|
||||||
|
|
||||||
|
#### Extended Markdown Syntax:
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
* [x] Add Divs and Spans for all your custom styling needs, via a simplified Markdown-like syntax:
|
||||||
|
```
|
||||||
|
{{myDivClass,#myId,color:red
|
||||||
|
My Div content
|
||||||
|
}}
|
||||||
|
|
||||||
|
Hello {{mySpan,color:blue World}} !
|
||||||
|
```
|
||||||
|
|
||||||
|
Fixes issues: [#348](https://github.com/naturalcrit/homebrewery/issues/348)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
* [x] Add inline CSS to Markdown objects via "curly injection" syntax:
|
||||||
|
```
|
||||||
|
Hello *world*{myClass,#id,color:red}
|
||||||
|
```
|
||||||
|
Fixes issues: [#403](https://github.com/naturalcrit/homebrewery/issues/403)
|
||||||
|
|
||||||
|
* [x] Rowspan, Colspan, and multiple header rows with extended table syntax:
|
||||||
|
```
|
||||||
|
| Header 1a | Header 1b | Header 1c |
|
||||||
|
| Header 2a | Header 2b | Header 2c |
|
||||||
|
|:---------:|:----------|:---------:|
|
||||||
|
| Span 2 columns || Span 2 |
|
||||||
|
| one col | one col | rows ^|
|
||||||
|
```
|
||||||
|
Fixes issues: [#773](https://github.com/naturalcrit/homebrewery/issues/773), [#191](https://github.com/naturalcrit/homebrewery/issues/191)
|
||||||
|
|
||||||
|
* [x] Hanging indents via `<dl>` tags, as seen in the **PHB → Spell** snippet. Add via "double-colon" syntax:
|
||||||
|
```
|
||||||
|
Term :: big long definition that bleeds onto multiple lines
|
||||||
|
```
|
||||||
|
Fixes issues: [#182](https://github.com/naturalcrit/homebrewery/issues/182), [#149](https://github.com/naturalcrit/homebrewery/issues/149)
|
||||||
|
|
||||||
|
* [x] Easier vertical spacing via colons alone on a line:
|
||||||
|
```
|
||||||
|
:::
|
||||||
|
```
|
||||||
|
Fixes issues: [#374](https://github.com/naturalcrit/homebrewery/issues/374)
|
||||||
|
|
||||||
|
* [x] Avoid paragraph indendation by ending the previous paragraph with a backslash `\` or two spaces ` `
|
||||||
|
```
|
||||||
|
Paragraph one\
|
||||||
|
Paragraph two
|
||||||
|
```
|
||||||
|
Fixes issues: [#636](https://github.com/naturalcrit/homebrewery/issues/636)
|
||||||
|
|
||||||
|
* [x] Code blocks can be inserted by surrounding it with rows of three backticks ` ``` `, for demonstration purposes or to share custom styles. Inline-code can be inserted with single backticks <code>`code`</code>
|
||||||
|
<pre><code>```
|
||||||
|
Here is some code!
|
||||||
|
```
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
Fixes issues: [#465](https://github.com/naturalcrit/homebrewery/issues/465)
|
||||||
|
|
||||||
|
#### New and Fixed Snippets
|
||||||
|
|
||||||
|
* [x] Column breaks now use `\column` instead of ` ``` ` backticks.
|
||||||
|
|
||||||
|
Fixes issues: [#607](https://github.com/naturalcrit/homebrewery/issues/607)
|
||||||
|
|
||||||
|
* [x] Page breaks using `\page` now only trigger when placed alone at the start of a line.
|
||||||
|
|
||||||
|
Fixes issues: [#1147](https://github.com/naturalcrit/homebrewery/issues/1147)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
{{taskList
|
||||||
|
* [x] New **EDITOR → QR Code** snippet.
|
||||||
|
|
||||||
|
Fixes issues: [#538](https://github.com/naturalcrit/homebrewery/issues/538)
|
||||||
|
|
||||||
|
* [x] New **IMAGES → Watercolor Splatter** snippet, which adds one of a range of stylish stains to your brew.
|
||||||
|
|
||||||
|
* [x] New **IMAGES → Watermark** snippet, which adds transparent text diagonally across the page.
|
||||||
|
|
||||||
|
* [x] New **PHB → Magic Item** snippet.
|
||||||
|
|
||||||
|
Fixes issues: [#671](https://github.com/naturalcrit/homebrewery/issues/671)
|
||||||
|
|
||||||
|
* [x] New **TABLES → 1/3 Class Table** snippet for 1/3 casters.
|
||||||
|
|
||||||
|
Fixes issues: [#191](https://github.com/naturalcrit/homebrewery/issues/191)
|
||||||
|
|
||||||
|
* [x] Improved **EDITOR → Table of Contents** snippet to actually look like the PHB style. Will auto-generate based on the headers in your brew.
|
||||||
|
|
||||||
|
Fixes issues: [#304](https://github.com/naturalcrit/homebrewery/issues/304)
|
||||||
|
|
||||||
|
* [x] Improved **PHB → Monster Stat Block** snippet with textures, and an option to remove the frame entirely.
|
||||||
|
|
||||||
|
* [x] Improved **PHB → Spell List** snippet can now be made single-column.
|
||||||
|
|
||||||
|
Fixes issues: [#509](https://github.com/naturalcrit/homebrewery/issues/509), [#914](https://github.com/naturalcrit/homebrewery/issues/914)
|
||||||
|
|
||||||
|
* [x] Improved **TABLES → Class Table** snippet is now cleaned up, has an option to remove the frame entirely, and includes additional boundary decorations.
|
||||||
|
|
||||||
|
Fixes issues: [#773](https://github.com/naturalcrit/homebrewery/issues/773), [#302](https://github.com/naturalcrit/homebrewery/issues/302)
|
||||||
|
|
||||||
|
#### Miscellaneous Formatting Fixes
|
||||||
|
|
||||||
|
* [x] Paragraphs are now able to split across columns.
|
||||||
|
|
||||||
|
Fixes issues: [#239](https://github.com/naturalcrit/homebrewery/issues/239)
|
||||||
|
|
||||||
|
* [x] Multiple fixes for bold/italicize using asterisks `* *`
|
||||||
|
|
||||||
|
Fixes issues: [#1321](https://github.com/naturalcrit/homebrewery/issues/1321), [#852](https://github.com/naturalcrit/homebrewery/issues/852)
|
||||||
|
|
||||||
|
* [x] Multiple for list items not displaying correctly.
|
||||||
|
|
||||||
|
Fixes issues: [#1085](https://github.com/naturalcrit/homebrewery/issues/1085), [#588](https://github.com/naturalcrit/homebrewery/issues/588)
|
||||||
|
|
||||||
|
* [x] "Smart quotes", so left and right quotes are different.
|
||||||
|
|
||||||
|
Fixes issues: [#849](https://github.com/naturalcrit/homebrewery/issues/849)
|
||||||
|
|
||||||
|
* [x] Long URLs in links now wrap properly.
|
||||||
|
|
||||||
|
Fixes issues: [#1136](https://github.com/naturalcrit/homebrewery/issues/1136)
|
||||||
|
|
||||||
|
* [x] Better support for `wide` blocks that span across the whole page! No more problems with contents getting shunted off the edge, and each new wide element in a page will restart the next item back at column one. Manual `\column` breaks will help organize subsequent content between the columns as needed.
|
||||||
|
|
||||||
|
Fixes issues: [#144](https://github.com/naturalcrit/homebrewery/issues/144), [#1024](https://github.com/naturalcrit/homebrewery/issues/1024)
|
||||||
|
|
||||||
|
* [x] Fonts now support a wider range of latin characters for non-English brews, including áéíóúñ¡¿, etc...
|
||||||
|
|
||||||
|
Fixes issues: [#116](https://github.com/naturalcrit/homebrewery/issues/116)
|
||||||
|
|
||||||
|
* [x] Drop-caps (fancy first letters) have been re-styled and re-aligned to correct the ugly overlapping and cut-off on some characters like K and Y.
|
||||||
|
|
||||||
|
Fixes issues: [#848](https://github.com/naturalcrit/homebrewery/issues/848)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Under-the-Hood Stuff
|
||||||
|
We had to make a whole lot of background upgrades and changes to get all of this working, and now that the framework is in place, there's a lot more planned and upcoming *"sometime"* :
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
* [ ] New Themes to style your brews. DMG, MM, a custom Homebrewery theme, and others.
|
||||||
|
* [ ] The ability to build your own custom themes using CSS, apply it to other brews, and share it with others!
|
||||||
|
* [ ] Easy control of item colors. Change your monster blocks, tables, and notes from yellow to green to red!
|
||||||
|
* [ ] New image-based snippets, including handwritten notes, title illustrations, and alternative decorations.
|
||||||
|
* [ ] New fun fonts like Elvish, Draconic, Orcish, etc.
|
||||||
|
* [ ] Better organization of personal brews using tags.
|
||||||
|
* [ ] ....a log-out button...?
|
||||||
|
* [ ] AND MORE.
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
::
|
||||||
|
#### Style Editor Panel
|
||||||
|
|
||||||
|
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\page
|
||||||
|
### Thursday, 09/09/2021 - v2.13.5
|
||||||
|
- Slightly better error logging and messages for users.
|
||||||
|
|
||||||
|
##### G-Ambatte :
|
||||||
|
- Added a search bar to the User page to help find your brews.
|
||||||
|
- Added page counts to brews in the User page; page count will be updated the next time a brew is edited.
|
||||||
|
- Fixed edge case where view counts could get reset.
|
||||||
|
- Fixed edge case where last-modified time was not accurate for Google Doc brews.
|
||||||
|
|
||||||
|
##### Gazook89 :
|
||||||
|
- Fixed typo in the **PRINT → Ink-Friendly** snippet.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Tuesday, 17/08/2021 - v2.13.4
|
||||||
|
- Fixed User page crashing when user has an untitled brew
|
||||||
|
|
||||||
|
##### G-Ambatte:
|
||||||
|
- Tweaks to user page tool tips
|
||||||
|
- Fix view counts being reset on Google Drive files
|
||||||
|
|
||||||
|
##### Gazook89 :
|
||||||
|
- New **PHB → Artist Credit** snippet
|
||||||
|
- **PRINT** snippets moved to the **Style Editor** tab
|
||||||
|
|
||||||
|
### Monday, 09/08/2021 - v2.13.3
|
||||||
|
|
||||||
|
##### G-Ambatte :
|
||||||
|
- Tooltips hovering over brews in dropdowns / user page.
|
||||||
|
- Fixed sort-by created date on user page.
|
||||||
|
|
||||||
|
##### Gazook89 :
|
||||||
|
- Hotkey Ctrl-/ and snippets to add HTML comments; use for notes that won't appear in your brew.
|
||||||
|
|
||||||
### Friday, 30/07/2021 - v2.13.2
|
### Friday, 30/07/2021 - v2.13.2
|
||||||
|
|
||||||
@@ -39,7 +501,6 @@ myStyle {color: black}
|
|||||||
- Pasting your brew into a "New" page and saving will transfer any CSS in the code fence to the Style tab.
|
- Pasting your brew into a "New" page and saving will transfer any CSS in the code fence to the Style tab.
|
||||||
- Unsaved work in the New page Style tab is now cached to your browser storage if you navigate away.
|
- Unsaved work in the New page Style tab is now cached to your browser storage if you navigate away.
|
||||||
|
|
||||||
|
|
||||||
### Thursday, 10/6/2021 - v2.12.0
|
### Thursday, 10/6/2021 - v2.12.0
|
||||||
|
|
||||||
- New "style" tab to better organize custom CSS in preparation for new themes and sharable styles.
|
- New "style" tab to better organize custom CSS in preparation for new themes and sharable styles.
|
||||||
@@ -50,6 +511,8 @@ myStyle {color: black}
|
|||||||
- Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery.
|
- Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery.
|
||||||
- Move cursor to end of snippet after insertion
|
- Move cursor to end of snippet after insertion
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Saturday, 20/3/2021 - v2.11.1
|
### Saturday, 20/3/2021 - v2.11.1
|
||||||
|
|
||||||
- Warning when opening brew in your Google Drive trash
|
- Warning when opening brew in your Google Drive trash
|
||||||
@@ -57,9 +520,6 @@ myStyle {color: black}
|
|||||||
##### G-Ambatte :
|
##### G-Ambatte :
|
||||||
- Snippet to remove drop caps (fancy first letter after title)
|
- Snippet to remove drop caps (fancy first letter after title)
|
||||||
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
### Saturday, 13/3/2021 - v2.11.0
|
### Saturday, 13/3/2021 - v2.11.0
|
||||||
|
|
||||||
- Many background things for upcoming v3. Get pumped.
|
- Many background things for upcoming v3. Get pumped.
|
||||||
@@ -93,6 +553,8 @@ myStyle {color: black}
|
|||||||
### Wednesday, 25/11/2020 - v2.10.4
|
### Wednesday, 25/11/2020 - v2.10.4
|
||||||
- Fixed Google Drive brews not saving metadata (view count, description, etc.) Note that we are still working on making published Google brews visible to the public when viewing your profile page.
|
- Fixed Google Drive brews not saving metadata (view count, description, etc.) Note that we are still working on making published Google brews visible to the public when viewing your profile page.
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
### Thursday, 22/10/2020 - v2.10.3
|
### Thursday, 22/10/2020 - v2.10.3
|
||||||
- Fixed brews with broken code crashing the edit page when loaded (the "blue screen of death" bug).
|
- Fixed brews with broken code crashing the edit page when loaded (the "blue screen of death" bug).
|
||||||
|
|
||||||
@@ -103,8 +565,6 @@ myStyle {color: black}
|
|||||||
- Fixed issue with users unable to create new brews
|
- Fixed issue with users unable to create new brews
|
||||||
- Fixing brews being lost when loaded via back button
|
- Fixing brews being lost when loaded via back button
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Wednesday, 07/10/2020 - v2.10.0
|
### Wednesday, 07/10/2020 - v2.10.0
|
||||||
- Google Drive integration -- Sign in with your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal Google Drive storage, and Google will keep a backup of each version! No more lost work surprises!
|
- Google Drive integration -- Sign in with your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal Google Drive storage, and Google will keep a backup of each version! No more lost work surprises!
|
||||||
|
|
||||||
@@ -134,8 +594,10 @@ myStyle {color: black}
|
|||||||
- "Report Issue" navbar button now links to the subreddit
|
- "Report Issue" navbar button now links to the subreddit
|
||||||
- Refactored background code
|
- Refactored background code
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Sunday, 04/06/2017 - v2.7.5
|
### Sunday, 04/06/2017 - v2.7.5
|
||||||
- Fixed the class feature snippet duplicating the entire brew
|
- Fixed Class Feature snippet duplicating entire brew
|
||||||
- Fixed headers in tables being duplicated
|
- Fixed headers in tables being duplicated
|
||||||
- Fixed border-image being scrambled on class tables and descriptive text boxes
|
- Fixed border-image being scrambled on class tables and descriptive text boxes
|
||||||
- Fixed pages going out of sync in large brews, causing them to be rendered off-page
|
- Fixed pages going out of sync in large brews, causing them to be rendered off-page
|
||||||
@@ -161,7 +623,7 @@ myStyle {color: black}
|
|||||||
|
|
||||||
### Sunday, 25/12/2016 - v2.7.0
|
### Sunday, 25/12/2016 - v2.7.0
|
||||||
- Switching over to using Vitreum v4
|
- Switching over to using Vitreum v4
|
||||||
- Removed gulp, all tasks are run through npm scripts
|
- Removed gulp, all tasks are run through npm scripts
|
||||||
- Updating docs for local dev
|
- Updating docs for local dev
|
||||||
- Removing support for Docker. I have never used it, nor will I ever test for it, so I don't want to continue to explictly support it on this repo. Feel free to make a fork and make it docker-able though :)
|
- Removing support for Docker. I have never used it, nor will I ever test for it, so I don't want to continue to explictly support it on this repo. Feel free to make a fork and make it docker-able though :)
|
||||||
- Changed icon for the metadata
|
- Changed icon for the metadata
|
||||||
@@ -171,6 +633,8 @@ myStyle {color: black}
|
|||||||
- Removed a lot of unused files in shared
|
- Removed a lot of unused files in shared
|
||||||
- vitreum v4 now lets me use codemirror as a pure node dependacy
|
- vitreum v4 now lets me use codemirror as a pure node dependacy
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
### Saturday, 03/12/2016 - v2.6.0
|
### Saturday, 03/12/2016 - v2.6.0
|
||||||
- Added report back to the edit page
|
- Added report back to the edit page
|
||||||
- Changed metaeditor icon
|
- Changed metaeditor icon
|
||||||
@@ -191,8 +655,6 @@ myStyle {color: black}
|
|||||||
- Added a hover tooltip to fully read the brew description
|
- Added a hover tooltip to fully read the brew description
|
||||||
- Made the brew items take up only 25% allowing you to view more per row.
|
- Made the brew items take up only 25% allowing you to view more per row.
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Wednesday, 23/11/2016 - v2.5.0
|
### Wednesday, 23/11/2016 - v2.5.0
|
||||||
- Metadata can now be added to brews
|
- Metadata can now be added to brews
|
||||||
- Added a metadata editor onto the edit and new pages
|
- Added a metadata editor onto the edit and new pages
|
||||||
@@ -212,6 +674,8 @@ myStyle {color: black}
|
|||||||
- Added final touches to the html validator and updating the rest of the branch
|
- Added final touches to the html validator and updating the rest of the branch
|
||||||
- If anyone finds issues with the new HTML validator, please let me know. I hope this will bring a more consistent feel to Homebrewery rendering.
|
- If anyone finds issues with the new HTML validator, please let me know. I hope this will bring a more consistent feel to Homebrewery rendering.
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Friday, 09/09/2016 - v2.4.0
|
### Friday, 09/09/2016 - v2.4.0
|
||||||
- Adding in a HTML validator that will display warnings whenever you save. This should stop a lot of the issues generated with pages not showing up.
|
- Adding in a HTML validator that will display warnings whenever you save. This should stop a lot of the issues generated with pages not showing up.
|
||||||
|
|
||||||
@@ -239,7 +703,7 @@ myStyle {color: black}
|
|||||||
- Even works after you print to pdf!
|
- Even works after you print to pdf!
|
||||||
|
|
||||||
### Tuesday, 07/06/2016 - v2.2.2
|
### Tuesday, 07/06/2016 - v2.2.2
|
||||||
- Fixed bug with new markdown lexer and aprser not working on print page
|
- Fixed bug with new markdown lexer and parser not working on print page
|
||||||
|
|
||||||
### Sunday, 05/06/2016 - v2.2.1
|
### Sunday, 05/06/2016 - v2.2.1
|
||||||
- Adding in a new Class table div block. The old Class table block used weird stacking of HTML elements, resulting is difficult to control behaviour and poor interactiosn with the rest of the page. This new block is much easier to style and work with.
|
- Adding in a new Class table div block. The old Class table block used weird stacking of HTML elements, resulting is difficult to control behaviour and poor interactiosn with the rest of the page. This new block is much easier to style and work with.
|
||||||
@@ -247,8 +711,10 @@ myStyle {color: black}
|
|||||||
- Added in a new auto-incremeting page number snippet (thakns u/Ryrok!)
|
- Added in a new auto-incremeting page number snippet (thakns u/Ryrok!)
|
||||||
- Lists in monster stat blocks should be fixed now
|
- Lists in monster stat blocks should be fixed now
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
### Saturday, 04/06/2016 - v2.2.0
|
### Saturday, 04/06/2016 - v2.2.0
|
||||||
- MIgrating The Homebrewery over to hombrewery.naturalcrit.com. It know runs on it's own server, with it's own repo separate from the other tools I'm working on. Makes updating and deploying much easier.
|
- Migrating The Homebrewery over to hombrewery.naturalcrit.com. It now runs on it's own server, with it's own repo separate from the other tools I'm working on. Makes updating and deploying much easier.
|
||||||
|
|
||||||
### Sunday, 29/05/2016 - v2.1.0
|
### Sunday, 29/05/2016 - v2.1.0
|
||||||
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const PAGE_HEIGHT = 1056;
|
|||||||
const PPR_THRESHOLD = 50;
|
const PPR_THRESHOLD = 50;
|
||||||
|
|
||||||
const BrewRenderer = createClass({
|
const BrewRenderer = createClass({
|
||||||
|
displayName : 'BrewRenderer',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
text : '',
|
text : '',
|
||||||
@@ -30,7 +31,7 @@ const BrewRenderer = createClass({
|
|||||||
if(this.props.renderer == 'legacy') {
|
if(this.props.renderer == 'legacy') {
|
||||||
pages = this.props.text.split('\\page');
|
pages = this.props.text.split('\\page');
|
||||||
} else {
|
} else {
|
||||||
pages = this.props.text.split(/^\\page/gm);
|
pages = this.props.text.split(/^\\page$/gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -62,7 +63,7 @@ const BrewRenderer = createClass({
|
|||||||
if(this.props.renderer == 'legacy') {
|
if(this.props.renderer == 'legacy') {
|
||||||
pages = this.props.text.split('\\page');
|
pages = this.props.text.split('\\page');
|
||||||
} else {
|
} else {
|
||||||
pages = this.props.text.split(/^\\page/gm);
|
pages = this.props.text.split(/^\\page$/gm);
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
pages : pages,
|
pages : pages,
|
||||||
@@ -117,7 +118,7 @@ const BrewRenderer = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderDummyPage : function(index){
|
renderDummyPage : function(index){
|
||||||
return <div className='phb' id={`p${index + 1}`} key={index}>
|
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
||||||
<i className='fas fa-spinner fa-spin' />
|
<i className='fas fa-spinner fa-spin' />
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
@@ -130,8 +131,14 @@ const BrewRenderer = createClass({
|
|||||||
renderPage : function(pageText, index){
|
renderPage : function(pageText, index){
|
||||||
if(this.props.renderer == 'legacy')
|
if(this.props.renderer == 'legacy')
|
||||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
||||||
else
|
else {
|
||||||
return <div className='phb3 page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
|
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
|
return (
|
||||||
|
<div className='page' id={`p${index + 1}`} key={index} >
|
||||||
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPages : function(){
|
renderPages : function(){
|
||||||
@@ -182,7 +189,6 @@ const BrewRenderer = createClass({
|
|||||||
: null}
|
: null}
|
||||||
|
|
||||||
<Frame initialContent={this.state.initialContent}
|
<Frame initialContent={this.state.initialContent}
|
||||||
head = <link href={`${this.props.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
|
||||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
||||||
contentDidMount={this.frameDidMount}>
|
contentDidMount={this.frameDidMount}>
|
||||||
<div className={'brewRenderer'}
|
<div className={'brewRenderer'}
|
||||||
@@ -194,17 +200,17 @@ const BrewRenderer = createClass({
|
|||||||
<RenderWarnings />
|
<RenderWarnings />
|
||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
|
<link href={`${this.props.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||||
<div className='pages' ref='pages'>
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{this.state.isMounted
|
||||||
{this.state.isMounted
|
&&
|
||||||
&&
|
<>
|
||||||
<>
|
{this.renderStyle()}
|
||||||
{this.renderStyle()}
|
<div className='pages' ref='pages'>
|
||||||
{this.renderPages()}
|
{this.renderPages()}
|
||||||
</>
|
</div>
|
||||||
}
|
</>
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
{this.renderPageInfo()}
|
{this.renderPageInfo()}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const _ = require('lodash');
|
|||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
const ErrorBar = createClass({
|
const ErrorBar = createClass({
|
||||||
|
displayName : 'ErrorBar',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
errors : []
|
errors : []
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames'); //Unused variable
|
const cx = require('classnames'); //Unused variable
|
||||||
|
|
||||||
const DISMISS_KEY = 'dismiss_notification7-10-20';
|
const DISMISS_KEY = 'dismiss_notification09-9-21';
|
||||||
|
|
||||||
const NotificationPopup = createClass({
|
const NotificationPopup = createClass({
|
||||||
|
displayName : 'NotificationPopup',
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
notifications : {}
|
notifications : {}
|
||||||
@@ -22,22 +23,39 @@ const NotificationPopup = createClass({
|
|||||||
notifications : {
|
notifications : {
|
||||||
psa : function(){
|
psa : function(){
|
||||||
return <li key='psa'>
|
return <li key='psa'>
|
||||||
<em>Google Drive Integration!</em> <br />
|
<em>V3.0.0 Released!</em> <br />
|
||||||
We have added Google Drive integration to the Homebrewery! <a target='_blank' href='https://www.naturalcrit.com/login'>Sign in</a> with
|
After a long and bumpy road, we decided it was high time we finally release version 3 of the homebrewery into the wild. You can check out a
|
||||||
your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal
|
brief overview and see how to opt-in to the new features here:
|
||||||
Google Drive storage, and Google will keep a backup of each version! No more lost work surprises!
|
<a target='_blank' href='https://homebrewery.naturalcrit.com/v3_preview'>V3 Welcome Page</a> and
|
||||||
|
<a target='_blank' href='https://homebrewery.naturalcrit.com/changelog'>the Changelog</a>.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
However, we are aware that there may be uncaught bugs. We encourage you to copy your brew into a text document before transferring to Google
|
<em>BE WARNED:</em> As we continue to develop V3, expect small tweaks in the styling, fonts, and snippets; your brews may look slightly
|
||||||
Drive just in case any issues arise as this update is rolled out.
|
different from day-to-day. All of your old documents will continue to work as normal; we are not touching them. If you don't want to deal
|
||||||
|
with the possibility of slight formatting changes, you may choose to stick with the Legacy renderer on any of your brews for as long as you like.
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<b>Note:</b> Transferring an existing brew to Google Drive will change the edit and share links of your document. If you have shared your
|
With this in mind, if you still wish to try out V3, you can opt-in any of your brews to the the V3 renderer.
|
||||||
document online, remember to update the links there as well.
|
This will likely break much of your formatting as a lot of the Markdown code has been updated, and starting from scratch may be cleaner.
|
||||||
|
(Don't worry, you can always change the renderer back to Legacy for any brew at any time).
|
||||||
|
</li>;
|
||||||
|
},
|
||||||
|
refreshGoogle : function (){
|
||||||
|
return <li key='refreshGoogle'>
|
||||||
|
<em>Refresh your Google Drive Credentials!</em> <br />
|
||||||
|
Currently a lot of people are striking issues with their Google credentials expiring, which happens one year after the last sign in via
|
||||||
|
Google. This can cause errors when trying to save your brews. If this happens, simply visit the
|
||||||
|
<a target='_blank' href='https://www.naturalcrit.com/login'>
|
||||||
|
logout page
|
||||||
|
</a>
|
||||||
|
, sign out, and then sign back in "with Google" to refresh your credentials. See
|
||||||
|
<a target='_blank' href='https://github.com/naturalcrit/homebrewery/discussions/1580'>
|
||||||
|
this discussion on Github
|
||||||
|
</a> for more details.
|
||||||
</li>;
|
</li>;
|
||||||
},
|
},
|
||||||
faq : function(){
|
faq : function(){
|
||||||
return <li key='faq'>
|
return <li key='faq'>
|
||||||
<em>Protect your work! </em> <br />
|
<em>Protect your work! </em> <br />
|
||||||
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
||||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||||
See the FAQ
|
See the FAQ
|
||||||
</a> to learn how to avoid losing your work!
|
</a> to learn how to avoid losing your work!
|
||||||
@@ -62,8 +80,10 @@ const NotificationPopup = createClass({
|
|||||||
return <div className='notificationPopup'>
|
return <div className='notificationPopup'>
|
||||||
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||||
<i className='fas fa-info-circle info' />
|
<i className='fas fa-info-circle info' />
|
||||||
<h3>Notice</h3>
|
<div className='header'>
|
||||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
<h3>Notice</h3>
|
||||||
|
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||||
|
</div>
|
||||||
<ul>{_.values(this.state.notifications)}</ul>
|
<ul>{_.values(this.state.notifications)}</ul>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
.popups{
|
.popups{
|
||||||
position : fixed;
|
position : fixed;
|
||||||
top : @navbarHeight;
|
top : @navbarHeight;
|
||||||
right : 15px;
|
right : 15px;
|
||||||
z-index : 10001;
|
z-index : 10001;
|
||||||
width : 350px;
|
width : 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notificationPopup{
|
.notificationPopup{
|
||||||
position : relative;
|
position : relative;
|
||||||
float : right;
|
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
width : 350px;
|
width : 100%;
|
||||||
padding : 15px;
|
padding : 15px;
|
||||||
padding-bottom : 10px;
|
padding-bottom : 10px;
|
||||||
padding-left : 55px;
|
padding-left : 25px;
|
||||||
background-color : @blue;
|
background-color : @blue;
|
||||||
color : white;
|
color : white;
|
||||||
a{
|
a{
|
||||||
color : @steel;
|
color : #e0e5c1;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
}
|
}
|
||||||
i.info{
|
i.info{
|
||||||
@@ -37,6 +36,9 @@
|
|||||||
opacity : 1;
|
opacity : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.header {
|
||||||
|
padding-left : 50px;
|
||||||
|
}
|
||||||
small{
|
small{
|
||||||
opacity : 0.7;
|
opacity : 0.7;
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./editor.less');
|
require('./editor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -25,6 +26,7 @@ const splice = function(str, index, inject){
|
|||||||
|
|
||||||
|
|
||||||
const Editor = createClass({
|
const Editor = createClass({
|
||||||
|
displayName : 'Editor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
@@ -59,6 +61,10 @@ const Editor = createClass({
|
|||||||
window.removeEventListener('resize', this.updateEditorSize);
|
window.removeEventListener('resize', this.updateEditorSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidUpdate : function() {
|
||||||
|
this.highlightCustomMarkdown();
|
||||||
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
if(this.refs.codeEditor) {
|
if(this.refs.codeEditor) {
|
||||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||||
@@ -68,15 +74,18 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleInject : function(injectText){
|
handleInject : function(injectText){
|
||||||
const text = (this.isText() ? this.props.brew.text : this.props.brew.style);
|
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 lines = text.split('\n');
|
||||||
const cursorPos = this.refs.codeEditor.getCursorPosition();
|
const cursorPos = this.refs.codeEditor.getCursorPosition();
|
||||||
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
|
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
|
||||||
|
|
||||||
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectText.split('\n').length, cursorPos.ch + injectText.length);
|
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.isText()) this.props.onTextChange(lines.join('\n'));
|
||||||
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
|
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -99,67 +108,69 @@ const Editor = createClass({
|
|||||||
if(this.state.view === 'text') {
|
if(this.state.view === 'text') {
|
||||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||||
|
|
||||||
//reset custom text styles
|
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||||
const customHighlights = codeMirror.getAllMarks();
|
//reset custom text styles
|
||||||
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
|
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
|
||||||
|
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||||
|
|
||||||
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
|
let editorPageCount = 2; // start page count from page 2
|
||||||
|
|
||||||
//reset custom line styles
|
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
||||||
codeMirror.removeLineClass(lineNumber, 'background');
|
|
||||||
codeMirror.removeLineClass(lineNumber, 'text');
|
|
||||||
|
|
||||||
// Legacy Codemirror styling
|
//reset custom line styles
|
||||||
if(this.props.renderer == 'legacy') {
|
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
||||||
if(line.includes('\\page')){
|
codeMirror.removeLineClass(lineNumber, 'text');
|
||||||
|
|
||||||
|
// Styling for \page breaks
|
||||||
|
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||||
|
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
||||||
|
|
||||||
|
// add back the original class 'background' but also add the new class '.pageline'
|
||||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||||
r.push(lineNumber);
|
const pageCountElement = Object.assign(document.createElement('span'), {
|
||||||
}
|
className : 'editor-page-count',
|
||||||
}
|
textContent : editorPageCount
|
||||||
|
});
|
||||||
|
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||||
|
|
||||||
// New Codemirror styling for V3 renderer
|
editorPageCount += 1;
|
||||||
if(this.props.renderer == 'V3') {
|
};
|
||||||
if(line.startsWith('\\page')){
|
|
||||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
|
||||||
r.push(lineNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(line.match(/^\\column$/)){
|
// New Codemirror styling for V3 renderer
|
||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
if(this.props.renderer == 'V3') {
|
||||||
r.push(lineNumber);
|
if(line.match(/^\\column$/)){
|
||||||
}
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
|
|
||||||
// Highlight inline spans {{content}}
|
|
||||||
if(line.includes('{{') && line.includes('}}')){
|
|
||||||
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
|
||||||
let match;
|
|
||||||
let blockCount = 0;
|
|
||||||
while ((match = regex.exec(line)) != null) {
|
|
||||||
if(match[0].startsWith('{')) {
|
|
||||||
blockCount += 1;
|
|
||||||
} else {
|
|
||||||
blockCount -= 1;
|
|
||||||
}
|
|
||||||
if(blockCount < 0) {
|
|
||||||
blockCount = 0;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
|
|
||||||
}
|
}
|
||||||
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
|
|
||||||
// Highlight block divs {{\n Content \n}}
|
|
||||||
let endCh = line.length+1;
|
|
||||||
|
|
||||||
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
|
// Highlight inline spans {{content}}
|
||||||
if(match)
|
if(line.includes('{{') && line.includes('}}')){
|
||||||
endCh = match.index+match[0].length;
|
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
||||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
let match;
|
||||||
|
let blockCount = 0;
|
||||||
|
while ((match = regex.exec(line)) != null) {
|
||||||
|
if(match[0].startsWith('{')) {
|
||||||
|
blockCount += 1;
|
||||||
|
} else {
|
||||||
|
blockCount -= 1;
|
||||||
|
}
|
||||||
|
if(blockCount < 0) {
|
||||||
|
blockCount = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
|
||||||
|
}
|
||||||
|
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
|
||||||
|
// Highlight block divs {{\n Content \n}}
|
||||||
|
let endCh = line.length+1;
|
||||||
|
|
||||||
|
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
|
||||||
|
if(match)
|
||||||
|
endCh = match.index+match[0].length;
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
});
|
||||||
return r;
|
|
||||||
}, []);
|
|
||||||
return lineNumbers;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -173,30 +184,61 @@ const Editor = createClass({
|
|||||||
this.refs.codeEditor?.updateSize();
|
this.refs.codeEditor?.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
||||||
|
rerenderParent : function (){
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
|
||||||
renderEditor : function(){
|
renderEditor : function(){
|
||||||
if(this.isText()){
|
if(this.isText()){
|
||||||
return <CodeEditor key='text'
|
return <>
|
||||||
ref='codeEditor'
|
<CodeEditor key='codeEditor'
|
||||||
language='gfm'
|
ref='codeEditor'
|
||||||
value={this.props.brew.text}
|
language='gfm'
|
||||||
onChange={this.props.onTextChange} />;
|
view={this.state.view}
|
||||||
|
value={this.props.brew.text}
|
||||||
|
onChange={this.props.onTextChange}
|
||||||
|
rerenderParent={this.rerenderParent} />
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isStyle()){
|
if(this.isStyle()){
|
||||||
return <CodeEditor key='style'
|
return <>
|
||||||
ref='codeEditor'
|
<CodeEditor key='codeEditor'
|
||||||
language='css'
|
ref='codeEditor'
|
||||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
language='css'
|
||||||
onChange={this.props.onStyleChange} />;
|
view={this.state.view}
|
||||||
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
|
onChange={this.props.onStyleChange}
|
||||||
|
enableFolding={false}
|
||||||
|
rerenderParent={this.rerenderParent} />
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isMeta()){
|
if(this.isMeta()){
|
||||||
return <MetadataEditor
|
return <>
|
||||||
metadata={this.props.brew}
|
<CodeEditor key='codeEditor'
|
||||||
onChange={this.props.onMetaChange} />;
|
view={this.state.view}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
rerenderParent={this.rerenderParent} />
|
||||||
|
<MetadataEditor
|
||||||
|
metadata={this.props.brew}
|
||||||
|
onChange={this.props.onMetaChange} />
|
||||||
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
redo : function(){
|
||||||
|
return this.refs.codeEditor?.redo();
|
||||||
|
},
|
||||||
|
|
||||||
|
historySize : function(){
|
||||||
|
return this.refs.codeEditor?.historySize();
|
||||||
|
},
|
||||||
|
|
||||||
|
undo : function(){
|
||||||
|
return this.refs.codeEditor?.undo();
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
this.highlightCustomMarkdown();
|
|
||||||
return (
|
return (
|
||||||
<div className='editor' ref='main'>
|
<div className='editor' ref='main'>
|
||||||
<SnippetBar
|
<SnippetBar
|
||||||
@@ -205,7 +247,10 @@ const Editor = createClass({
|
|||||||
onViewChange={this.handleViewChange}
|
onViewChange={this.handleViewChange}
|
||||||
onInject={this.handleInject}
|
onInject={this.handleInject}
|
||||||
showEditButtons={this.props.showEditButtons}
|
showEditButtons={this.props.showEditButtons}
|
||||||
renderer={this.props.renderer} />
|
renderer={this.props.renderer}
|
||||||
|
undo={this.undo}
|
||||||
|
redo={this.redo}
|
||||||
|
historySize={this.historySize()} />
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,42 +4,58 @@
|
|||||||
width : 100%;
|
width : 100%;
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor{
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.pageLine{
|
.pageLine{
|
||||||
background-color : fade(#333, 15%);
|
background : #33333328;
|
||||||
border-bottom : #333 solid 1px;
|
border-top : #339 solid 1px;
|
||||||
|
}
|
||||||
|
.editor-page-count{
|
||||||
|
color : grey;
|
||||||
|
float : right;
|
||||||
}
|
}
|
||||||
.columnSplit{
|
.columnSplit{
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
color : grey;
|
color : grey;
|
||||||
background-color : fade(#299, 15%);
|
background-color : fade(#299, 15%);
|
||||||
border-bottom : #299 solid 1px;
|
border-bottom : #299 solid 1px;
|
||||||
}
|
}
|
||||||
.block{
|
.block{
|
||||||
color : purple;
|
color : purple;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.inline-block{
|
.inline-block{
|
||||||
color : red;
|
color : red;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brewJump{
|
.brewJump{
|
||||||
position: absolute;
|
position : absolute;
|
||||||
background-color: @teal;
|
background-color : @teal;
|
||||||
cursor: pointer;
|
cursor : pointer;
|
||||||
width : 30px;
|
width : 30px;
|
||||||
height : 30px;
|
height : 30px;
|
||||||
display : flex;
|
display : flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
bottom : 20px;
|
bottom : 20px;
|
||||||
right : 20px;
|
right : 20px;
|
||||||
z-index: 1000000;
|
z-index : 1000000;
|
||||||
justify-content:center;
|
justify-content : center;
|
||||||
.tooltipLeft("Jump to brew page");
|
.tooltipLeft("Jump to brew page");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editorToolbar{
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 50%;
|
||||||
|
color: black;
|
||||||
|
font-size: 13px;
|
||||||
|
z-index: 9;
|
||||||
|
span {
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const request = require('superagent');
|
|||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
const MetadataEditor = createClass({
|
const MetadataEditor = createClass({
|
||||||
|
displayName : 'MetadataEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
metadata : {
|
metadata : {
|
||||||
@@ -58,25 +59,13 @@ const MetadataEditor = createClass({
|
|||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.delete(`/api/${this.props.metadata.editId}`)
|
request.delete(`/api/${this.props.metadata.googleId}${this.props.metadata.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end(function(err, res){
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getRedditLink : function(){
|
|
||||||
const meta = this.props.metadata;
|
|
||||||
|
|
||||||
const shareLink = (meta.googleId || '') + meta.shareId;
|
|
||||||
const title = `${meta.title} [${meta.systems.join(' ')}]`;
|
|
||||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
|
||||||
|
|
||||||
**[Homebrewery Link](https://homebrewery.naturalcrit.com/share/${shareLink})**`;
|
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderSystems : function(){
|
renderSystems : function(){
|
||||||
return _.map(SYSTEMS, (val)=>{
|
return _.map(SYSTEMS, (val)=>{
|
||||||
return <label key={val}>
|
return <label key={val}>
|
||||||
@@ -127,21 +116,6 @@ const MetadataEditor = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShareToReddit : function(){
|
|
||||||
if(!this.props.metadata.shareId) return;
|
|
||||||
|
|
||||||
return <div className='field reddit'>
|
|
||||||
<label>reddit</label>
|
|
||||||
<div className='value'>
|
|
||||||
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
|
|
||||||
<button className='publish'>
|
|
||||||
<i className='fab fa-reddit-alien' /> share to reddit
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderRenderOptions : function(){
|
renderRenderOptions : function(){
|
||||||
if(!global.enable_v3) return;
|
if(!global.enable_v3) return;
|
||||||
|
|
||||||
@@ -167,6 +141,10 @@ const MetadataEditor = createClass({
|
|||||||
onChange={(e)=>this.handleRenderer('V3', e)} />
|
onChange={(e)=>this.handleRenderer('V3', e)} />
|
||||||
V3
|
V3
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<a href='/v3_preview' target='_blank' rel='noopener noreferrer'>
|
||||||
|
Click here for a quick intro to V3!
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
@@ -211,8 +189,6 @@ const MetadataEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this.renderShareToReddit()}
|
|
||||||
|
|
||||||
{this.renderDelete()}
|
{this.renderDelete()}
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -43,6 +43,11 @@
|
|||||||
display : inline-flex;
|
display : inline-flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
}
|
}
|
||||||
|
a {
|
||||||
|
font-size : 0.7em;
|
||||||
|
font-weight : 800;
|
||||||
|
display : inline-flex;
|
||||||
|
}
|
||||||
input{
|
input{
|
||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
@@ -62,9 +67,6 @@
|
|||||||
.button(@silver);
|
.button(@silver);
|
||||||
}
|
}
|
||||||
small{
|
small{
|
||||||
position : absolute;
|
|
||||||
bottom : -15px;
|
|
||||||
left : 0px;
|
|
||||||
font-size : 0.6em;
|
font-size : 0.6em;
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
}
|
}
|
||||||
@@ -75,11 +77,6 @@
|
|||||||
.button(@red);
|
.button(@red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.reddit.field .value{
|
|
||||||
button{
|
|
||||||
.button(@purple);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.authors.field .value{
|
.authors.field .value{
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const execute = function(val, brew){
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Snippetbar = createClass({
|
const Snippetbar = createClass({
|
||||||
|
displayName : 'SnippetBar',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {},
|
brew : {},
|
||||||
@@ -22,7 +23,10 @@ const Snippetbar = createClass({
|
|||||||
onInject : ()=>{},
|
onInject : ()=>{},
|
||||||
onToggle : ()=>{},
|
onToggle : ()=>{},
|
||||||
showEditButtons : true,
|
showEditButtons : true,
|
||||||
renderer : 'legacy'
|
renderer : 'legacy',
|
||||||
|
undo : ()=>{},
|
||||||
|
redo : ()=>{},
|
||||||
|
historySize : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -39,12 +43,10 @@ const Snippetbar = createClass({
|
|||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
let snippets = [];
|
let snippets = [];
|
||||||
|
|
||||||
if(this.props.view === 'text') {
|
if(this.props.renderer === 'V3')
|
||||||
if(this.props.renderer === 'V3')
|
snippets = SnippetsV3.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
snippets = SnippetsV3;
|
else
|
||||||
else
|
snippets = SnippetsLegacy.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
snippets = SnippetsLegacy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.map(snippets, (snippetGroup)=>{
|
return _.map(snippets, (snippetGroup)=>{
|
||||||
return <SnippetGroup
|
return <SnippetGroup
|
||||||
@@ -62,6 +64,15 @@ const Snippetbar = createClass({
|
|||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
return <div className='editors'>
|
return <div className='editors'>
|
||||||
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
|
onClick={this.props.undo} >
|
||||||
|
<i className='fas fa-undo' />
|
||||||
|
</div>
|
||||||
|
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
|
||||||
|
onClick={this.props.redo} >
|
||||||
|
<i className='fas fa-redo' />
|
||||||
|
</div>
|
||||||
|
<div className='divider'></div>
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
onClick={()=>this.props.onViewChange('text')}>
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
<i className='fa fa-beer' />
|
<i className='fa fa-beer' />
|
||||||
@@ -93,6 +104,7 @@ module.exports = Snippetbar;
|
|||||||
|
|
||||||
|
|
||||||
const SnippetGroup = createClass({
|
const SnippetGroup = createClass({
|
||||||
|
displayName : 'SnippetGroup',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {},
|
brew : {},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
top : 0px;
|
top : 0px;
|
||||||
right : 0px;
|
right : 0px;
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
width : 90px;
|
width : 125px;
|
||||||
justify-content : space-between;
|
justify-content : space-between;
|
||||||
&>div{
|
&>div{
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
@@ -30,6 +30,29 @@
|
|||||||
&.meta{
|
&.meta{
|
||||||
.tooltipLeft('Properties');
|
.tooltipLeft('Properties');
|
||||||
}
|
}
|
||||||
|
&.undo{
|
||||||
|
.tooltipLeft('Undo');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
&.active{
|
||||||
|
color : black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.redo{
|
||||||
|
.tooltipLeft('Redo');
|
||||||
|
font-size : 0.75em;
|
||||||
|
color : grey;
|
||||||
|
&.active{
|
||||||
|
color : black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.divider {
|
||||||
|
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
||||||
|
width: 5px;
|
||||||
|
&:hover{
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.snippetBarButton{
|
.snippetBarButton{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
module.exports = function(classname){
|
module.exports = function(classname){
|
||||||
|
|
||||||
@@ -10,33 +11,32 @@ module.exports = function(classname){
|
|||||||
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
||||||
|
|
||||||
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
||||||
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
const skillList = ['Acrobatics', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
||||||
|
|
||||||
|
|
||||||
return [
|
return dedent`
|
||||||
'## Class Features',
|
## Class Features
|
||||||
`As a ${classname}, you gain the following class features`,
|
As a ${classname}, you gain the following class features
|
||||||
'#### Hit Points',
|
#### Hit Points
|
||||||
'___',
|
|
||||||
`- **Hit Dice:** 1d${hitDie} per ${classname} level`,
|
**Hit Dice:** :: 1d${hitDie} per ${classname} level
|
||||||
`- **Hit Points at 1st Level:** ${hitDie} + your Constitution modifier`,
|
**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`,
|
**Hit Points at Higher Levels:** :: 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st
|
||||||
'',
|
|
||||||
'#### Proficiencies',
|
#### Proficiencies
|
||||||
'___',
|
|
||||||
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
|
**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'}`,
|
**Weapons:** :: ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}
|
||||||
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
**Tools:** :: ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}
|
||||||
'',
|
|
||||||
'___',
|
**Saving Throws:** :: ${_.sampleSize(abilityList, 2).join(', ')}
|
||||||
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
**Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}
|
||||||
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
|
||||||
'',
|
#### Equipment
|
||||||
'#### Equipment',
|
You start with the following equipment, in addition to the equipment granted by your background:
|
||||||
'You start with the following equipment, in addition to the equipment granted by your background:',
|
- *(a)* a martial weapon and a shield or *(b)* two martial weapons
|
||||||
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',
|
- *(a)* five javelins or *(b)* any simple melee weapon
|
||||||
'- *(a)* five javelins or *(b)* any simple melee weapon',
|
- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}
|
||||||
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
|
|
||||||
'\n\n\n'
|
`;
|
||||||
].join('\n');
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,85 +2,77 @@ const _ = require('lodash');
|
|||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
'Astrological Botany',
|
'Astrological Botany',
|
||||||
'Astrological Chemistry',
|
|
||||||
'Biochemical Sorcery',
|
'Biochemical Sorcery',
|
||||||
'Civil Alchemy',
|
'Civil Divination',
|
||||||
'Consecrated Biochemistry',
|
'Consecrated Augury',
|
||||||
'Demonic Anthropology',
|
'Demonic Anthropology',
|
||||||
'Divinatory Mineralogy',
|
'Divinatory Mineralogy',
|
||||||
'Genetic Banishing',
|
|
||||||
'Hermetic Geography',
|
|
||||||
'Immunological Incantations',
|
|
||||||
'Nuclear Illusionism',
|
|
||||||
'Ritual Astronomy',
|
|
||||||
'Seismological Divination',
|
|
||||||
'Spiritual Biochemistry',
|
|
||||||
'Statistical Occultism',
|
|
||||||
'Police Necromancer',
|
|
||||||
'Sixgun Poisoner',
|
|
||||||
'Pharmaceutical Gunslinger',
|
|
||||||
'Infernal Banker',
|
|
||||||
'Spell Analyst',
|
|
||||||
'Gunslinger Corruptor',
|
|
||||||
'Torque Interfacer',
|
|
||||||
'Exo Interfacer',
|
'Exo Interfacer',
|
||||||
|
'Genetic Banishing',
|
||||||
'Gunpowder Torturer',
|
'Gunpowder Torturer',
|
||||||
'Orbital Gravedigger',
|
'Gunslinger Corruptor',
|
||||||
'Phased Linguist',
|
'Hermetic Geography',
|
||||||
'Mathematical Pharmacist',
|
'Immunological Cultist',
|
||||||
'Plasma Outlaw',
|
|
||||||
'Malefic Chemist',
|
'Malefic Chemist',
|
||||||
'Police Cultist'
|
'Mathematical Pharmacy',
|
||||||
|
'Nuclear Biochemistry',
|
||||||
|
'Orbital Gravedigger',
|
||||||
|
'Pharmaceutical Outlaw',
|
||||||
|
'Phased Linguist',
|
||||||
|
'Plasma Gunslinger',
|
||||||
|
'Police Necromancer',
|
||||||
|
'Ritual Astronomy',
|
||||||
|
'Sixgun Poisoner',
|
||||||
|
'Seismological Alchemy',
|
||||||
|
'Spiritual Illusionism',
|
||||||
|
'Statistical Occultism',
|
||||||
|
'Spell Analyst',
|
||||||
|
'Torque Interfacer'
|
||||||
];
|
];
|
||||||
|
|
||||||
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
|
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
|
||||||
|
|
||||||
const levels = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th', '11th', '12th', '13th', '14th', '15th', '16th', '17th', '18th', '19th', '20th'];
|
const levels = ['1st', '2nd', '3rd', '4th', '5th',
|
||||||
|
'6th', '7th', '8th', '9th', '10th',
|
||||||
|
'11th', '12th', '13th', '14th', '15th',
|
||||||
|
'16th', '17th', '18th', '19th', '20th'];
|
||||||
|
|
||||||
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
|
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
|
||||||
|
|
||||||
const getFeature = (level)=>{
|
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
||||||
let res = [];
|
|
||||||
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
|
const drawSlots = function(Slots, rows, padding){
|
||||||
res = ['Ability Score Improvement'];
|
let slots = Number(Slots);
|
||||||
}
|
return _.times(rows, function(i){
|
||||||
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
|
const max = maxes[i];
|
||||||
if(!res.length) return '─';
|
if(slots < 1) return _.pad('—', padding);
|
||||||
return res.join(', ');
|
const res = _.min([max, slots]);
|
||||||
|
slots -= res;
|
||||||
|
return _.pad(res.toString(), padding);
|
||||||
|
}).join(' | ');
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
full : function(){
|
full : function(classes){
|
||||||
const classname = _.sample(classnames);
|
const classname = _.sample(classnames);
|
||||||
|
|
||||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
|
||||||
const drawSlots = function(Slots){
|
|
||||||
let slots = Number(Slots);
|
|
||||||
return _.times(9, function(i){
|
|
||||||
const max = maxes[i];
|
|
||||||
if(slots < 1) return '—';
|
|
||||||
const res = _.min([max, slots]);
|
|
||||||
slots -= res;
|
|
||||||
return res;
|
|
||||||
}).join(' | ');
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
let cantrips = 3;
|
let cantrips = 3;
|
||||||
let spells = 1;
|
let spells = 1;
|
||||||
let slots = 2;
|
let slots = 2;
|
||||||
return `<div class='classTable wide'>\n##### The ${classname}\n` +
|
return `{{${classes}\n##### The ${classname}\n` +
|
||||||
`| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
|
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
||||||
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
|
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
||||||
|
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
||||||
_.map(levels, function(levelName, level){
|
_.map(levels, function(levelName, level){
|
||||||
const res = [
|
const res = [
|
||||||
levelName,
|
_.pad(levelName, 5),
|
||||||
`+${profBonus[level]}`,
|
_.pad(`+${profBonus[level]}`, 2),
|
||||||
getFeature(level),
|
_.padEnd(_.sample(features), 21),
|
||||||
cantrips,
|
_.pad(cantrips.toString(), 8),
|
||||||
spells,
|
_.pad(spells.toString(), 6),
|
||||||
drawSlots(slots)
|
drawSlots(slots, 9, 2),
|
||||||
].join(' | ');
|
].join(' | ');
|
||||||
|
|
||||||
cantrips += _.random(0, 1);
|
cantrips += _.random(0, 1);
|
||||||
@@ -88,27 +80,53 @@ module.exports = {
|
|||||||
slots += _.random(0, 2);
|
slots += _.random(0, 2);
|
||||||
|
|
||||||
return `| ${res} |`;
|
return `| ${res} |`;
|
||||||
}).join('\n')}\n</div>\n\n`;
|
}).join('\n')}\n}}\n\n`;
|
||||||
},
|
},
|
||||||
|
|
||||||
half : function(){
|
half : function(classes){
|
||||||
const classname = _.sample(classnames);
|
const classname = _.sample(classnames);
|
||||||
|
|
||||||
let featureScore = 1;
|
let featureScore = 1;
|
||||||
return `<div class='classTable'>\n##### The ${classname}\n` +
|
return `{{${classes}\n##### The ${classname}\n` +
|
||||||
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
|
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
||||||
`|:---:|:---:|:---|:---:|\n${
|
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
||||||
_.map(levels, function(levelName, level){
|
_.map(levels, function(levelName, level){
|
||||||
const res = [
|
const res = [
|
||||||
levelName,
|
_.pad(levelName, 5),
|
||||||
`+${profBonus[level]}`,
|
_.pad(`+${profBonus[level]}`, 2),
|
||||||
getFeature(level),
|
_.padEnd(_.sample(features), 23),
|
||||||
`+${featureScore}`
|
_.pad(`+${featureScore}`, 21),
|
||||||
].join(' | ');
|
].join(' | ');
|
||||||
|
|
||||||
featureScore += _.random(0, 1);
|
featureScore += _.random(0, 1);
|
||||||
|
|
||||||
return `| ${res} |`;
|
return `| ${res} |`;
|
||||||
}).join('\n')}\n</div>\n\n`;
|
}).join('\n')}\n}}\n\n`;
|
||||||
|
},
|
||||||
|
|
||||||
|
third : function(classes){
|
||||||
|
const classname = _.sample(classnames);
|
||||||
|
|
||||||
|
let cantrips = 3;
|
||||||
|
let spells = 1;
|
||||||
|
let slots = 2;
|
||||||
|
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
||||||
|
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
||||||
|
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
||||||
|
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
||||||
|
_.map(levels, function(levelName, level){
|
||||||
|
const res = [
|
||||||
|
_.pad(levelName, 6),
|
||||||
|
_.pad(cantrips.toString(), 8),
|
||||||
|
_.pad(spells.toString(), 7),
|
||||||
|
drawSlots(slots, 4, 7),
|
||||||
|
].join(' | ');
|
||||||
|
|
||||||
|
cantrips += _.random(0, 1);
|
||||||
|
spells += _.random(0, 1);
|
||||||
|
slots += _.random(0, 1);
|
||||||
|
|
||||||
|
return `| ${res} |`;
|
||||||
|
}).join('\n')}\n}}\n\n`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -100,25 +100,25 @@ const subtitles = [
|
|||||||
|
|
||||||
module.exports = ()=>{
|
module.exports = ()=>{
|
||||||
return `<style>
|
return `<style>
|
||||||
.phb#p1{ text-align:center; }
|
.page#p1{ text-align:center; counter-increment: none; }
|
||||||
.phb#p1:after{ display:none; }
|
.page#p1:after{ display:none; }
|
||||||
.phb#p2 { counter-reset:phb-page-numbers; }
|
.page:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
|
||||||
.phb:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
|
.page:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
|
||||||
.phb:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
|
.page:nth-child(2n)::after { transform: scaleX(1); }
|
||||||
.phb:nth-child(2n)::after { transform: scaleX(1); }
|
.page:nth-child(2n+1)::after { transform: scaleX(-1); }
|
||||||
.phb:nth-child(2n+1)::after { transform: scaleX(-1); }
|
.page:nth-child(2n) .footnote { left: inherit; text-align: right; }
|
||||||
.phb:nth-child(2n) .footnote { left: inherit; text-align: right; }
|
.page:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
|
||||||
.phb:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div style='margin-top:450px;'></div>
|
{{margin-top:225px}}
|
||||||
|
|
||||||
# ${_.sample(titles)}
|
# ${_.sample(titles)}
|
||||||
|
|
||||||
<div style='margin-top:25px'></div>
|
{{margin-top:25px}}
|
||||||
<div class='wide'>
|
|
||||||
|
{{wide
|
||||||
##### ${_.sample(subtitles)}
|
##### ${_.sample(subtitles)}
|
||||||
</div>
|
}}
|
||||||
|
|
||||||
\\page`;
|
\\page`;
|
||||||
};
|
};
|
||||||
@@ -60,13 +60,13 @@ module.exports = {
|
|||||||
const levels = ['Cantrips (0 Level)', '1st Level', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
const levels = ['Cantrips (0 Level)', '1st Level', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
||||||
|
|
||||||
const content = _.map(levels, (level)=>{
|
const content = _.map(levels, (level)=>{
|
||||||
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
const spells = _.map(_.sampleSize(spellNames, _.random(4, 10)), (spell)=>{
|
||||||
return `- ${spell}`;
|
return `- ${spell}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
return `##### ${level} \n${spells} \n`;
|
return `##### ${level} \n${spells} \n`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
return `{{spellList\n${content}\n}}`;
|
return `{{spellList,wide\n${content}\n}}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
spell : function(){
|
spell : function(){
|
||||||
|
|||||||
@@ -105,6 +105,20 @@ const genAbilities = function(){
|
|||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const genLongAbilities = function(){
|
||||||
|
return _.sample([
|
||||||
|
dedent`***Pack Tactics.*** These guys work together like peanut butter and jelly. Jelly and peanut butter.
|
||||||
|
|
||||||
|
When one of these guys attacks, the target is covered with, well, peanut butter and jelly.`,
|
||||||
|
dedent`***Hangriness.*** This creature is angry, and hungry. It will refuse to do anything with you until its hunger is satisfied.
|
||||||
|
|
||||||
|
When in visual contact with this creature, you must purchase an extra order of fries, even if they say they aren't hungry.`,
|
||||||
|
dedent`***Full of Detergent.*** This creature has swallowed an entire bottle of dish detergent and is actually having a pretty good time.
|
||||||
|
|
||||||
|
While walking near this creature, you must make a dexterity check or become "a soapy mess" for three hours, after which your skin will get all dry and itchy.`
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
const genAction = function(){
|
const genAction = function(){
|
||||||
const name = _.sample([
|
const name = _.sample([
|
||||||
'Abdominal Drop',
|
'Abdominal Drop',
|
||||||
@@ -159,11 +173,11 @@ module.exports = {
|
|||||||
**Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
|
**Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
|
||||||
**Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP)
|
**Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP)
|
||||||
___
|
___
|
||||||
|
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n:\n')}
|
||||||
:
|
:
|
||||||
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n\t\t\t\n\t\t\t')}
|
${genLongAbilities()}
|
||||||
:
|
|
||||||
### Actions
|
### Actions
|
||||||
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n\t\t\t\n\t\t\t')}
|
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n:\n')}
|
||||||
}}
|
}}
|
||||||
\n`;
|
\n`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ const ClassFeatureGen = require('./classfeature.gen.js');
|
|||||||
const CoverPageGen = require('./coverpage.gen.js');
|
const CoverPageGen = require('./coverpage.gen.js');
|
||||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
const watercolorGen = require('./watercolor.gen.js');
|
||||||
|
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Editor',
|
groupName : 'Text Editor',
|
||||||
icon : 'fas fa-pencil-alt',
|
icon : 'fas fa-pencil-alt',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Column Break',
|
name : 'Column Break',
|
||||||
@@ -42,33 +44,20 @@ module.exports = [
|
|||||||
{{wide
|
{{wide
|
||||||
Everything in here will be extra wide. Tables, text, everything!
|
Everything in here will be extra wide. Tables, text, everything!
|
||||||
Beware though, CSS columns can behave a bit weird sometimes. You may
|
Beware though, CSS columns can behave a bit weird sometimes. You may
|
||||||
have to rely on the automatic column-break rather than \`\column\` if
|
have to manually place column breaks with \`\column\` to make the
|
||||||
you mix columns and wide blocks on the same page.
|
surrounding text flow with this wide block the way you want.
|
||||||
}}
|
}}
|
||||||
\n`
|
\n`
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name : 'Image',
|
|
||||||
icon : 'fas fa-image',
|
|
||||||
gen : dedent`
|
|
||||||
 {width:325px}
|
|
||||||
Credit: Kyounghwan Kim`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Background Image',
|
|
||||||
icon : 'fas fa-tree',
|
|
||||||
gen : ` {position:absolute,top:50px,right:30px,width:280px}`
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name : 'QR Code',
|
name : 'QR Code',
|
||||||
icon : 'fas fa-qrcode',
|
icon : 'fas fa-qrcode',
|
||||||
gen : (brew)=>{
|
gen : (brew)=>{
|
||||||
return `![]` +
|
return `![]` +
|
||||||
`(https://api.qrserver.com/v1/create-qr-code/?data=` +
|
`(https://api.qrserver.com/v1/create-qr-code/?data=` +
|
||||||
`https://homebrewery.naturalcrit.com/share/${brew.shareId}` +
|
`https://homebrewery.naturalcrit.com${brew.shareId ? `/share/${brew.shareId}` : ''}` +
|
||||||
`&size=100x100) {width:100px;mix-blend-mode:multiply}`;
|
`&size=100x100) {width:100px;mix-blend-mode:multiply}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Page Number',
|
name : 'Page Number',
|
||||||
@@ -90,28 +79,87 @@ module.exports = [
|
|||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
gen : TableOfContentsGen
|
gen : TableOfContentsGen
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
groupName : 'Style Editor',
|
||||||
|
icon : 'fas fa-pencil-alt',
|
||||||
|
view : 'style',
|
||||||
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Remove Drop Cap',
|
name : 'Remove Drop Cap',
|
||||||
icon : 'fas fa-remove-format',
|
icon : 'fas fa-remove-format',
|
||||||
gen : '<style>\n' +
|
gen : dedent`/* Removes Drop Caps */
|
||||||
' .phb3 h1+p:first-letter {\n' +
|
.page h1+p:first-letter {
|
||||||
' all: unset;\n' +
|
all: unset;
|
||||||
' }\n' +
|
}\n\n
|
||||||
'</style>'
|
/* Removes Small-Caps in first line */
|
||||||
|
.page h1+p:first-line {
|
||||||
|
all: unset;
|
||||||
|
}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Tweak Drop Cap',
|
name : 'Tweak Drop Cap',
|
||||||
icon : 'fas fa-sliders-h',
|
icon : 'fas fa-sliders-h',
|
||||||
gen : '<style>\n' +
|
gen : dedent`/* Drop Cap settings */
|
||||||
' /* Drop Cap settings */\n' +
|
.page h1 + p::first-letter {
|
||||||
' .phb3 h1 + p::first-letter {\n' +
|
font-family: SolberaImitationRemake;
|
||||||
' float: left;\n' +
|
font-size: 3.5cm;
|
||||||
' font-family: SolberaImitationRemake;\n' +
|
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||||
' font-size: 3.5cm;\n' +
|
line-height: 1em;
|
||||||
' color: #222;\n' +
|
}\n\n`
|
||||||
' line-height: .8em;\n' +
|
},
|
||||||
' }\n' +
|
{
|
||||||
'</style>'
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
gen : '/* This is a comment that will not be rendered into your brew. */'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/*********************** 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 : 'Watercolor Splatter',
|
||||||
|
icon : 'fas fa-fill-drip',
|
||||||
|
gen : watercolorGen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Watermark',
|
||||||
|
icon : 'fas fa-id-card',
|
||||||
|
gen : dedent`
|
||||||
|
{{watermark Homebrewery}}\n`
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -122,6 +170,7 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
groupName : 'PHB',
|
groupName : 'PHB',
|
||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Spell',
|
name : 'Spell',
|
||||||
@@ -191,6 +240,18 @@ module.exports = [
|
|||||||
icon : 'fas fa-hat-wizard',
|
icon : 'fas fa-hat-wizard',
|
||||||
gen : MagicGen.item,
|
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`;
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -201,17 +262,8 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
groupName : 'Tables',
|
groupName : 'Tables',
|
||||||
icon : 'fas fa-table',
|
icon : 'fas fa-table',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
|
||||||
name : 'Class Table',
|
|
||||||
icon : 'fas fa-table',
|
|
||||||
gen : ClassTableGen.full,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Half Class Table',
|
|
||||||
icon : 'fas fa-list-alt',
|
|
||||||
gen : ClassTableGen.half,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name : 'Table',
|
name : 'Table',
|
||||||
icon : 'fas fa-th-list',
|
icon : 'fas fa-th-list',
|
||||||
@@ -271,6 +323,36 @@ module.exports = [
|
|||||||
}}
|
}}
|
||||||
\n`;
|
\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'),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -278,33 +360,46 @@ module.exports = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**************** PRINT *************/
|
/**************** PAGE *************/
|
||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Print',
|
groupName : 'Print',
|
||||||
icon : 'fas fa-print',
|
icon : 'fas fa-print',
|
||||||
|
view : 'style',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'A4 PageSize',
|
name : 'A4 Page Size',
|
||||||
icon : 'far fa-file',
|
icon : 'far fa-file',
|
||||||
gen : ['<style>',
|
gen : dedent`/* A4 Page Size */
|
||||||
' .phb{',
|
.page{
|
||||||
' width : 210mm;',
|
width : 210mm;
|
||||||
' height : 296.8mm;',
|
height : 296.8mm;
|
||||||
' }',
|
}\n\n`
|
||||||
'</style>'
|
},
|
||||||
].join('\n')
|
{
|
||||||
|
name : 'Square Page Size',
|
||||||
|
icon : 'far fa-file',
|
||||||
|
gen : dedent`/* Square Page Size */
|
||||||
|
.page {
|
||||||
|
width : 125mm;
|
||||||
|
height : 125mm;
|
||||||
|
padding : 12.5mm;
|
||||||
|
columns : unset;
|
||||||
|
}\n\n`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Ink Friendly',
|
name : 'Ink Friendly',
|
||||||
icon : 'fas fa-tint',
|
icon : 'fas fa-tint',
|
||||||
gen : ['<style>',
|
gen : dedent`
|
||||||
' .phb{ background : white;}',
|
/* Ink Friendly */
|
||||||
' .phb img{ display : none;}',
|
*:is(.page,.monster,.note,.descriptive) {
|
||||||
' .phb hr+blockquote{background : white;}',
|
background : white !important;
|
||||||
'</style>',
|
filter : drop-shadow(0px 0px 3px #888) !important;
|
||||||
''
|
}
|
||||||
].join('\n')
|
|
||||||
|
.page img {
|
||||||
|
visibility : hidden;
|
||||||
|
}\n\n`
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,19 +53,19 @@ module.exports = function(brew){
|
|||||||
const TOC = getTOC(pages);
|
const TOC = getTOC(pages);
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
if(g1.title !== null) {
|
if(g1.title !== null) {
|
||||||
r.push(`\t\t- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
|
r.push(`- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
|
||||||
}
|
}
|
||||||
if(g1.children.length){
|
if(g1.children.length){
|
||||||
_.each(g1.children, (g2, idx2)=>{
|
_.each(g1.children, (g2, idx2)=>{
|
||||||
if(g2.title !== null) {
|
if(g2.title !== null) {
|
||||||
r.push(`\t\t - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
|
r.push(` - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
|
||||||
}
|
}
|
||||||
if(g2.children.length){
|
if(g2.children.length){
|
||||||
_.each(g2.children, (g3, idx3)=>{
|
_.each(g2.children, (g3, idx3)=>{
|
||||||
if(g2.title !== null) {
|
if(g2.title !== null) {
|
||||||
r.push(`\t\t - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||||
} else { // Don't over-indent if no level-2 parent entry
|
} else { // Don't over-indent if no level-2 parent entry
|
||||||
r.push(`\t\t - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ module.exports = function(brew){
|
|||||||
{{toc,wide
|
{{toc,wide
|
||||||
# Table Of Contents
|
# Table Of Contents
|
||||||
|
|
||||||
${markdown}
|
${markdown}
|
||||||
}}
|
}}
|
||||||
\n`;
|
\n`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
};
|
||||||
@@ -51,7 +51,7 @@ const spellNames = [
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
spellList : function(){
|
spellList : function(){
|
||||||
const levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
const levels = ['Cantrips (0 Level)', '1st Level', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
||||||
|
|
||||||
const content = _.map(levels, (level)=>{
|
const content = _.map(levels, (level)=>{
|
||||||
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
||||||
@@ -88,4 +88,4 @@ module.exports = {
|
|||||||
'\n\n\n'
|
'\n\n\n'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ const MonsterBlockGen = require('./monsterblock.gen.js');
|
|||||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||||
const CoverPageGen = require('./coverpage.gen.js');
|
const CoverPageGen = require('./coverpage.gen.js');
|
||||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Editor',
|
groupName : 'Text Editor',
|
||||||
icon : 'fas fa-pencil-alt',
|
icon : 'fas fa-pencil-alt',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Column Break',
|
name : 'Column Break',
|
||||||
@@ -77,29 +78,45 @@ module.exports = [
|
|||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
gen : TableOfContentsGen
|
gen : TableOfContentsGen
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'Style Editor',
|
||||||
|
icon : 'fas fa-pencil-alt',
|
||||||
|
view : 'style',
|
||||||
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Remove Drop Cap',
|
name : 'Remove Drop Cap',
|
||||||
icon : 'fas fa-remove-format',
|
icon : 'fas fa-remove-format',
|
||||||
gen : '<style>\n' +
|
gen : dedent`/* Removes Drop Caps */
|
||||||
' .phb h1+p:first-letter {\n' +
|
.phb h1+p:first-letter {
|
||||||
' all: unset;\n' +
|
all: unset;
|
||||||
' }\n' +
|
}\n\n`
|
||||||
'</style>'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Tweak Drop Cap',
|
name : 'Tweak Drop Cap',
|
||||||
icon : 'fas fa-sliders-h',
|
icon : 'fas fa-sliders-h',
|
||||||
gen : '<style>\n' +
|
gen : dedent`/* Drop Cap Settings */
|
||||||
' /* Drop Cap settings */\n' +
|
.phb h1 + p::first-letter {
|
||||||
' .phb h1 + p::first-letter {\n' +
|
float: left;
|
||||||
' float: left;\n' +
|
font-family: Solberry;
|
||||||
' font-family: Solberry;\n' +
|
font-size: 10em;
|
||||||
' font-size: 10em;\n' +
|
color: #222;
|
||||||
' color: #222;\n' +
|
line-height: .8em;
|
||||||
' line-height: .8em;\n' +
|
}\n\n`
|
||||||
' }\n' +
|
|
||||||
'</style>'
|
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
gen : '/* This is a comment that will not be rendered into your brew. */'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -109,6 +126,7 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
groupName : 'PHB',
|
groupName : 'PHB',
|
||||||
icon : 'fas fa-book',
|
icon : 'fas fa-book',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Spell',
|
name : 'Spell',
|
||||||
@@ -166,6 +184,14 @@ module.exports = [
|
|||||||
icon : 'far fa-file-word',
|
icon : 'far fa-file-word',
|
||||||
gen : CoverPageGen,
|
gen : CoverPageGen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Artist Credit',
|
||||||
|
icon : 'fas fa-signature',
|
||||||
|
gen : '<div class=\'artist\' style=\'top:90px;right:30px;\'>\n' +
|
||||||
|
'##### Starry Night\n' +
|
||||||
|
'[Van Gogh](https://www.vangoghmuseum.nl/en)\n' +
|
||||||
|
'</div>\n'
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -176,6 +202,7 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
groupName : 'Tables',
|
groupName : 'Tables',
|
||||||
icon : 'fas fa-table',
|
icon : 'fas fa-table',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Class Table',
|
name : 'Class Table',
|
||||||
@@ -224,30 +251,25 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
name : 'Split Table',
|
name : 'Split Table',
|
||||||
icon : 'fas fa-th-large',
|
icon : 'fas fa-th-large',
|
||||||
gen : function(){
|
gen : dedent`\n
|
||||||
return [
|
<div style='column-count:2'>
|
||||||
'<div style=\'column-count:2\'>',
|
| d10 | Damage Type |
|
||||||
'| d10 | Damage Type |',
|
|:---:|:------------|
|
||||||
'|:---:|:------------|',
|
| 1 | Acid |
|
||||||
'| 1 | Acid |',
|
| 2 | Cold |
|
||||||
'| 2 | Cold |',
|
| 3 | Fire |
|
||||||
'| 3 | Fire |',
|
| 4 | Force |
|
||||||
'| 4 | Force |',
|
| 5 | Lightning |
|
||||||
'| 5 | Lightning |',
|
|
||||||
'',
|
| d10 | Damage Type |
|
||||||
'```',
|
|:---:|:------------|
|
||||||
'```',
|
| 6 | Necrotic |
|
||||||
'',
|
| 7 | Poison |
|
||||||
'| d10 | Damage Type |',
|
| 8 | Psychic |
|
||||||
'|:---:|:------------|',
|
| 9 | Radiant |
|
||||||
'| 6 | Necrotic |',
|
| 10 | Thunder |
|
||||||
'| 7 | Poison |',
|
</div>
|
||||||
'| 8 | Psychic |',
|
\n`
|
||||||
'| 9 | Radiant |',
|
|
||||||
'| 10 | Thunder |',
|
|
||||||
'</div>\n\n',
|
|
||||||
].join('\n');
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -260,28 +282,44 @@ module.exports = [
|
|||||||
{
|
{
|
||||||
groupName : 'Print',
|
groupName : 'Print',
|
||||||
icon : 'fas fa-print',
|
icon : 'fas fa-print',
|
||||||
|
view : 'style',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'A4 PageSize',
|
name : 'A4 Page Size',
|
||||||
icon : 'far fa-file',
|
icon : 'far fa-file',
|
||||||
gen : ['<style>',
|
gen : ['/* A4 Page Size */',
|
||||||
' .phb{',
|
'.phb {',
|
||||||
' width : 210mm;',
|
' width : 210mm;',
|
||||||
' height : 296.8mm;',
|
' height : 296.8mm;',
|
||||||
' }',
|
'}'
|
||||||
'</style>'
|
].join('\n')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Square Page Size',
|
||||||
|
icon : 'far fa-file',
|
||||||
|
gen : ['/* Square Page Size */',
|
||||||
|
'.phb {',
|
||||||
|
' width : 125mm;',
|
||||||
|
' height : 125mm;',
|
||||||
|
' padding : 12.5mm;',
|
||||||
|
' columns : unset;',
|
||||||
|
'}',
|
||||||
|
''
|
||||||
].join('\n')
|
].join('\n')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Ink Friendly',
|
name : 'Ink Friendly',
|
||||||
icon : 'fas fa-tint',
|
icon : 'fas fa-tint',
|
||||||
gen : ['<style>',
|
gen : dedent`
|
||||||
' .phb{ background : white;}',
|
/* Ink Friendly */
|
||||||
' .phb img{ display : none;}',
|
.phb, .phb blockquote, .phb hr+blockquote {
|
||||||
' .phb hr+blockquote{background : white;}',
|
background : white;
|
||||||
'</style>',
|
box-shadow : 0px 0px 3px;
|
||||||
''
|
}
|
||||||
].join('\n')
|
|
||||||
|
.phb img {
|
||||||
|
visibility : hidden;
|
||||||
|
}`
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const NewPage = require('./pages/newPage/newPage.jsx');
|
|||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||||
|
|
||||||
const Homebrew = createClass({
|
const Homebrew = createClass({
|
||||||
|
displayName : 'Homebrewery',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
url : '',
|
url : '',
|
||||||
@@ -31,11 +32,14 @@ const Homebrew = createClass({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
componentWillMount : function() {
|
|
||||||
global.account = this.props.account;
|
getInitialState : function(){
|
||||||
global.version = this.props.version;
|
global.version = this.props.version;
|
||||||
|
global.account = this.props.account;
|
||||||
global.enable_v3 = this.props.enable_v3;
|
global.enable_v3 = this.props.enable_v3;
|
||||||
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function (){
|
render : function (){
|
||||||
return (
|
return (
|
||||||
<Router location={this.props.url}>
|
<Router location={this.props.url}>
|
||||||
@@ -49,6 +53,8 @@ const Homebrew = createClass({
|
|||||||
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
|
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
|
||||||
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
|
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
|
||||||
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
||||||
|
<Route path='/faq' exact component={()=><SharePage brew={this.props.brew} />}/>
|
||||||
|
<Route path='/v3_preview' exact component={()=><HomePage brew={this.props.brew} />}/>
|
||||||
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
|
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
const Account = createClass({
|
const Account = createClass({
|
||||||
|
displayName : 'AccountNavItem',
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
url : ''
|
url : ''
|
||||||
@@ -18,11 +18,42 @@ const Account = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleLogout : function(){
|
||||||
|
if(confirm('Are you sure you want to log out?')) {
|
||||||
|
// Reset divider position
|
||||||
|
window.localStorage.removeItem('naturalcrit-pane-split');
|
||||||
|
// Clear login cookie
|
||||||
|
document.cookie = `nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;samesite=lax;${window.domain ? `domain=${window.domain}` : ''}`;
|
||||||
|
window.location = '/';
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
if(global.account){
|
if(global.account){
|
||||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
|
return <Nav.dropdown>
|
||||||
{global.account.username}
|
<Nav.item
|
||||||
</Nav.item>;
|
className='account'
|
||||||
|
color='orange'
|
||||||
|
icon='fas fa-user'
|
||||||
|
>
|
||||||
|
{global.account.username}
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
href={`/user/${global.account.username}`}
|
||||||
|
color='yellow'
|
||||||
|
icon='fas fa-beer'
|
||||||
|
>
|
||||||
|
brews
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
className='logout'
|
||||||
|
color='red'
|
||||||
|
icon='fas fa-power-off'
|
||||||
|
onClick={this.handleLogout}
|
||||||
|
>
|
||||||
|
logout
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const MAX_TITLE_LENGTH = 50;
|
|||||||
|
|
||||||
|
|
||||||
const EditTitle = createClass({
|
const EditTitle = createClass({
|
||||||
|
displayName : 'EditTitleNavItem',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
title : '',
|
title : '',
|
||||||
|
|||||||
30
client/homebrew/navbar/help.navitem.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
module.exports = function(props){
|
||||||
|
return <Nav.dropdown>
|
||||||
|
<Nav.item color='grey' icon='fas fa-question-circle'>
|
||||||
|
need help?
|
||||||
|
</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** : `)}`}
|
||||||
|
newTab={true}
|
||||||
|
rel='noopener noreferrer'>
|
||||||
|
report issue
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
||||||
|
href='/migrate'
|
||||||
|
newTab={true}
|
||||||
|
rel='noopener noreferrer'>
|
||||||
|
migrate
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>;
|
||||||
|
};
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
|
|
||||||
module.exports = function(props){
|
|
||||||
return <Nav.item
|
|
||||||
newTab={true}
|
|
||||||
color='red'
|
|
||||||
icon='fas fa-bug'
|
|
||||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
|
||||||
report issue
|
|
||||||
</Nav.item>;
|
|
||||||
};
|
|
||||||
@@ -6,6 +6,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const PatreonNavItem = require('./patreon.navitem.jsx');
|
const PatreonNavItem = require('./patreon.navitem.jsx');
|
||||||
|
|
||||||
const Navbar = createClass({
|
const Navbar = createClass({
|
||||||
|
displayName : 'Navbar',
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
//showNonChromeWarning : false,
|
//showNonChromeWarning : false,
|
||||||
@@ -13,12 +14,10 @@ const Navbar = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
getInitialState : function() {
|
||||||
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
return {
|
||||||
this.setState({
|
ver : global.version
|
||||||
//showNonChromeWarning : !isChrome,
|
};
|
||||||
ver : window.version
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -39,7 +38,9 @@ const Navbar = createClass({
|
|||||||
<Nav.item href='/' className='homebrewLogo'>
|
<Nav.item href='/' className='homebrewLogo'>
|
||||||
<div>The Homebrewery</div>
|
<div>The Homebrewery</div>
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item>{`v${this.state.ver}`}</Nav.item>
|
<Nav.item newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
|
||||||
|
{`v${this.state.ver}`}
|
||||||
|
</Nav.item>
|
||||||
<PatreonNavItem />
|
<PatreonNavItem />
|
||||||
{/*this.renderChromeWarning()*/}
|
{/*this.renderChromeWarning()*/}
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
@import 'naturalcrit/styles/colors.less';
|
||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
@keyframes coloring {
|
@keyframes pinkColoring {
|
||||||
//from {color: white;}
|
//from {color: white;}
|
||||||
//to {color: red;}
|
//to {color: red;}
|
||||||
0% {color: pink;}
|
0% {color: pink;}
|
||||||
@@ -62,19 +63,21 @@
|
|||||||
}
|
}
|
||||||
i{
|
i{
|
||||||
.animate(color);
|
.animate(color);
|
||||||
animation-name: coloring;
|
animation-name: pinkColoring;
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
color: pink;
|
color: pink;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.recent.navItem{
|
.recent.navItem {
|
||||||
position : relative;
|
position : relative;
|
||||||
.dropdown{
|
.dropdown{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 28px;
|
top : 28px;
|
||||||
left : 0px;
|
left : 0px;
|
||||||
z-index : 10000;
|
z-index : 10000;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
overflow : hidden auto;
|
||||||
|
max-height : ~"calc(100vh - 28px)";
|
||||||
h4{
|
h4{
|
||||||
display : block;
|
display : block;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
@@ -88,11 +91,12 @@
|
|||||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
||||||
}
|
}
|
||||||
.item{
|
.item{
|
||||||
|
#backgroundColors;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : relative;
|
position : relative;
|
||||||
display : block;
|
display : block;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
padding : 13px 5px;
|
padding : 8px 5px 13px;
|
||||||
background-color : #333;
|
background-color : #333;
|
||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
@@ -138,4 +142,7 @@
|
|||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.account.navItem{
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const VIEW_KEY = 'homebrewery-recently-viewed';
|
|||||||
|
|
||||||
|
|
||||||
const RecentItems = createClass({
|
const RecentItems = createClass({
|
||||||
|
DisplayName : 'RecentItems',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
storageKey : '',
|
storageKey : '',
|
||||||
@@ -123,8 +123,8 @@ const RecentItems = createClass({
|
|||||||
if(!this.state.showDropdown) return null;
|
if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
const makeItems = (brews)=>{
|
const makeItems = (brews)=>{
|
||||||
return _.map(brews, (brew)=>{
|
return _.map(brews, (brew, i)=>{
|
||||||
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer'>
|
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='title'>{brew.title || '[ no title ]'}</span>
|
||||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||||
</a>;
|
</a>;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true'
|
|||||||
|
|
||||||
|
|
||||||
const RedditShare = createClass({
|
const RedditShare = createClass({
|
||||||
|
displayName : 'RedditShareNavItem',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ const cx = require('classnames');
|
|||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../googleDrive.png');
|
const googleDriveIcon = require('../../../../googleDrive.png');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
|
displayName : 'BrewItem',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
@@ -29,26 +31,18 @@ const BrewItem = createClass({
|
|||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.props.brew.googleId) {
|
request.delete(`/api/${this.props.brew.googleId}${this.props.brew.editId}`)
|
||||||
request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`)
|
.send()
|
||||||
.send()
|
.end(function(err, res){
|
||||||
.end(function(err, res){
|
location.reload();
|
||||||
location.reload();
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
request.delete(`/api/${this.props.brew.editId}`)
|
|
||||||
.send()
|
|
||||||
.end(function(err, res){
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderDeleteBrewLink : function(){
|
renderDeleteBrewLink : function(){
|
||||||
if(!this.props.brew.editId) return;
|
if(!this.props.brew.editId) return;
|
||||||
|
|
||||||
return <a onClick={this.deleteBrew}>
|
return <a className='deleteLink' onClick={this.deleteBrew}>
|
||||||
<i className='fas fa-trash-alt' />
|
<i className='fas fa-trash-alt' title='Delete' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,8 +54,8 @@ const BrewItem = createClass({
|
|||||||
editLink = this.props.brew.googleId + editLink;
|
editLink = this.props.brew.googleId + editLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
return <a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<i className='fas fa-pencil-alt' />
|
<i className='fas fa-pencil-alt' title='Edit' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -73,8 +67,8 @@ const BrewItem = createClass({
|
|||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = this.props.brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
return <a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
<i className='fas fa-share-alt' />
|
<i className='fas fa-share-alt' title='Share' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -86,8 +80,8 @@ const BrewItem = createClass({
|
|||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = this.props.brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a href={`/download/${shareLink}`}>
|
return <a className='downloadLink' href={`/download/${shareLink}`}>
|
||||||
<i className='fas fa-download' />
|
<i className='fas fa-download' title='Download' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -101,19 +95,30 @@ const BrewItem = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
const brew = this.props.brew;
|
const brew = this.props.brew;
|
||||||
return <div className='brewItem'>
|
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||||
<h2>{brew.title}</h2>
|
|
||||||
<p className='description' >{brew.description}</p>
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
|
return <div className='brewItem'>
|
||||||
|
<div className='text'>
|
||||||
|
<h2>{brew.title}</h2>
|
||||||
|
<p className='description'>{brew.description}</p>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
<span>
|
<span title={`Authors:\n${brew.authors.join('\n')}`}>
|
||||||
<i className='fas fa-user' /> {brew.authors.join(', ')}
|
<i className='fas fa-user'/> {brew.authors.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<br />
|
||||||
<i className='fas fa-eye' /> {brew.views}
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
|
<i className='fas fa-eye'/> {brew.views}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
{brew.pageCount &&
|
||||||
|
<span title={`Page count: ${brew.pageCount}`}>
|
||||||
|
<i className='far fa-file' /> {brew.pageCount}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
<span title={dedent`
|
||||||
|
Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
||||||
|
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
||||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
@@ -10,25 +10,30 @@
|
|||||||
min-height : 105px;
|
min-height : 105px;
|
||||||
margin-right : 15px;
|
margin-right : 15px;
|
||||||
margin-bottom : 15px;
|
margin-bottom : 15px;
|
||||||
padding : 5px 15px 5px 8px;
|
padding : 5px 15px 2px 8px;
|
||||||
padding-right : 15px;
|
padding-right : 15px;
|
||||||
border : 1px solid #c9ad6a;
|
border : 1px solid #c9ad6a;
|
||||||
border-radius : 5px;
|
border-radius : 5px;
|
||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
h4{
|
box-shadow : 0px 4px 5px 0px #333;
|
||||||
margin-bottom : 5px;
|
background-color : #cab2802e;
|
||||||
font-size : 2.2em;
|
.text {
|
||||||
|
min-height : 54px;
|
||||||
|
h4{
|
||||||
|
margin-bottom : 5px;
|
||||||
|
font-size : 2.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.info{
|
.info{
|
||||||
position: absolute;
|
position: initial;
|
||||||
bottom: 0px;
|
bottom: 2px;
|
||||||
margin-bottom: 4px;
|
|
||||||
font-family : ScalySans;
|
font-family : ScalySans;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
&>span{
|
&>span{
|
||||||
margin-right : 12px;
|
margin-right : 12px;
|
||||||
|
line-height : 1.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
160
client/homebrew/pages/basePages/listPage/listPage.jsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
require('./listPage.less');
|
||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||||
|
|
||||||
|
const ListPage = createClass({
|
||||||
|
displayName : 'ListPage',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
brewCollection : [
|
||||||
|
{
|
||||||
|
title : '',
|
||||||
|
class : '',
|
||||||
|
brews : []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
navItems : <></>
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
sortType : 'alpha',
|
||||||
|
sortDir : 'asc',
|
||||||
|
filterString : ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
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}/>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sortBrewOrder : function(brew){
|
||||||
|
if(!brew.title){brew.title = 'No Title';}
|
||||||
|
const mapping = {
|
||||||
|
'alpha' : _.deburr(brew.title.toLowerCase()),
|
||||||
|
'created' : moment(brew.createdAt).format(),
|
||||||
|
'updated' : moment(brew.updatedAt).format(),
|
||||||
|
'views' : brew.views,
|
||||||
|
'latest' : moment(brew.lastViewed).format()
|
||||||
|
};
|
||||||
|
return mapping[this.state.sortType];
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSortOptionChange : function(event){
|
||||||
|
this.setState({
|
||||||
|
sortType : event.target.value
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSortDirChange : function(event){
|
||||||
|
this.setState({
|
||||||
|
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSortOption : function(sortTitle, sortValue){
|
||||||
|
return <td>
|
||||||
|
<button
|
||||||
|
value={`${sortValue}`}
|
||||||
|
onClick={this.handleSortOptionChange}
|
||||||
|
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||||
|
>
|
||||||
|
{`${sortTitle}`}
|
||||||
|
</button>
|
||||||
|
</td>;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFilterTextChange : function(e){
|
||||||
|
this.setState({
|
||||||
|
filterString : e.target.value
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderFilterOption : function(){
|
||||||
|
return <td>
|
||||||
|
<label>
|
||||||
|
<i className='fas fa-search'></i>
|
||||||
|
<input
|
||||||
|
type='search'
|
||||||
|
placeholder='search title/description'
|
||||||
|
onChange={this.handleFilterTextChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</td>;
|
||||||
|
},
|
||||||
|
|
||||||
|
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>
|
||||||
|
</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));
|
||||||
|
});
|
||||||
|
|
||||||
|
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderBrewCollection : function(brewCollection){
|
||||||
|
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))}
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <div className='listPage sitePage'>
|
||||||
|
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
||||||
|
{this.props.navItems}
|
||||||
|
|
||||||
|
<div className='content V3'>
|
||||||
|
<div className='phb'>
|
||||||
|
{this.renderSortOptions()}
|
||||||
|
{this.renderBrewCollection(this.props.brewCollection)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = ListPage;
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
-webkit-column-gap : auto;
|
-webkit-column-gap : auto;
|
||||||
-moz-column-gap : auto;
|
-moz-column-gap : auto;
|
||||||
}
|
}
|
||||||
.userPage{
|
.listPage{
|
||||||
.content{
|
.content{
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
.phb{
|
.phb{
|
||||||
@@ -34,8 +34,9 @@
|
|||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
position : fixed;
|
position : fixed;
|
||||||
top : 35px;
|
top : 35px;
|
||||||
|
left : calc(50vw - 408px);
|
||||||
border : 2px solid #58180D;
|
border : 2px solid #58180D;
|
||||||
width : 675px;
|
width : 800px;
|
||||||
background-color : #EEE5CE;
|
background-color : #EEE5CE;
|
||||||
padding : 2px;
|
padding : 2px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
@@ -52,6 +53,9 @@
|
|||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
tbody tr{
|
tbody tr{
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
|
i{
|
||||||
|
padding-right : 5px
|
||||||
|
}
|
||||||
button{
|
button{
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
color : #58180D;
|
color : #58180D;
|
||||||
@@ -10,7 +10,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
@@ -27,6 +27,7 @@ const googleDriveInactive = require('../../googleDriveMono.png');
|
|||||||
const SAVE_TIMEOUT = 3000;
|
const SAVE_TIMEOUT = 3000;
|
||||||
|
|
||||||
const EditPage = createClass({
|
const EditPage = createClass({
|
||||||
|
displayName : 'EditPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
@@ -196,73 +197,21 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
|
|
||||||
if(this.state.saveGoogle) {
|
const brew = this.state.brew;
|
||||||
if(transfer) {
|
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
const res = await request
|
|
||||||
.post('/api/newGoogle/')
|
|
||||||
.send(this.state.brew)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err.status === 401
|
|
||||||
? 'Not signed in!'
|
|
||||||
: 'Error Transferring to Google!');
|
|
||||||
this.setState({ errors: err, saveGoogle: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
if(!res) { return; }
|
const params = `${transfer ? `?transfer${this.state.saveGoogle ? 'To' : 'From'}Google=true` : ''}`;
|
||||||
|
const res = await request
|
||||||
|
.put(`/api/update/${brew.editId}${params}`)
|
||||||
|
.send(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error Updating Local Brew');
|
||||||
|
this.setState({ errors: err });
|
||||||
|
});
|
||||||
|
|
||||||
console.log('Deleting Local Copy');
|
this.savedBrew = res.body;
|
||||||
await request.delete(`/api/${this.state.brew.editId}`)
|
if(transfer) {
|
||||||
.send()
|
history.replaceState(null, null, `/edit/${this.savedBrew.googleId ?? ''}${this.savedBrew.editId}`);
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error deleting Local Copy');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
|
|
||||||
} else {
|
|
||||||
const res = await request
|
|
||||||
.put(`/api/updateGoogle/${this.state.brew.editId}`)
|
|
||||||
.send(this.state.brew)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err.status === 401
|
|
||||||
? 'Not signed in!'
|
|
||||||
: 'Error Saving to Google!');
|
|
||||||
this.setState({ errors: err });
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(transfer) {
|
|
||||||
const res = await request.post('/api')
|
|
||||||
.send(this.state.brew)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error creating Local Copy');
|
|
||||||
this.setState({ errors: err });
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
await request.get(`/api/removeGoogle/${this.state.brew.googleId}${this.state.brew.editId}`)
|
|
||||||
.send()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error Deleting Google Brew');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
|
|
||||||
} else {
|
|
||||||
const res = await request
|
|
||||||
.put(`/api/update/${this.state.brew.editId}`)
|
|
||||||
.send(this.state.brew)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error Updating Local Brew');
|
|
||||||
this.setState({ errors: err });
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
@@ -322,7 +271,9 @@ const EditPage = createClass({
|
|||||||
let errMsg = '';
|
let errMsg = '';
|
||||||
try {
|
try {
|
||||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||||
errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
||||||
|
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||||
|
console.log(errMsg);
|
||||||
} catch (e){}
|
} catch (e){}
|
||||||
|
|
||||||
if(this.state.errors.status == '401'){
|
if(this.state.errors.status == '401'){
|
||||||
@@ -344,6 +295,27 @@ const EditPage = createClass({
|
|||||||
</Nav.item>;
|
</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'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
@@ -373,7 +345,21 @@ const EditPage = createClass({
|
|||||||
this.state.brew.shareId;
|
this.state.brew.shareId;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getRedditLink : function(){
|
||||||
|
|
||||||
|
const shareLink = this.processShareId();
|
||||||
|
const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
|
||||||
|
const title = `${this.props.brew.title} ${systems}`;
|
||||||
|
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||||
|
|
||||||
|
**[Homebrewery Link](https://homebrewery.naturalcrit.com/share/${shareLink})**`;
|
||||||
|
|
||||||
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
||||||
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
|
const shareLink = this.processShareId();
|
||||||
|
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
|
|
||||||
{this.state.alertTrashedGoogleBrew &&
|
{this.state.alertTrashedGoogleBrew &&
|
||||||
@@ -393,10 +379,21 @@ const EditPage = createClass({
|
|||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
{this.renderSaveButton()}
|
{this.renderSaveButton()}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<ReportIssue />
|
<HelpNavItem/>
|
||||||
<Nav.item newTab={true} href={`/share/${this.processShareId()}`} color='teal' icon='fas fa-share-alt'>
|
<Nav.dropdown>
|
||||||
Share
|
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||||
</Nav.item>
|
share
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
||||||
|
view
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`https://homebrewery.naturalcrit.com/share/${shareLink}`);}}>
|
||||||
|
copy url
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||||
|
post to reddit
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
<PrintLink shareId={this.processShareId()} />
|
||||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||||
<Account />
|
<Account />
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const cx = require('classnames');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const ErrorPage = createClass({
|
|||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<PatreonNavItem />
|
<PatreonNavItem />
|
||||||
<IssueNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
require('./homePage.less');
|
require('./homePage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
@@ -8,7 +9,7 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
|
|
||||||
const HomePage = createClass({
|
const HomePage = createClass({
|
||||||
|
displayName : 'HomePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
@@ -49,18 +51,15 @@ const HomePage = createClass({
|
|||||||
this.refs.editor.update();
|
this.refs.editor.update();
|
||||||
},
|
},
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState({
|
this.setState((prevState)=>({
|
||||||
brew : { text: text }
|
brew : _.merge({}, prevState.brew, { text: text })
|
||||||
});
|
}));
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar ver={this.props.ver}>
|
return <Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
<IssueNavItem />
|
<HelpNavItem />
|
||||||
<Nav.item newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
|
|
||||||
Changelog
|
|
||||||
</Nav.item>
|
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
@@ -81,7 +80,7 @@ const HomePage = createClass({
|
|||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
showEditButtons={false}
|
showEditButtons={false}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} />
|
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
position : absolute;
|
position : absolute;
|
||||||
display : block;
|
display : block;
|
||||||
right : 70px;
|
right : 70px;
|
||||||
bottom : 70px;
|
bottom : 50px;
|
||||||
z-index : 100;
|
z-index : 100;
|
||||||
z-index : 5001;
|
z-index : 5001;
|
||||||
padding : 1em;
|
padding : 1em;
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
position : absolute;
|
position : absolute;
|
||||||
display : block;
|
display : block;
|
||||||
right : 200px;
|
right : 200px;
|
||||||
bottom : 90px;
|
bottom : 70px;
|
||||||
z-index : 100;
|
z-index : 100;
|
||||||
z-index : 5000;
|
z-index : 5000;
|
||||||
padding : 0.8em;
|
padding : 0.8em;
|
||||||
@@ -40,4 +40,4 @@
|
|||||||
right : 350px;
|
right : 350px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
202
client/homebrew/pages/homePage/migrate.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# How to Convert a Legacy Document to v3
|
||||||
|
Here you will find a number of steps to guide you through converting a Legacy document into a Homebrewery v3 document.
|
||||||
|
|
||||||
|
**The first thing you'll want to do is switch the editor's rendering engine from `Legacy` to `v3`.** This will be the renderer we design features for moving forward.
|
||||||
|
|
||||||
|
There are some examples of Legacy code in the code pane if you need more context behind some of the changes.
|
||||||
|
|
||||||
|
**This document will evolve as users like yourself inform us of issues with it, or areas of conversion that it does not cover. _Please_ reach out if you have any suggestions for this document.**
|
||||||
|
|
||||||
|
## Simple Replacements
|
||||||
|
To make your life a little easier with this section, a text editor like [VSCode](https://code.visualstudio.com/) or Notepad will help a lot.
|
||||||
|
|
||||||
|
The following table describes Legacy and other document elements and their Homebrewery counterparts. A simple find/replace should get these in working order.
|
||||||
|
|
||||||
|
| Legacy / Other | Homebrewery |
|
||||||
|
|:----------------|:-----------------------------|
|
||||||
|
| `\pagebreak` | `\page` |
|
||||||
|
| `======` | `\page` |
|
||||||
|
| `\pagebreaknum` | `{{pageNumber,auto}}\n\page` |
|
||||||
|
| `@=====` | `{{pageNumber,auto}}\n\page` |
|
||||||
|
| `\columnbreak` | `\column` |
|
||||||
|
| `.phb` | `.page` |
|
||||||
|
|
||||||
|
## Classed or Styled Divs
|
||||||
|
Anything that relies on the following syntax can be changed to the new Homebrewery v3 curly brace syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
<div class="classTable wide">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
:
|
||||||
|
The above example is equivalent to the following in v3 syntax.
|
||||||
|
|
||||||
|
```
|
||||||
|
{{classTable,wide
|
||||||
|
...
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
:
|
||||||
|
Some examples of this include class tables (as shown above), descriptive blocks, notes, and spell lists.
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
## Margins and Padding
|
||||||
|
Any manual margins and padding to push text down the page will likely need to be updated. Colons can be used on lines by themselves to push things down the page vertically if you'd rather not set pixel-perfect margins or padding.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
In Legacy, notes are denoted using markdown blockquote syntax. In Homebrewery v3, this is replaced by the curly brace syntax.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
> ##### Catchy Title
|
||||||
|
> Useful Information
|
||||||
|
-->
|
||||||
|
|
||||||
|
{{note
|
||||||
|
##### Title
|
||||||
|
Information
|
||||||
|
}}
|
||||||
|
|
||||||
|
## Split Tables
|
||||||
|
Split tables also use the curly brace syntax, as the new renderer can handle style values separately from class names.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div style='column-count:2'>
|
||||||
|
|
||||||
|
| d8 | Loot |
|
||||||
|
|:---:|:-----------:|
|
||||||
|
| 1 | 100gp |
|
||||||
|
| 2 | 200gp |
|
||||||
|
| 3 | 300gp |
|
||||||
|
| 4 | 400gp |
|
||||||
|
|
||||||
|
| d8 | Loot |
|
||||||
|
|:---:|:-----------:|
|
||||||
|
| 5 | 500gp |
|
||||||
|
| 6 | 600gp |
|
||||||
|
| 7 | 700gp |
|
||||||
|
| 8 | 1000gp |
|
||||||
|
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
##### Typical Difficulty Classes
|
||||||
|
{{column-count:2
|
||||||
|
| Task Difficulty | DC |
|
||||||
|
|:----------------|:--:|
|
||||||
|
| Very easy | 5 |
|
||||||
|
| Easy | 10 |
|
||||||
|
| Medium | 15 |
|
||||||
|
|
||||||
|
| Task Difficulty | DC |
|
||||||
|
|:------------------|:--:|
|
||||||
|
| Hard | 20 |
|
||||||
|
| Very hard | 25 |
|
||||||
|
| Nearly impossible | 30 |
|
||||||
|
}}
|
||||||
|
|
||||||
|
## Blockquotes
|
||||||
|
Blockquotes are denoted by the `>` character at the beginning of the line. In Homebrewery's v3 renderer, they hold virtually no meaning and have no CSS styling. You are free to use blockquotes when styling your document or creating themes without needing to worry about your CSS affecting other parts of the document.
|
||||||
|
|
||||||
|
{{pageNumber,auto}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
## Stat Blocks
|
||||||
|
|
||||||
|
There are pretty significant differences between stat blocks on the Legacy renderer and Homebrewery v3. This section contains a list of changes that will need to be made to update the stat block.
|
||||||
|
|
||||||
|
### Initial Changes
|
||||||
|
You will want to **remove all leading** `___` that started the stat block in Legacy, and replace that with `{{monster` before the stat block, and `}}` after it.
|
||||||
|
|
||||||
|
**If you want a frame** around the stat block, you can add `,frame` to the curly brace definition.
|
||||||
|
|
||||||
|
**If the stat block was wide**, make sure to add `,wide` to the curly brace definition.
|
||||||
|
|
||||||
|
### Blockquotes
|
||||||
|
The key difference is the lack of blockquotes. Legacy documents use the `>` symbol at the start of the line for each line in the stat block, and the v3 renderer does not. **You will want to remove all `>` characters at the beginning of all lines, and delete any leading spaces.**
|
||||||
|
|
||||||
|
### Lists
|
||||||
|
The basic characteristics and advanced characteristics sections are not list elements in Homebrewery. You will want to **remove all `-` or `*` characters from the beginning of lines.**
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
In order to have the correct spacing after removing the list elements, you will want to **add two colons between the name of each basic/advanced characteristic and its value.** _(see example in the code pane)_
|
||||||
|
|
||||||
|
Additionally, in the special traits and actions sections, you will want to add a colon at the beginning of each line that separates a trait/action from another, as seen below. **Any empty lines between special traits and actions should contain only a colon.** _(see example in the code pane)_
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
{{margin-top:102px}}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
### Legacy/Other Document Example:
|
||||||
|
___
|
||||||
|
> ## Centaur
|
||||||
|
> *Large Monstrosity, neutral good*
|
||||||
|
>___
|
||||||
|
> - **Armor Class** 12
|
||||||
|
> - **Hit Points** 45(6d10 + 12)
|
||||||
|
> - **Speed** 50ft.
|
||||||
|
>___
|
||||||
|
>|STR|DEX|CON|INT|WIS|CHA|
|
||||||
|
>|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
|
>|18 (+4)|14 (+2)|14 (+2)|9 (-1)|13 (+1)|11 (+0)|
|
||||||
|
>___
|
||||||
|
> - **Skills** Athletics +6, Perception +3, Survival +3
|
||||||
|
> - **Senses** passive Perception 13
|
||||||
|
> - **Languages** Elvish, Sylvan
|
||||||
|
> - **Challenge** 2 (450 XP)
|
||||||
|
> ___
|
||||||
|
> ***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||||
|
>
|
||||||
|
> ***Second Thing*** More details.
|
||||||
|
>
|
||||||
|
> ### Actions
|
||||||
|
> ***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||||
|
>
|
||||||
|
> ***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||||
|
>
|
||||||
|
> ***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||||
|
>
|
||||||
|
> ***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Homebrewery v3 Example:
|
||||||
|
|
||||||
|
{{monster
|
||||||
|
## Centaur
|
||||||
|
*Large monstrosity, neutral good*
|
||||||
|
___
|
||||||
|
**Armor Class** :: 12
|
||||||
|
**Hit Points** :: 45(6d10 + 12)
|
||||||
|
**Speed** :: 50ft.
|
||||||
|
___
|
||||||
|
| STR | DEX | CON | INT | WIS | CHA |
|
||||||
|
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
|
|18 (+4)|14 (+2)|14 (+2)|9 (-1) |13 (+1)|11 (+0)|
|
||||||
|
___
|
||||||
|
**Skills** :: Athletics +6, Perception +3, Survival +3
|
||||||
|
**Senses** :: passive Perception 13
|
||||||
|
**Languages** :: Elvish, Sylvan
|
||||||
|
**Challenge** :: 2 (450 XP)
|
||||||
|
___
|
||||||
|
***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||||
|
:
|
||||||
|
***Second Thing*** More details.
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||||
|
:
|
||||||
|
***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||||
|
:
|
||||||
|
***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||||
|
:
|
||||||
|
***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{pageNumber,auto}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
# The Homebrewery
|
# The Homebrewery
|
||||||
|
|
||||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||||
|
|
||||||
### Homebrew D&D made easy
|
### Homebrew D&D made easy
|
||||||
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||||
|
|
||||||
**Try it! **Simply edit the text on the left and watch it *update live* on the right.
|
**Try it!** Simply edit the text on the left and watch it *update live* on the right.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -14,7 +15,7 @@ When you create your own homebrew you will be given a *edit url* and a *share ur
|
|||||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||||
|
|
||||||
## Helping out
|
## Helping out
|
||||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/stolksdorf) to help me keep the servers running.
|
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
||||||
|
|
||||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||||
|
|
||||||
@@ -36,32 +37,41 @@ This tool will **always** be free, never have ads, and I will never offer any "p
|
|||||||
```
|
```
|
||||||
```
|
```
|
||||||
|
|
||||||
## Big things coming in v3.0.0
|
## V3.0.0 Released!
|
||||||
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. All brews made previous to the release of v3.0.0 will still render normally.
|
With the latest major update to *The Homebrewery* we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like **div** and **span** in most cases. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||||
|
|
||||||
|
**You can enable V3 via the <span class="fa fa-info-circle" style="text-indent:0"></span> Properties button!**
|
||||||
|
|
||||||
## New Things All The Time!
|
## New Things All The Time!
|
||||||
What's new in the latest update? Check out the full changelog [here](/changelog)
|
What's new in the latest update? Check out the full changelog [here](/changelog)
|
||||||
|
|
||||||
### Bugs, Issues, Suggestions?
|
### Bugs, Issues, Suggestions?
|
||||||
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let me know!.
|
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
||||||
|
|
||||||
|
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
||||||
|
|
||||||
|
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
||||||
|
|
||||||
### Legal Junk
|
### Legal Junk
|
||||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself.
|
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). This means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
If you wish to sell or in some way gain profit for what you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||||
|
|
||||||
### More Resources
|
### More Resources
|
||||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/comments/3uwxx9/resources_open_to_the_community/).
|
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||||
|
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||||
|
|
||||||
|
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
|
||||||
|
|
||||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
|
|
||||||
|
|
||||||
<div class='pageNumber'>1</div>
|
<div class='pageNumber'>1</div>
|
||||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||||
|
|
||||||
|
<div style='position: absolute; top: 20px; right: 20px;'>
|
||||||
|
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||||
|
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||||
|
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||||
|
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
\page
|
\page
|
||||||
|
|
||||||
@@ -96,5 +106,3 @@ If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just ref
|
|||||||
|
|
||||||
<div class='pageNumber'>2</div>
|
<div class='pageNumber'>2</div>
|
||||||
<div class='footnote'>PART 2 | BORING STUFF</div>
|
<div class='footnote'>PART 2 | BORING STUFF</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
175
client/homebrew/pages/homePage/welcome_msg_v3.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
```css
|
||||||
|
.page #example + table td {
|
||||||
|
border:1px dashed #00000030;
|
||||||
|
}
|
||||||
|
.page {
|
||||||
|
padding-bottom : 1.1cm;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# The Homebrewery *V3*
|
||||||
|
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||||
|
|
||||||
|
### Homebrew D&D made easy
|
||||||
|
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||||
|
|
||||||
|
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
||||||
|
|
||||||
|
### Editing and Sharing
|
||||||
|
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
||||||
|
|
||||||
|
Any changes you make while on the *edit url* will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew, so be careful about who you share it with.
|
||||||
|
|
||||||
|
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||||
|
|
||||||
|
{{note
|
||||||
|
##### PDF Creation
|
||||||
|
PDF Printing works best in Google Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
|
||||||
|
|
||||||
|
After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
|
||||||
|
* Set the **Destination** to "Save as PDF"
|
||||||
|
* Set **Paper Size** to "Letter"
|
||||||
|
* If you are printing on A4 paper, make sure to have the **PRINT → {{far,fa-file}} A4 Pagesize** snippet in your brew
|
||||||
|
* In **Options** make sure "Background Images" is selected.
|
||||||
|
* Hit print and enjoy! You're done!
|
||||||
|
|
||||||
|
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
||||||
|
}}
|
||||||
|
|
||||||
|
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||||
|
|
||||||
|
{{artist,bottom:160px,left:100px
|
||||||
|
##### Homebrew Mug
|
||||||
|
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{pageNumber 1}}
|
||||||
|
{{footnote PART 1 | FANCINESS}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
## New in V3.0.0
|
||||||
|
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
||||||
|
|
||||||
|
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
||||||
|
|
||||||
|
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
||||||
|
|
||||||
|
#### New Things All The Time!
|
||||||
|
Check out the latest updates in the full changelog [here](/changelog).
|
||||||
|
|
||||||
|
### Helping out
|
||||||
|
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
||||||
|
|
||||||
|
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||||
|
|
||||||
|
### Bugs, Issues, Suggestions?
|
||||||
|
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
||||||
|
|
||||||
|
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
||||||
|
|
||||||
|
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
||||||
|
|
||||||
|
### Legal Junk
|
||||||
|
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||||
|
|
||||||
|
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||||
|
|
||||||
|
#### Crediting Me
|
||||||
|
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||||
|
|
||||||
|
### More Homebrew Resources
|
||||||
|
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||||
|
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||||
|
|
||||||
|
{{position:absolute;top:20px;right:20px;width:auto
|
||||||
|
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||||
|
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||||
|
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||||
|
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
## Markdown+
|
||||||
|
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||||
|
|
||||||
|
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
||||||
|
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
||||||
|
|
||||||
|
### Curly Brackets
|
||||||
|
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
||||||
|
|
||||||
|
#### Span
|
||||||
|
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
||||||
|
|
||||||
|
|
||||||
|
#### Block
|
||||||
|
{{purple,#book,text-align:center,background:#aa88aa55
|
||||||
|
My favorite book is Wheel of Time. This block has a class of `purple`, an id of `book`, and centered text with a colored background. The opening and closing brackets are on lines separate from the block contents.
|
||||||
|
}}
|
||||||
|
|
||||||
|
#### Injection
|
||||||
|
For any element not inside a span or block, you can *inject* attributes using the same syntax but with single brackets in a single line immediately after the element.
|
||||||
|
|
||||||
|
Inline elements like *italics* {color:#D35400} or images require the injection on the same line.
|
||||||
|
|
||||||
|
Block elements like headers require the injection to start on the line immediately following.
|
||||||
|
|
||||||
|
##### A Purple Header
|
||||||
|
{color:purple,text-align:center}
|
||||||
|
|
||||||
|
\* *this does not currently work for tables yet*
|
||||||
|
|
||||||
|
### Vertical Spacing
|
||||||
|
A blank line can be achieved with a run of one or more `:` alone on a line. More `:`'s will create more space.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Much nicer than `<br><br><br><br><br>`
|
||||||
|
|
||||||
|
### Definition Lists
|
||||||
|
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||||
|
|
||||||
|
### Column Breaks
|
||||||
|
Column and page breaks with `\column` and `\page`.
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||||
|
|
||||||
|
A cell can be spanned across columns by grouping multiple pipe `|` characters at the end of a cell.
|
||||||
|
|
||||||
|
Row spanning is achieved by adding a `^` at the end of a cell just before the `|`.
|
||||||
|
|
||||||
|
These can be combined to span a cell across both columns and rows. Cells must have the same colspan if they are to be rowspan'd.
|
||||||
|
|
||||||
|
##### Example
|
||||||
|
| Head A | Spanned Header ||
|
||||||
|
| Head B | Head C | Head D |
|
||||||
|
|:-------|:------:|:------:|
|
||||||
|
| 1A | 1B | 1C |
|
||||||
|
| 2A ^| 2B | 2C |
|
||||||
|
| 3A ^| 3B 3C ||
|
||||||
|
| 4A | 4B 4C^||
|
||||||
|
| 5A ^| 5B | 5C |
|
||||||
|
| 6A | 6B ^| 6C |
|
||||||
|
|
||||||
|
## Images
|
||||||
|
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*.
|
||||||
|
|
||||||
|
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
||||||
|
|
||||||
|
 {width:100px,border:"2px solid",border-radius:10px}
|
||||||
|
|
||||||
|
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
|
||||||
|
|
||||||
|
## Snippets
|
||||||
|
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
||||||
|
|
||||||
|
## Style Editor Panel
|
||||||
|
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||||
|
|
||||||
|
{{pageNumber 2}}
|
||||||
|
{{footnote PART 2 | BORING STUFF}}
|
||||||
@@ -11,7 +11,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
@@ -19,9 +19,11 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
|
||||||
|
|
||||||
const NewPage = createClass({
|
const NewPage = createClass({
|
||||||
|
displayName : 'NewPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
@@ -44,43 +46,44 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
|
const brew = this.props.brew;
|
||||||
|
|
||||||
|
if(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 = metaStorage?.renderer || brew.renderer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
text : this.props.brew.text || '',
|
text : brew.text || '',
|
||||||
style : this.props.brew.style || undefined,
|
style : brew.style || undefined,
|
||||||
gDrive : false,
|
gDrive : false,
|
||||||
title : this.props.brew.title || '',
|
title : brew.title || '',
|
||||||
description : this.props.brew.description || '',
|
description : brew.description || '',
|
||||||
tags : this.props.brew.tags || '',
|
tags : brew.tags || '',
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : this.props.brew.systems || [],
|
systems : brew.systems || [],
|
||||||
renderer : this.props.brew.renderer || 'legacy'
|
renderer : brew.renderer || 'legacy'
|
||||||
},
|
},
|
||||||
|
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
errors : [],
|
errors : null,
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text)
|
htmlErrors : Markdown.validate(brew.text)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
const brewStorage = localStorage.getItem(BREWKEY);
|
|
||||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
|
||||||
|
|
||||||
const brew = this.state.brew;
|
|
||||||
|
|
||||||
if(!this.props.brew.text || !this.props.brew.style){
|
|
||||||
brew.text = this.props.brew.text || (brewStorage ?? '');
|
|
||||||
brew.style = this.props.brew.style || (styleStorage ?? undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : brew,
|
|
||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
|
||||||
}));
|
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -126,7 +129,19 @@ const NewPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, metadata),
|
brew : _.merge({}, prevState.brew, metadata),
|
||||||
}));
|
}));
|
||||||
|
localStorage.setItem(METAKEY, JSON.stringify({
|
||||||
|
// 'title' : this.state.brew.title,
|
||||||
|
// 'description' : this.state.brew.description,
|
||||||
|
'renderer' : this.state.brew.renderer
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
clearErrors : function(){
|
||||||
|
this.setState({
|
||||||
|
errors : null,
|
||||||
|
isSaving : false
|
||||||
|
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
@@ -142,57 +157,100 @@ const NewPage = createClass({
|
|||||||
const index = brew.text.indexOf('```\n\n');
|
const index = brew.text.indexOf('```\n\n');
|
||||||
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
};
|
}
|
||||||
|
|
||||||
if(this.state.saveGoogle) {
|
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
const res = await request
|
|
||||||
.post('/api/newGoogle/')
|
const res = await request
|
||||||
|
.post(`/api${this.state.saveGoogle ? '?transferToGoogle=true' : ''}`)
|
||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err.status === 401
|
console.log(err);
|
||||||
? 'Not signed in!'
|
this.setState({ isSaving: false, errors: err });
|
||||||
: 'Error Creating New Google Brew!');
|
|
||||||
this.setState({ isSaving: false });
|
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
if(!res) return;
|
||||||
|
|
||||||
brew = res.body;
|
brew = res.body;
|
||||||
localStorage.removeItem(BREWKEY);
|
localStorage.removeItem(BREWKEY);
|
||||||
localStorage.removeItem(STYLEKEY);
|
localStorage.removeItem(STYLEKEY);
|
||||||
window.location = `/edit/${brew.googleId}${brew.editId}`;
|
localStorage.removeItem(METAKEY);
|
||||||
} else {
|
window.location = `/edit/${brew.googleId ?? ''}${brew.editId}`;
|
||||||
request.post('/api')
|
|
||||||
.send(brew)
|
|
||||||
.end((err, res)=>{
|
|
||||||
if(err){
|
|
||||||
this.setState({
|
|
||||||
isSaving : false
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.onbeforeunload = function(){};
|
|
||||||
brew = res.body;
|
|
||||||
localStorage.removeItem(BREWKEY);
|
|
||||||
localStorage.removeItem(STYLEKEY);
|
|
||||||
window.location = `/edit/${brew.editId}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
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){
|
if(this.state.isSaving){
|
||||||
return <Nav.item icon='fas fa-spinner fa-spin' className='saveButton'>
|
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||||
save...
|
save...
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
} else {
|
} else {
|
||||||
return <Nav.item icon='fas fa-save' className='saveButton' onClick={this.save}>
|
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
|
||||||
save
|
save
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
print : function(){
|
print : function(){
|
||||||
localStorage.setItem('print', `<style>\n${this.state.brew.style}\n</style>\n\n${this.state.brew.text}`);
|
|
||||||
window.open('/print?dialog=true&local=print', '_blank');
|
window.open('/print?dialog=true&local=print', '_blank');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -212,7 +270,7 @@ const NewPage = createClass({
|
|||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderSaveButton()}
|
{this.renderSaveButton()}
|
||||||
{this.renderLocalPrintButton()}
|
{this.renderLocalPrintButton()}
|
||||||
<IssueNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
@@ -1,10 +1,82 @@
|
|||||||
.newPage{
|
.newPage{
|
||||||
|
.navItem.save{
|
||||||
.saveButton{
|
|
||||||
background-color: @orange;
|
background-color: @orange;
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color: @green;
|
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,7 +7,12 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
const BREWKEY = 'homebrewery-new';
|
||||||
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
|
||||||
const PrintPage = createClass({
|
const PrintPage = createClass({
|
||||||
|
displayName : 'PrintPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
query : {},
|
query : {},
|
||||||
@@ -21,36 +26,56 @@ const PrintPage = createClass({
|
|||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brewText : this.props.brew.text
|
brew : {
|
||||||
|
text : this.props.brew.text || '',
|
||||||
|
style : this.props.brew.style || undefined,
|
||||||
|
renderer : this.props.brew.renderer || 'legacy'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
if(this.props.query.local){
|
if(this.props.query.local == 'print'){
|
||||||
this.setState((prevState, prevProps)=>({
|
const brewStorage = localStorage.getItem(BREWKEY);
|
||||||
brewText : localStorage.getItem(prevProps.query.local)
|
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||||
}));
|
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||||
|
|
||||||
|
this.setState((prevState, prevProps)=>{
|
||||||
|
return {
|
||||||
|
brew : {
|
||||||
|
text : brewStorage,
|
||||||
|
style : styleStorage,
|
||||||
|
renderer : metaStorage.renderer || 'legacy'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.props.query.dialog) window.print();
|
if(this.props.query.dialog) window.print();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderStyle : function() {
|
||||||
|
if(!this.state.brew.style) return;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.state.brew.style} </style>` }} />;
|
||||||
|
},
|
||||||
|
|
||||||
renderPages : function(){
|
renderPages : function(){
|
||||||
if(this.props.brew.renderer == 'legacy') {
|
if(this.state.brew.renderer == 'legacy') {
|
||||||
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
|
return _.map(this.state.brew.text.split('\\page'), (pageText, index)=>{
|
||||||
return <div
|
return <div
|
||||||
className='phb page'
|
className='phb page'
|
||||||
id={`p${index + 1}`}
|
id={`p${index + 1}`}
|
||||||
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(page) }}
|
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }}
|
||||||
key={index} />;
|
key={index} />;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return _.map(this.state.brewText.split(/^\\page/gm), (page, index)=>{
|
return _.map(this.state.brew.text.split(/^\\page$/gm), (pageText, index)=>{
|
||||||
return <div
|
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
className='phb3 page'
|
return (
|
||||||
id={`p${index + 1}`}
|
<div className='page' id={`p${index + 1}`} key={index} >
|
||||||
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||||
key={index} />;
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,10 +84,12 @@ const PrintPage = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
return <div>
|
return <div>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<link href={`${this.props.brew.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
<link href={`${this.state.brew.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||||
{/* Apply CSS from Style tab */}
|
{/* Apply CSS from Style tab */}
|
||||||
<div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.brew.style} </style>` }} />
|
{this.renderStyle()}
|
||||||
{this.renderPages()}
|
<div className='pages' ref='pages'>
|
||||||
|
{this.renderPages()}
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = createClass({
|
||||||
|
displayName : 'SharePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
@@ -29,23 +30,19 @@ const SharePage = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
showDropdown : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
handleControlKeys : function(e){
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == P_KEY){
|
if(e.keyCode == P_KEY){
|
||||||
window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
|
window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
@@ -57,28 +54,6 @@ const SharePage = createClass({
|
|||||||
this.props.brew.shareId;
|
this.props.brew.shareId;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDropdown : function(show){
|
|
||||||
this.setState({
|
|
||||||
showDropdown : show
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDropdown : function(){
|
|
||||||
if(!this.state.showDropdown) return null;
|
|
||||||
|
|
||||||
return <div className='dropdown'>
|
|
||||||
<a href={`/source/${this.processShareId()}`} className='item'>
|
|
||||||
view
|
|
||||||
</a>
|
|
||||||
<a href={`/download/${this.processShareId()}`} className='item'>
|
|
||||||
download
|
|
||||||
</a>
|
|
||||||
<a href={`/new/${this.processShareId()}`} className='item'>
|
|
||||||
clone to new
|
|
||||||
</a>
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='sharePage sitePage'>
|
return <div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
@@ -90,12 +65,20 @@ const SharePage = createClass({
|
|||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.props.brew.shareId && <>
|
{this.props.brew.shareId && <>
|
||||||
<PrintLink shareId={this.processShareId()} />
|
<PrintLink shareId={this.processShareId()} />
|
||||||
<Nav.item icon='fas fa-code' color='red' className='source'
|
<Nav.dropdown>
|
||||||
onMouseEnter={()=>this.handleDropdown(true)}
|
<Nav.item color='red' icon='fas fa-code'>
|
||||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
source
|
||||||
source
|
</Nav.item>
|
||||||
{this.renderDropdown()}
|
<Nav.item color='blue' href={`/source/${this.processShareId()}`}>
|
||||||
</Nav.item>
|
view
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' href={`/download/${this.processShareId()}`}>
|
||||||
|
download
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' href={`/new/${this.processShareId()}`}>
|
||||||
|
clone to new
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>
|
||||||
</>}
|
</>}
|
||||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
||||||
<Account />
|
<Account />
|
||||||
|
|||||||
@@ -2,49 +2,4 @@
|
|||||||
.content{
|
.content{
|
||||||
overflow-y : hidden;
|
overflow-y : hidden;
|
||||||
}
|
}
|
||||||
.source.navItem{
|
}
|
||||||
position : relative;
|
|
||||||
.dropdown{
|
|
||||||
position : absolute;
|
|
||||||
top : 28px;
|
|
||||||
left : 0px;
|
|
||||||
z-index : 10000;
|
|
||||||
width : 100%;
|
|
||||||
h4{
|
|
||||||
display : block;
|
|
||||||
box-sizing : border-box;
|
|
||||||
padding : 5px 0px;
|
|
||||||
background-color : #333;
|
|
||||||
font-size : 0.8em;
|
|
||||||
color : #bbb;
|
|
||||||
text-align : center;
|
|
||||||
border-top : 1px solid #888;
|
|
||||||
&:nth-of-type(1){ background-color: darken(@teal, 20%); }
|
|
||||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
|
||||||
}
|
|
||||||
.item{
|
|
||||||
.animate(background-color);
|
|
||||||
position : relative;
|
|
||||||
display : block;
|
|
||||||
width : 100%;
|
|
||||||
vertical-align : middle;
|
|
||||||
padding : 13px 5px;
|
|
||||||
box-sizing : border-box;
|
|
||||||
background-color : #333;
|
|
||||||
color : white;
|
|
||||||
text-decoration : none;
|
|
||||||
border-top : 1px solid #888;
|
|
||||||
&:hover{
|
|
||||||
background-color : @blue;
|
|
||||||
}
|
|
||||||
.title{
|
|
||||||
display : inline-block;
|
|
||||||
overflow : hidden;
|
|
||||||
width : 100%;
|
|
||||||
text-overflow : ellipsis;
|
|
||||||
white-space : nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,20 @@
|
|||||||
require('./userPage.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
// const brew = {
|
|
||||||
// title : 'SUPER Long title woah now',
|
|
||||||
// authors : []
|
|
||||||
// };
|
|
||||||
|
|
||||||
//const BREWS = _.times(25, ()=>{ return brew;});
|
|
||||||
|
|
||||||
|
|
||||||
const UserPage = createClass({
|
const UserPage = createClass({
|
||||||
|
displayName : 'UserPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
@@ -28,130 +22,47 @@ const UserPage = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `'` : `'s`);
|
||||||
sortType : 'alpha',
|
|
||||||
sortDir : 'asc'
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getUsernameWithS : function() {
|
|
||||||
if(this.props.username.endsWith('s'))
|
|
||||||
return `${this.props.username}'`;
|
|
||||||
return `${this.props.username}'s`;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderBrews : function(brews){
|
const brews = _.groupBy(this.props.brews, (brew)=>{
|
||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
|
||||||
|
|
||||||
const sortedBrews = this.sortBrews(brews);
|
|
||||||
|
|
||||||
return _.map(sortedBrews, (brew, idx)=>{
|
|
||||||
return <BrewItem brew={brew} key={idx}/>;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
sortBrewOrder : function(brew){
|
|
||||||
if(!brew.title){brew.title = 'No Title';};
|
|
||||||
const mapping = {
|
|
||||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
|
||||||
'created' : brew.createdAt,
|
|
||||||
'updated' : brew.updatedAt,
|
|
||||||
'views' : brew.views,
|
|
||||||
'latest' : brew.lastViewed
|
|
||||||
};
|
|
||||||
return mapping[this.state.sortType];
|
|
||||||
},
|
|
||||||
|
|
||||||
sortBrews : function(brews){
|
|
||||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSortOptionChange : function(event){
|
|
||||||
this.setState({
|
|
||||||
sortType : event.target.value
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSortDirChange : function(event){
|
|
||||||
this.setState({
|
|
||||||
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderSortOption : function(sortTitle, sortValue){
|
|
||||||
return <td>
|
|
||||||
<button
|
|
||||||
value={`${sortValue}`}
|
|
||||||
onClick={this.handleSortOptionChange}
|
|
||||||
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
|
||||||
>
|
|
||||||
{`${sortTitle}`}
|
|
||||||
</button>
|
|
||||||
</td>;
|
|
||||||
},
|
|
||||||
|
|
||||||
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>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
getSortedBrews : function(){
|
|
||||||
return _.groupBy(this.props.brews, (brew)=>{
|
|
||||||
return (brew.published ? 'published' : 'private');
|
return (brew.published ? 'published' : 'private');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const brewCollection = [
|
||||||
|
{
|
||||||
|
title : `${usernameWithS} published brews`,
|
||||||
|
class : 'published',
|
||||||
|
brews : brews.published
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if(this.props.username == global.account?.username){
|
||||||
|
brewCollection.push(
|
||||||
|
{
|
||||||
|
title : `${usernameWithS} unpublished brews`,
|
||||||
|
class : 'unpublished',
|
||||||
|
brews : brews.private
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
brewCollection : brewCollection
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
navItems : function() {
|
||||||
|
return <Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
const brews = this.getSortedBrews();
|
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()}></ListPage>;
|
||||||
|
|
||||||
return <div className='userPage sitePage'>
|
|
||||||
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
|
||||||
<Navbar>
|
|
||||||
<Nav.section>
|
|
||||||
<NewBrew />
|
|
||||||
<RecentNavItem />
|
|
||||||
<Account />
|
|
||||||
</Nav.section>
|
|
||||||
</Navbar>
|
|
||||||
|
|
||||||
<div className='content V3'>
|
|
||||||
<div className='phb'>
|
|
||||||
{this.renderSortOptions()}
|
|
||||||
<div>
|
|
||||||
<h1>{this.getUsernameWithS()} brews</h1>
|
|
||||||
{this.renderBrews(brews.published)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1>{this.getUsernameWithS()} unpublished brews</h1>
|
|
||||||
{this.renderBrews(brews.private)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
"host" : "homebrewery.local.naturalcrit.com:8000",
|
"host" : "homebrewery.local.naturalcrit.com:8000",
|
||||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||||
"secret" : "secret",
|
"secret" : "secret",
|
||||||
"web_port" : 8000
|
"web_port" : 8000,
|
||||||
|
"enable_v3" : true
|
||||||
}
|
}
|
||||||
|
|||||||
152
faq.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
```css
|
||||||
|
h5 {
|
||||||
|
font-size: .35cm !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskList li {
|
||||||
|
list-style-type : none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskList li input {
|
||||||
|
margin-left : -0.52cm;
|
||||||
|
transform: translateY(.05cm);
|
||||||
|
filter: brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||||
|
}
|
||||||
|
|
||||||
|
.taskList li input[checked] {
|
||||||
|
filter: sepia(100%) hue-rotate(60deg) saturate(3.5) contrast(4) brightness(1.1) drop-shadow(1px 2px 1px #222);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre + * {
|
||||||
|
margin-top: 0.17cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin-top: 0.17cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page pre code {
|
||||||
|
word-break:break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p + pre {
|
||||||
|
margin-top : 0.1cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h1 + p:first-letter {
|
||||||
|
all:unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .toc ul {
|
||||||
|
margin-top:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h3 {
|
||||||
|
font-family:inherit;
|
||||||
|
font-size:inherit;
|
||||||
|
border:inherit;
|
||||||
|
margin-top:12px;
|
||||||
|
margin-bottom:5px
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h3:before {
|
||||||
|
content:'Q.';
|
||||||
|
position:absolute;
|
||||||
|
font-size:2em;
|
||||||
|
margin-left:-1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .columnSplit + h3 {
|
||||||
|
margin-top:0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# FAQ
|
||||||
|
{{wide Updated Oct. 11, 2021}}
|
||||||
|
|
||||||
|
|
||||||
|
### The site is down for me! Anyone else?
|
||||||
|
|
||||||
|
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
|
||||||
|
|
||||||
|
### How do I log out?
|
||||||
|
|
||||||
|
Go to https://homebrewery.naturalcrit.com/login, and hit the "*logout*" link.
|
||||||
|
|
||||||
|
### Why am I getting an error when trying to save, and my account is linked to Google?
|
||||||
|
|
||||||
|
A sign-in with Google only lasts a year until the authentication expires. You must go [here](https://www.naturalcrit.com/login), click the *Log-out* button, and then sign back in using your Google account.
|
||||||
|
|
||||||
|
### I lost my password, how do I reset it? How do I change my password?
|
||||||
|
|
||||||
|
Homebrewery is specifically designed to not hold personal information as a measure to protect both users and admin, and does not require an email address. Thus it would be difficult to send a new password to a user. Reach out to the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username.
|
||||||
|
|
||||||
|
If you have linked your account with a Google account, you would change your password within Google.
|
||||||
|
|
||||||
|
### Is there a way to restore a previous version of my brew?
|
||||||
|
|
||||||
|
Currently, there is no way to do this through the site yourself. This would take too much of a toll on the amount of storage the homebrewery requires. However, we do have daily backups of our database that we keep for 8 days, and you can contact the moderators on [the subreddit](https://www.reddit.com/r/homebrewery) with your Homebrewery username, the name of the lost brew, and the last known time it was working properly. We can manually look through our backups and restore it if it exists.
|
||||||
|
|
||||||
|
### I worked on a brew for X hours, and suddenly all the text disappeared!
|
||||||
|
|
||||||
|
This usually happens if you accidentally drag-select all of your text and then start typing which overwrites the selection. Do not panic, and do not refresh the page or reload your brew quite yet as it is probably auto-saved in this state already. Simply press CTRL+Z as many times as needed to undo your last few changes and you will be back to where you were, then make sure to save your brew in the "good" state.
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Why is only Chrome supported?
|
||||||
|
|
||||||
|
Different browsers have differing abilities to handle web styling (or "CSS"). For example, Firefox is not currently capable of handling column breaks well but Chrome has no problem. Also, each browser has slight differences in how they display pages which can make it a nightmare to compensate for. These capabilities change over time and we are hopeful that each browser update bridges these gaps and adds more features; until then, we will develop with one browser in mind.
|
||||||
|
|
||||||
|
### Both my friend and myself are using Chrome, but the brews still look different. Why?
|
||||||
|
|
||||||
|
A pixel can be rendered differently depending on the browser, operating system, computer, or screen. Unless you and your friend have exactly the same setup, it is likely your online brew will have very tiny differences. However, sometimes a few pixels is all it takes to create *big* differences....for example, an extra pixel can cause a whole line of text or even a monster stat block to run out of space in it's current column and be pushed to the next column or even off the page.
|
||||||
|
|
||||||
|
The best way to avoid this is to leave space at the end of a column equal to one or two lines of text. Or, create a PDF from your document for sharing--- PDF's are designed to be rendered the same on all devices.
|
||||||
|
|
||||||
|
### Why do I need to manually create a new page? Why doesn't text flow between pages?
|
||||||
|
|
||||||
|
A Homebrewery document is at it's core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
|
||||||
|
|
||||||
|
### Where do I get images?
|
||||||
|
The Homebrewery does not provide images for use besides some page elements and example images for snippets. You will need to find your own images for use and be sure you are following the appropriate license requirements.
|
||||||
|
|
||||||
|
Once you have an image you would like to use, it is recommended to host it somewhere that won't disappear; commonly, people host their images on [Imgur](https://www.imgur.com). Create an account and upload your images there, and use the *Direct Link* that is shown when you click into the image from the gallery in your Homebrewery document.
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### A particular font does not work for my language, what do I do?
|
||||||
|
The fonts used were originally created for use with the English language, though revisions since then have added more support for other languages. They are still not complete sets and may be missing a glyph/character you need. Unfortunately, the volunteer group as it stands at the time of this writing does not have a font guru, so it would be difficult to add more glyphs (especially complicated glyphs). Let us know which glyph is missing on the subreddit, but you may need to search [Google Fonts](https://fonts.google.com) for an alternative font if you need something fast.
|
||||||
|
|
||||||
|
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
|
||||||
|
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
|
||||||
|
|
||||||
|
### The preview window is suddenly gone, I can only see the editor side of the Homebrewery (or the other way around).
|
||||||
|
|
||||||
|
1. Press `CTRL`+`SHIFT`+`i` (or right-click and select "Inspect") while in the Homebrewery.
|
||||||
|
|
||||||
|
2. Expand...
|
||||||
|
```
|
||||||
|
- `body`
|
||||||
|
- `main`
|
||||||
|
- `div class="homebrew"`
|
||||||
|
- `div class="editPage page"`
|
||||||
|
- `div class="content"`
|
||||||
|
- `div class="splitPane"`
|
||||||
|
```
|
||||||
|
|
||||||
|
There you will find 3 divs: `div class="pane" [...]`, `div class="divider" [...]`, and `div class="pane" [...]`.
|
||||||
|
|
||||||
|
The `class="pane"` looks similar to this: `div class="pane" data-reactid="36" style="flex: 0 0 auto; width: 925px;"`.
|
||||||
|
|
||||||
|
Change whatever stands behind width: to something smaller than your display width.
|
||||||
|
|
||||||
|
### I have white borders on the bottom/sides of the print preview.
|
||||||
|
|
||||||
|
The Homebrewery paper size and your print paper size do not match.
|
||||||
|
|
||||||
|
The Homebrewery defaults to creating US Letter page sizes. If you are printing with A4 size paper, you must add the "A4 Page Size" snippet. In the "Print" dialog be sure your Paper Size matches the page size in Homebrewery.
|
||||||
|
|
||||||
|
|
||||||
|
### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page?
|
||||||
|
|
||||||
|
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software.
|
||||||
@@ -10,7 +10,7 @@ These instructions assume that you are installing to a completely new, fresh Fre
|
|||||||
|
|
||||||
2. Install wget (`pkg install -y wget`). On a fresh jail, you will be prompted to press 'Y' to set up `pkg`.
|
2. Install wget (`pkg install -y wget`). On a fresh jail, you will be prompted to press 'Y' to set up `pkg`.
|
||||||
|
|
||||||
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
|
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
|
||||||
|
|
||||||
4. Make the downloaded file executable (`chmod +x install.sh`).
|
4. Make the downloaded file executable (`chmod +x install.sh`).
|
||||||
|
|
||||||
35
install/README.UBUNTU.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Ubuntu Installation Instructions
|
||||||
|
|
||||||
|
## Before Installing
|
||||||
|
|
||||||
|
These instructions assume that you are installing to a completely new, fresh Ubuntu installation. As such, some steps will not be necessary if you are installing to an existing Ubuntu instance.
|
||||||
|
|
||||||
|
## Installation instructions
|
||||||
|
|
||||||
|
1. Install Ubuntu.
|
||||||
|
|
||||||
|
2. Install wget (`apt install -y wget`). This may already be installed, depending on your exact Ubuntu version.
|
||||||
|
|
||||||
|
3. Download the installation script (`wget https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/ubuntu/install.sh`).
|
||||||
|
|
||||||
|
4. Make the downloaded file executable (`chmod +x install.sh`).
|
||||||
|
|
||||||
|
5. Run the script (`sudo ./install.sh`). This will automatically download all of the required packages, install both them and HomeBrewery, configure the system and finally start HomeBrewery.
|
||||||
|
|
||||||
|
**NOTE:** At this time, the script **ONLY** installs HomeBrewery. It does **NOT** install the NaturalCrit login system, as that is currently a completely separate project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
These installation instructions have been tested on the following Ubuntu releases:
|
||||||
|
|
||||||
|
- *ubuntu-20.04.3-desktop-amd64*
|
||||||
|
|
||||||
|
## Final Notes
|
||||||
|
|
||||||
|
While this installation process works successfully at the time of writing (December 19, 2021), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
||||||
|
|
||||||
|
Regards,
|
||||||
|
G
|
||||||
|
December 19, 2021
|
||||||
13
install/ubuntu/etc/systemd/system/homebrewery.service
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Homebrewery Web Server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=root
|
||||||
|
After=mongodb
|
||||||
|
Environment=NODE_ENV=local
|
||||||
|
WorkingDirectory=/usr/local/homebrewery
|
||||||
|
ExecStart=node server.js
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
34
install/ubuntu/install.sh
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Install CURL and add required NodeJS source to package repo
|
||||||
|
echo ::Install CURL
|
||||||
|
apt install -y curl
|
||||||
|
echo ::Add NodeJS source to package repo
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||||
|
|
||||||
|
# Install required packages
|
||||||
|
echo ::Install Homebrewery requirements
|
||||||
|
apt satisfy -y git nodejs npm mongodb
|
||||||
|
|
||||||
|
# Clone Homebrewery repo
|
||||||
|
echo ::Get Homebrewery files
|
||||||
|
cd /usr/local/
|
||||||
|
git clone https://github.com/naturalcrit/homebrewery.git
|
||||||
|
|
||||||
|
# Install Homebrewery
|
||||||
|
echo ::Install Homebrewery
|
||||||
|
cd homebrewery
|
||||||
|
npm install
|
||||||
|
npm audit fix
|
||||||
|
npm run postinstall
|
||||||
|
|
||||||
|
# Create Homebrewery service
|
||||||
|
echo ::Create Homebrewery service
|
||||||
|
ln -s /usr/local/homebrewery/install/ubuntu/etc/systemd/system/homebrewery.service /etc/systemd/system/homebrewery.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
echo ::Set Homebrewery to start automatically
|
||||||
|
systemctl enable homebrewery
|
||||||
|
|
||||||
|
# Start Homebrewery
|
||||||
|
echo ::Start Homebrewery
|
||||||
|
systemctl start homebrewery
|
||||||
12008
package-lock.json
generated
69
package.json
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "2.13.2",
|
"version": "3.0.8",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "14.15.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -18,8 +18,11 @@
|
|||||||
"lint:dry": "eslint **/*.{js,jsx}",
|
"lint:dry": "eslint **/*.{js,jsx}",
|
||||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "pico-check",
|
"test": "jest",
|
||||||
"test:dev": "pico-check -v -w",
|
"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",
|
||||||
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"phb": "node scripts/phb.js",
|
"phb": "node scripts/phb.js",
|
||||||
"prod": "set NODE_ENV=production && npm run build",
|
"prod": "set NODE_ENV=production && npm run build",
|
||||||
"postinstall": "npm run buildall",
|
"postinstall": "npm run buildall",
|
||||||
@@ -30,53 +33,63 @@
|
|||||||
"eslintIgnore": [
|
"eslintIgnore": [
|
||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"pico-check": {
|
"jest": {
|
||||||
"require": "./tests/test.init.js"
|
"testTimeout" : 15000,
|
||||||
|
"modulePaths": [
|
||||||
|
"mode_modules",
|
||||||
|
"shared",
|
||||||
|
"server"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
"@babel/preset-env",
|
"@babel/preset-env",
|
||||||
"@babel/preset-react"
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-transform-runtime"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.14.8",
|
"@babel/core": "^7.17.8",
|
||||||
"@babel/plugin-transform-runtime": "^7.14.5",
|
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||||
"@babel/preset-env": "^7.14.8",
|
"@babel/preset-env": "^7.16.11",
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"codemirror": "^5.62.2",
|
"codemirror": "^5.65.2",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.6",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.9.0",
|
"dedent-tabs": "^0.10.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.3",
|
||||||
"express-async-handler": "^1.1.4",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.1",
|
"express-static-gzip": "2.1.5",
|
||||||
"fs-extra": "10.0.0",
|
"fs-extra": "10.0.1",
|
||||||
"googleapis": "82.0.0",
|
"googleapis": "98.0.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "2.1.3",
|
"marked": "4.0.12",
|
||||||
|
"marked-extended-tables": "^1.0.3",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"mongoose": "^5.13.4",
|
"mongoose": "^6.2.8",
|
||||||
"nanoid": "3.1.23",
|
"nanoid": "3.3.1",
|
||||||
"nconf": "^0.11.3",
|
"nconf": "^0.11.3",
|
||||||
"prop-types": "15.7.2",
|
"query-string": "7.1.1",
|
||||||
"query-string": "7.0.1",
|
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-frame-component": "4.1.3",
|
"react-frame-component": "4.1.3",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.3.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.31.0",
|
"eslint": "^8.11.0",
|
||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"pico-check": "^2.1.3"
|
"jest": "^27.5.1",
|
||||||
|
"supertest": "^6.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ const transforms = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const build = async ({ bundle, render, ssr })=>{
|
const build = async ({ bundle, render, ssr })=>{
|
||||||
await fs.outputFile('./build/admin/bundle.css', await lessTransform.generate({ paths: './shared' }));
|
const css = await lessTransform.generate({ paths: './shared' });
|
||||||
|
await fs.outputFile('./build/admin/bundle.css', css);
|
||||||
await fs.outputFile('./build/admin/bundle.js', bundle);
|
await fs.outputFile('./build/admin/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/admin/ssr.js', ssr);
|
await fs.outputFile('./build/admin/ssr.js', ssr);
|
||||||
await fs.outputFile('./build/admin/render.js', render);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.emptyDirSync('./build/admin');
|
fs.emptyDirSync('./build/admin');
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const build = async ({ bundle, render, ssr })=>{
|
|||||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||||
await fs.copy('./themes/fonts', './build/fonts');
|
await fs.copy('./themes/fonts', './build/fonts');
|
||||||
|
await fs.copy('./themes/assets', './build/assets');
|
||||||
let src = './themes/5ePhbLegacy.style.less';
|
let src = './themes/5ePhbLegacy.style.less';
|
||||||
//Parse brew theme files
|
//Parse brew theme files
|
||||||
less.render(fs.readFileSync(src).toString(), {
|
less.render(fs.readFileSync(src).toString(), {
|
||||||
@@ -73,6 +74,6 @@ pack('./client/homebrew/homebrew.jsx', {
|
|||||||
if(isDev){
|
if(isDev){
|
||||||
livereload('./build');
|
livereload('./build');
|
||||||
watchFile('./server.js', {
|
watchFile('./server.js', {
|
||||||
watch : ['./client'] // Watch additional folders if you want
|
watch : ['./client', './server'] // Watch additional folders if you want
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,18 @@
|
|||||||
"codemirror/mode/gfm/gfm.js",
|
"codemirror/mode/gfm/gfm.js",
|
||||||
"codemirror/mode/css/css.js",
|
"codemirror/mode/css/css.js",
|
||||||
"codemirror/mode/javascript/javascript.js",
|
"codemirror/mode/javascript/javascript.js",
|
||||||
|
"codemirror/addon/fold/foldcode.js",
|
||||||
|
"codemirror/addon/fold/foldgutter.js",
|
||||||
|
"codemirror/addon/fold/xml-fold.js",
|
||||||
|
"codemirror/addon/search/search.js",
|
||||||
|
"codemirror/addon/search/searchcursor.js",
|
||||||
|
"codemirror/addon/search/jump-to-line.js",
|
||||||
|
"codemirror/addon/search/match-highlighter.js",
|
||||||
|
"codemirror/addon/search/matchesonscrollbar.js",
|
||||||
|
"codemirror/addon/dialog/dialog.js",
|
||||||
|
"codemirror/addon/edit/closetag.js",
|
||||||
|
"codemirror/addon/edit/trailingspace.js",
|
||||||
|
"codemirror/addon/selection/active-line.js",
|
||||||
"moment",
|
"moment",
|
||||||
"superagent",
|
"superagent",
|
||||||
"marked"
|
"marked"
|
||||||
|
|||||||
273
server.js
@@ -1,267 +1,12 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
const DB = require('./server/db.js');
|
||||||
const _ = require('lodash');
|
const server = require('./server/app.js');
|
||||||
const jwt = require('jwt-simple');
|
const config = require('./server/config.js');
|
||||||
const express = require('express');
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
const homebrewApi = require('./server/homebrew.api.js');
|
DB.connect(config).then(()=>{
|
||||||
const GoogleActions = require('./server/googleActions.js');
|
// Ensure that we have successfully connected to the database
|
||||||
const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
|
// before launching server
|
||||||
const sanitizeFilename = require('sanitize-filename');
|
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||||
const asyncHandler = require('express-async-handler');
|
server.app.listen(PORT, ()=>{
|
||||||
|
console.log(`server on port: ${PORT}`);
|
||||||
const brewAccessTypes = ['edit', 'share', 'raw'];
|
|
||||||
|
|
||||||
//Get the brew object from the HB database or Google Drive
|
|
||||||
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
|
||||||
if(!brewAccessTypes.includes(accessType))
|
|
||||||
throw ('Invalid Access Type when getting brew');
|
|
||||||
let brew;
|
|
||||||
if(id.length > 12) {
|
|
||||||
const googleId = id.slice(0, -12);
|
|
||||||
id = id.slice(-12);
|
|
||||||
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
|
|
||||||
} else {
|
|
||||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
|
||||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
|
||||||
}
|
|
||||||
|
|
||||||
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
|
||||||
//Split brew.text into text and style
|
|
||||||
//unless the Access Type is RAW, in which case return immediately
|
|
||||||
if(accessType == 'raw') {
|
|
||||||
return brew;
|
|
||||||
}
|
|
||||||
if(brew.text.startsWith('```css')) {
|
|
||||||
const index = brew.text.indexOf('```\n\n');
|
|
||||||
brew.style = brew.text.slice(7, index - 1);
|
|
||||||
brew.text = brew.text.slice(index + 5);
|
|
||||||
}
|
|
||||||
return brew;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sanitizeBrew = (brew, full=false)=>{
|
|
||||||
delete brew._id;
|
|
||||||
delete brew.__v;
|
|
||||||
if(full){
|
|
||||||
delete brew.editId;
|
|
||||||
}
|
|
||||||
return brew;
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
|
|
||||||
|
|
||||||
process.chdir(__dirname);
|
|
||||||
|
|
||||||
//app.use(express.static(`${__dirname}/build`));
|
|
||||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
|
||||||
app.use(require('cookie-parser')());
|
|
||||||
app.use(require('./server/forcessl.mw.js'));
|
|
||||||
|
|
||||||
const config = require('nconf')
|
|
||||||
.argv()
|
|
||||||
.env({ lowerCase: true })
|
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
|
||||||
.file('defaults', { file: 'config/default.json' });
|
|
||||||
|
|
||||||
//DB
|
|
||||||
const mongoose = require('mongoose');
|
|
||||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
|
|
||||||
{ retryWrites: false, useNewUrlParser: true, useCreateIndex: true, useUnifiedTopology: true });
|
|
||||||
mongoose.connection.on('error', ()=>{
|
|
||||||
console.log('Error : Could not connect to a Mongo Database.');
|
|
||||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
|
||||||
throw 'Can not connect to Mongo';
|
|
||||||
});
|
|
||||||
|
|
||||||
//Account Middleware
|
|
||||||
app.use((req, res, next)=>{
|
|
||||||
if(req.cookies && req.cookies.nc_session){
|
|
||||||
try {
|
|
||||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
|
||||||
//console.log("Just loaded up JWT from cookie:");
|
|
||||||
//console.log(req.account);
|
|
||||||
} catch (e){}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.config = {
|
|
||||||
google_client_id : config.get('google_client_id'),
|
|
||||||
google_client_secret : config.get('google_client_secret')
|
|
||||||
};
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(homebrewApi);
|
|
||||||
app.use(require('./server/admin.api.js'));
|
|
||||||
|
|
||||||
const HomebrewModel = require('./server/homebrew.model.js').model;
|
|
||||||
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
|
||||||
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
|
|
||||||
|
|
||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
|
||||||
|
|
||||||
//Robots.txt
|
|
||||||
app.get('/robots.txt', (req, res)=>{
|
|
||||||
return res.sendFile(`${__dirname}/robots.txt`);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Home page
|
|
||||||
app.get('/', async (req, res, next)=>{
|
|
||||||
const brew = {
|
|
||||||
text : welcomeText
|
|
||||||
};
|
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Changelog page
|
|
||||||
app.get('/changelog', async (req, res, next)=>{
|
|
||||||
const brew = {
|
|
||||||
title : 'Changelog',
|
|
||||||
text : changelogText
|
|
||||||
};
|
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
//Source page
|
|
||||||
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
|
||||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
|
||||||
|
|
||||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
|
||||||
let text = brew.text;
|
|
||||||
for (const replaceStr in replaceStrings) {
|
|
||||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
|
||||||
}
|
|
||||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
|
||||||
res.status(200).send(text);
|
|
||||||
}));
|
|
||||||
|
|
||||||
//Download brew source page
|
|
||||||
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
|
||||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
|
||||||
const prefix = 'HB - ';
|
|
||||||
|
|
||||||
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"`
|
|
||||||
});
|
});
|
||||||
res.status(200).send(brew.text);
|
|
||||||
}));
|
|
||||||
|
|
||||||
//User Page
|
|
||||||
app.get('/user/:username', async (req, res, next)=>{
|
|
||||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
|
||||||
|
|
||||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
|
||||||
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(googleBrews)
|
|
||||||
brews = _.concat(brews, googleBrews);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.brews = _.map(brews, (brew)=>{
|
|
||||||
return sanitizeBrew(brew, !ownAccount);
|
|
||||||
});
|
|
||||||
|
|
||||||
return next();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Edit Page
|
|
||||||
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
|
||||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
|
||||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
|
||||||
}));
|
|
||||||
|
|
||||||
//New Page
|
|
||||||
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
|
||||||
const brew = await getBrewFromId(req.params.id, 'share');
|
|
||||||
brew.title = `CLONE - ${brew.title}`;
|
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
|
||||||
}));
|
|
||||||
|
|
||||||
//Share Page
|
|
||||||
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
|
||||||
const brew = await getBrewFromId(req.params.id, 'share');
|
|
||||||
|
|
||||||
if(req.params.id.length > 12) {
|
|
||||||
const googleId = req.params.id.slice(0, -12);
|
|
||||||
const shareId = req.params.id.slice(-12);
|
|
||||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
|
||||||
.catch((err)=>{next(err);});
|
|
||||||
} else {
|
|
||||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
|
||||||
}));
|
|
||||||
|
|
||||||
//Print Page
|
|
||||||
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
|
||||||
const brew = await getBrewFromId(req.params.id, 'share');
|
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
|
||||||
}));
|
|
||||||
|
|
||||||
//Render the page
|
|
||||||
const templateFn = require('./client/template.js');
|
|
||||||
app.use((req, res)=>{
|
|
||||||
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')
|
|
||||||
};
|
|
||||||
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
|
|
||||||
.then((page)=>{ res.send(page); })
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
return res.sendStatus(500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//v=====----- Error-Handling Middleware -----=====v//
|
|
||||||
//Format Errors so all fields will be sent
|
|
||||||
const replaceErrors = (key, value)=>{
|
|
||||||
if(value instanceof Error) {
|
|
||||||
const error = {};
|
|
||||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
|
||||||
error[key] = value[key];
|
|
||||||
});
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPureError = (error)=>{
|
|
||||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
|
||||||
};
|
|
||||||
|
|
||||||
app.use((err, req, res, next)=>{
|
|
||||||
const status = err.status || 500;
|
|
||||||
console.error(err);
|
|
||||||
res.status(status).send(getPureError(err));
|
|
||||||
});
|
|
||||||
//^=====--------------------------------------=====^//
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
|
||||||
app.listen(PORT);
|
|
||||||
console.log(`server on port:${PORT}`);
|
|
||||||
|
|||||||
309
server/app.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
|
// Set working directory to project root
|
||||||
|
process.chdir(`${__dirname}/..`);
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
const jwt = require('jwt-simple');
|
||||||
|
const express = require('express');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const app = express();
|
||||||
|
const config = require('./config.js');
|
||||||
|
|
||||||
|
const homebrewApi = require('./homebrew.api.js');
|
||||||
|
const GoogleActions = require('./googleActions.js');
|
||||||
|
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||||
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
|
const asyncHandler = require('express-async-handler');
|
||||||
|
|
||||||
|
const brewAccessTypes = ['edit', 'share', 'raw'];
|
||||||
|
|
||||||
|
//Get the brew object from the HB database or Google Drive
|
||||||
|
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
||||||
|
if(!brewAccessTypes.includes(accessType))
|
||||||
|
throw ('Invalid Access Type when getting brew');
|
||||||
|
let brew;
|
||||||
|
if(id.length > 12) {
|
||||||
|
const googleId = id.slice(0, -12);
|
||||||
|
id = id.slice(-12);
|
||||||
|
brew = await GoogleActions.getGoogleBrew(googleId, id, accessType);
|
||||||
|
} else {
|
||||||
|
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
||||||
|
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
||||||
|
}
|
||||||
|
|
||||||
|
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
||||||
|
//Split brew.text into text and style
|
||||||
|
//unless the Access Type is RAW, in which case return immediately
|
||||||
|
if(accessType == 'raw') {
|
||||||
|
return brew;
|
||||||
|
}
|
||||||
|
splitTextStyleAndMetadata(brew);
|
||||||
|
return brew;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sanitizeBrew = (brew, full=false)=>{
|
||||||
|
delete brew._id;
|
||||||
|
delete brew.__v;
|
||||||
|
if(full){
|
||||||
|
delete brew.editId;
|
||||||
|
}
|
||||||
|
return brew;
|
||||||
|
};
|
||||||
|
|
||||||
|
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']));
|
||||||
|
brew.text = brew.text.slice(index + 5);
|
||||||
|
}
|
||||||
|
if(brew.text.startsWith('```css')) {
|
||||||
|
const index = brew.text.indexOf('```\n\n');
|
||||||
|
brew.style = brew.text.slice(7, index - 1);
|
||||||
|
brew.text = brew.text.slice(index + 5);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
|
|
||||||
|
//app.use(express.static(`${__dirname}/build`));
|
||||||
|
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||||
|
app.use(require('cookie-parser')());
|
||||||
|
app.use(require('./forcessl.mw.js'));
|
||||||
|
|
||||||
|
//Account Middleware
|
||||||
|
app.use((req, res, next)=>{
|
||||||
|
if(req.cookies && req.cookies.nc_session){
|
||||||
|
try {
|
||||||
|
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||||
|
//console.log("Just loaded up JWT from cookie:");
|
||||||
|
//console.log(req.account);
|
||||||
|
} catch (e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.config = {
|
||||||
|
google_client_id : config.get('google_client_id'),
|
||||||
|
google_client_secret : config.get('google_client_secret')
|
||||||
|
};
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(homebrewApi);
|
||||||
|
app.use(require('./admin.api.js'));
|
||||||
|
|
||||||
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
|
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||||
|
const welcomeTextV3 = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||||
|
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||||
|
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
|
||||||
|
const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
||||||
|
|
||||||
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
|
//Robots.txt
|
||||||
|
app.get('/robots.txt', (req, res)=>{
|
||||||
|
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||||
|
});
|
||||||
|
|
||||||
|
//Home page
|
||||||
|
app.get('/', async (req, res, next)=>{
|
||||||
|
const brew = {
|
||||||
|
text : welcomeText
|
||||||
|
};
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
//Home page v3
|
||||||
|
app.get('/v3_preview', async (req, res, next)=>{
|
||||||
|
const brew = {
|
||||||
|
text : welcomeTextV3,
|
||||||
|
renderer : 'V3'
|
||||||
|
};
|
||||||
|
splitTextStyleAndMetadata(brew);
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
//Legacy/Other Document -> v3 Migration Guide
|
||||||
|
app.get('/migrate', async (req, res, next)=>{
|
||||||
|
const brew = {
|
||||||
|
text : migrateText,
|
||||||
|
renderer : 'V3'
|
||||||
|
};
|
||||||
|
splitTextStyleAndMetadata(brew);
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
//Changelog page
|
||||||
|
app.get('/changelog', async (req, res, next)=>{
|
||||||
|
const brew = {
|
||||||
|
title : 'Changelog',
|
||||||
|
text : changelogText,
|
||||||
|
renderer : 'V3'
|
||||||
|
};
|
||||||
|
splitTextStyleAndMetadata(brew);
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
//FAQ page
|
||||||
|
app.get('/faq', async (req, res, next)=>{
|
||||||
|
const brew = {
|
||||||
|
title : 'FAQ',
|
||||||
|
text : faqText,
|
||||||
|
renderer : 'V3'
|
||||||
|
};
|
||||||
|
splitTextStyleAndMetadata(brew);
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
//Source page
|
||||||
|
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
||||||
|
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||||
|
|
||||||
|
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||||
|
let text = brew.text;
|
||||||
|
for (const replaceStr in replaceStrings) {
|
||||||
|
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||||
|
}
|
||||||
|
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||||
|
res.status(200).send(text);
|
||||||
|
}));
|
||||||
|
|
||||||
|
//Download brew source page
|
||||||
|
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
||||||
|
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||||
|
const prefix = 'HB - ';
|
||||||
|
|
||||||
|
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"`
|
||||||
|
});
|
||||||
|
res.status(200).send(brew.text);
|
||||||
|
}));
|
||||||
|
|
||||||
|
//User Page
|
||||||
|
app.get('/user/:username', async (req, res, next)=>{
|
||||||
|
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||||
|
|
||||||
|
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(ownAccount && req?.account?.googleId){
|
||||||
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
|
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(googleBrews) {
|
||||||
|
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||||
|
brews = _.concat(brews, googleBrews);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.brews = _.map(brews, (brew)=>{
|
||||||
|
return sanitizeBrew(brew, !ownAccount);
|
||||||
|
});
|
||||||
|
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
|
//Edit Page
|
||||||
|
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
||||||
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||||
|
const brew = await getBrewFromId(req.params.id, 'edit');
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
|
//New Page
|
||||||
|
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
||||||
|
const brew = await getBrewFromId(req.params.id, 'share');
|
||||||
|
brew.title = `CLONE - ${brew.title}`;
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
|
//Share Page
|
||||||
|
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
||||||
|
const brew = await getBrewFromId(req.params.id, 'share');
|
||||||
|
|
||||||
|
if(req.params.id.length > 12) {
|
||||||
|
const googleId = req.params.id.slice(0, -12);
|
||||||
|
const shareId = req.params.id.slice(-12);
|
||||||
|
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||||
|
.catch((err)=>{next(err);});
|
||||||
|
} else {
|
||||||
|
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||||
|
}
|
||||||
|
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
|
//Print Page
|
||||||
|
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
||||||
|
const brew = await getBrewFromId(req.params.id, 'share');
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
|
//Render the page
|
||||||
|
const templateFn = require('./../client/template.js');
|
||||||
|
app.use((req, res)=>{
|
||||||
|
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')
|
||||||
|
};
|
||||||
|
const title = req.brew ? req.brew.title : '';
|
||||||
|
templateFn('homebrew', title, props)
|
||||||
|
.then((page)=>{ res.send(page); })
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log(err);
|
||||||
|
return res.sendStatus(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//v=====----- Error-Handling Middleware -----=====v//
|
||||||
|
//Format Errors so all fields will be sent
|
||||||
|
const replaceErrors = (key, value)=>{
|
||||||
|
if(value instanceof Error) {
|
||||||
|
const error = {};
|
||||||
|
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||||
|
error[key] = value[key];
|
||||||
|
});
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPureError = (error)=>{
|
||||||
|
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||||
|
};
|
||||||
|
|
||||||
|
app.use((err, req, res, next)=>{
|
||||||
|
const status = err.status || 500;
|
||||||
|
console.error(err);
|
||||||
|
res.status(status).send(getPureError(err));
|
||||||
|
});
|
||||||
|
//^=====--------------------------------------=====^//
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
app : app
|
||||||
|
};
|
||||||
5
server/config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = require('nconf')
|
||||||
|
.argv()
|
||||||
|
.env({ lowerCase: true })
|
||||||
|
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||||
|
.file('defaults', { file: 'config/default.json' });
|
||||||
37
server/db.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// The main purpose of this file is to provide an interface for database
|
||||||
|
// connection. Even though the code is quite simple and basically a tiny
|
||||||
|
// wrapper around mongoose package, it works as single point where
|
||||||
|
// database setup/config is performed and the interface provided here can be
|
||||||
|
// reused by both the main application and all tests which require database
|
||||||
|
// connection.
|
||||||
|
|
||||||
|
const Mongoose = require('mongoose');
|
||||||
|
|
||||||
|
const getMongoDBURL = (config)=>{
|
||||||
|
return config.get('mongodb_uri') ||
|
||||||
|
config.get('mongolab_uri') ||
|
||||||
|
'mongodb://localhost/homebrewery';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConnectionError = (error)=>{
|
||||||
|
if(error) {
|
||||||
|
console.error('Could not connect to a Mongo database: \n');
|
||||||
|
console.error(error);
|
||||||
|
console.error('\nIf you are running locally, make sure mongodb.exe is running and DB URL is configured properly');
|
||||||
|
process.exit(1); // non-zero exit code to indicate an error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disconnect = async ()=>{
|
||||||
|
return await Mongoose.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const connect = async (config)=>{
|
||||||
|
return await Mongoose.connect(getMongoDBURL(config),
|
||||||
|
{ retryWrites: false }, handleConnectionError);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
connect : connect,
|
||||||
|
disconnect : disconnect
|
||||||
|
};
|
||||||
@@ -3,21 +3,30 @@ const _ = require('lodash');
|
|||||||
const { google } = require('googleapis');
|
const { google } = require('googleapis');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const token = require('./token.js');
|
const token = require('./token.js');
|
||||||
const config = require('nconf')
|
const config = require('./config.js');
|
||||||
.argv()
|
|
||||||
.env({ lowerCase: true }) // Load environment variables
|
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
|
||||||
.file('defaults', { file: 'config/default.json' });
|
|
||||||
|
|
||||||
//let oAuth2Client;
|
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.');
|
||||||
|
}
|
||||||
|
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
||||||
|
|
||||||
GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
authCheck : (account, res)=>{
|
authCheck : (account, res)=>{
|
||||||
if(!account || !account.googleId){ // If not signed into Google
|
if(!account || !account.googleId){ // If not signed into Google
|
||||||
const err = new Error('Not Signed In');
|
const err = new Error('Not Signed In');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
throw err;
|
throw (err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oAuth2Client = new google.auth.OAuth2(
|
const oAuth2Client = new google.auth.OAuth2(
|
||||||
@@ -47,7 +56,7 @@ GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getGoogleFolder : async (auth)=>{
|
getGoogleFolder : async (auth)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
fileMetadata = {
|
fileMetadata = {
|
||||||
'name' : 'Homebrewery',
|
'name' : 'Homebrewery',
|
||||||
@@ -60,6 +69,7 @@ GoogleActions = {
|
|||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error searching Google Drive Folders');
|
console.log('Error searching Google Drive Folders');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
});
|
});
|
||||||
|
|
||||||
let folderId;
|
let folderId;
|
||||||
@@ -69,8 +79,9 @@ GoogleActions = {
|
|||||||
resource : fileMetadata
|
resource : fileMetadata
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error creating google app folder');
|
console.log('Error creating Google Drive folder');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
});
|
});
|
||||||
|
|
||||||
folderId = obj.data.id;
|
folderId = obj.data.id;
|
||||||
@@ -81,102 +92,81 @@ GoogleActions = {
|
|||||||
return folderId;
|
return folderId;
|
||||||
},
|
},
|
||||||
|
|
||||||
listGoogleBrews : async (req, res)=>{
|
listGoogleBrews : async (auth)=>{
|
||||||
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
|
||||||
|
|
||||||
//TODO: Change to service account to allow non-owners to view published files.
|
|
||||||
// Requires a driveId parameter in the drive.files.list command
|
|
||||||
// const keys = JSON.parse(config.get('service_account'));
|
|
||||||
// const auth = google.auth.fromJSON(keys);
|
|
||||||
// auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
|
||||||
|
|
||||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
|
||||||
|
|
||||||
const obj = await drive.files.list({
|
const obj = await drive.files.list({
|
||||||
pageSize : 100,
|
pageSize : 1000,
|
||||||
fields : 'nextPageToken, files(id, name, description, modifiedTime, properties)',
|
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
return console.error(`Error Listing Google Brews: ${err}`);
|
console.log(`Error Listing Google Brews`);
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
//TODO: Should break out here, but continues on for some reason.
|
//TODO: Should break out here, but continues on for some reason.
|
||||||
});
|
});
|
||||||
|
|
||||||
if(!obj.data.files.length) {
|
if(!obj.data.files.length) {
|
||||||
console.log('No files found.');
|
console.log('No files found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const brews = obj.data.files.map((file)=>{
|
const brews = obj.data.files.map((file)=>{
|
||||||
return {
|
return {
|
||||||
text : '',
|
text : '',
|
||||||
shareId : file.properties.shareId,
|
shareId : file.properties.shareId,
|
||||||
editId : file.properties.editId,
|
editId : file.properties.editId,
|
||||||
createdAt : file.createdTime,
|
createdAt : file.createdTime,
|
||||||
updatedAt : file.modifiedTime,
|
updatedAt : file.modifiedTime,
|
||||||
gDrive : true,
|
gDrive : true,
|
||||||
googleId : file.id,
|
googleId : file.id,
|
||||||
|
pageCount : parseInt(file.properties.pageCount),
|
||||||
title : file.properties.title,
|
title : file.properties.title,
|
||||||
description : file.description,
|
description : file.description,
|
||||||
views : file.properties.views,
|
views : parseInt(file.properties.views),
|
||||||
tags : '',
|
tags : '',
|
||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
authors : [req.account.username], //TODO: properly save and load authors to google drive
|
systems : []
|
||||||
systems : []
|
};
|
||||||
};
|
});
|
||||||
});
|
|
||||||
|
|
||||||
return brews;
|
return brews;
|
||||||
},
|
},
|
||||||
|
|
||||||
existsGoogleBrew : async (auth, id)=>{
|
updateGoogleBrew : async (brew)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
const drive = google.drive({ version: 'v3' });
|
||||||
|
|
||||||
const result = await drive.files.get({ fileId: id })
|
await drive.files.update({
|
||||||
|
fileId : brew.googleId,
|
||||||
|
resource : {
|
||||||
|
name : `${brew.title}.txt`,
|
||||||
|
description : `${brew.description}`,
|
||||||
|
properties : {
|
||||||
|
title : brew.title,
|
||||||
|
published : brew.published,
|
||||||
|
version : brew.version,
|
||||||
|
renderer : brew.renderer,
|
||||||
|
tags : brew.tags,
|
||||||
|
pageCount : brew.pageCount,
|
||||||
|
systems : brew.systems.join()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
media : {
|
||||||
|
mimeType : 'text/plain',
|
||||||
|
body : brew.text
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('error checking file exists...');
|
console.log('Error saving to google');
|
||||||
console.log(err);
|
console.error(err);
|
||||||
return false;
|
throw (err);
|
||||||
|
//return res.status(500).send('Error while saving');
|
||||||
});
|
});
|
||||||
|
|
||||||
if(result){return true;}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
updateGoogleBrew : async (auth, brew)=>{
|
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
|
||||||
|
|
||||||
if(await GoogleActions.existsGoogleBrew(auth, brew.googleId) == true) {
|
|
||||||
await drive.files.update({
|
|
||||||
fileId : brew.googleId,
|
|
||||||
resource : { name : `${brew.title}.txt`,
|
|
||||||
description : `${brew.description}`,
|
|
||||||
properties : { title : brew.title,
|
|
||||||
published : brew.published,
|
|
||||||
lastViewed : brew.lastViewed,
|
|
||||||
views : brew.views,
|
|
||||||
version : brew.version,
|
|
||||||
renderer : brew.renderer,
|
|
||||||
tags : brew.tags,
|
|
||||||
systems : brew.systems.join() }
|
|
||||||
},
|
|
||||||
media : { mimeType : 'text/plain',
|
|
||||||
body : brew.text }
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error saving to google');
|
|
||||||
console.error(err);
|
|
||||||
//return res.status(500).send('Error while saving');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (brew);
|
return (brew);
|
||||||
},
|
},
|
||||||
|
|
||||||
newGoogleBrew : async (auth, brew)=>{
|
newGoogleBrew : async (auth, brew)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const media = {
|
const media = {
|
||||||
mimeType : 'text/plain',
|
mimeType : 'text/plain',
|
||||||
@@ -190,10 +180,12 @@ GoogleActions = {
|
|||||||
'description' : `${brew.description}`,
|
'description' : `${brew.description}`,
|
||||||
'parents' : [folderId],
|
'parents' : [folderId],
|
||||||
'properties' : { //AppProperties is not accessible
|
'properties' : { //AppProperties is not accessible
|
||||||
'shareId' : nanoid(12),
|
'shareId' : brew.shareId || nanoid(12),
|
||||||
'editId' : nanoid(12),
|
'editId' : brew.editId || nanoid(12),
|
||||||
'title' : brew.title,
|
'title' : brew.title,
|
||||||
'views' : '0'
|
'views' : '0',
|
||||||
|
'pageCount' : brew.pageCount,
|
||||||
|
'renderer' : brew.renderer || 'legacy'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,8 +194,9 @@ GoogleActions = {
|
|||||||
media : media
|
media : media
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
|
console.log('Error while creating new Google brew');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return res.status(500).send('Error while creating google brew');
|
throw (err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(!obj) return;
|
if(!obj) return;
|
||||||
@@ -227,6 +220,7 @@ GoogleActions = {
|
|||||||
updatedAt : new Date(),
|
updatedAt : new Date(),
|
||||||
gDrive : true,
|
gDrive : true,
|
||||||
googleId : obj.data.id,
|
googleId : obj.data.id,
|
||||||
|
pageCount : fileMetadata.properties.pageCount,
|
||||||
|
|
||||||
title : brew.title,
|
title : brew.title,
|
||||||
description : brew.description,
|
description : brew.description,
|
||||||
@@ -240,9 +234,8 @@ GoogleActions = {
|
|||||||
return newHomebrew;
|
return newHomebrew;
|
||||||
},
|
},
|
||||||
|
|
||||||
readFileMetadata : async (auth, id, accessId, accessType)=>{
|
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||||
|
const drive = google.drive({ version: 'v3' });
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -261,16 +254,7 @@ GoogleActions = {
|
|||||||
throw ('Share ID does not match');
|
throw ('Share ID does not match');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Access file using service account. Using API key only causes "automated query" lockouts after a while.
|
const serviceDrive = google.drive({ version: 'v3' });
|
||||||
|
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
|
||||||
JSON.parse(config.get('service_account')) :
|
|
||||||
config.get('service_account');
|
|
||||||
|
|
||||||
const serviceAuth = google.auth.fromJSON(keys);
|
|
||||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
|
||||||
|
|
||||||
const serviceDrive = google.drive({ version: 'v3', auth: serviceAuth });
|
|
||||||
|
|
||||||
const file = await serviceDrive.files.get({
|
const file = await serviceDrive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -298,6 +282,7 @@ GoogleActions = {
|
|||||||
createdAt : obj.data.createdTime,
|
createdAt : obj.data.createdTime,
|
||||||
updatedAt : obj.data.modifiedTime,
|
updatedAt : obj.data.modifiedTime,
|
||||||
lastViewed : obj.data.properties.lastViewed,
|
lastViewed : obj.data.properties.lastViewed,
|
||||||
|
pageCount : obj.data.properties.pageCount,
|
||||||
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
||||||
version : parseInt(obj.data.properties.version) || 0,
|
version : parseInt(obj.data.properties.version) || 0,
|
||||||
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
||||||
@@ -310,10 +295,8 @@ GoogleActions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteGoogleBrew : async (req, res, id)=>{
|
deleteGoogleBrew : async (auth, id)=>{
|
||||||
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
|
||||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
|
||||||
|
|
||||||
const googleId = id.slice(0, -12);
|
const googleId = id.slice(0, -12);
|
||||||
const accessId = id.slice(-12);
|
const accessId = id.slice(-12);
|
||||||
@@ -325,7 +308,6 @@ GoogleActions = {
|
|||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error loading from Google');
|
console.log('Error loading from Google');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(obj && obj.data.properties.editId != accessId) {
|
if(obj && obj.data.properties.editId != accessId) {
|
||||||
@@ -340,34 +322,26 @@ GoogleActions = {
|
|||||||
console.log('Can\'t delete Google file');
|
console.log('Can\'t delete Google file');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
increaseView : async (id, accessId, accessType, brew)=>{
|
increaseView : async (id, accessId, accessType, brew)=>{
|
||||||
//service account because this is modifying another user's file properties
|
const drive = google.drive({ version: 'v3' });
|
||||||
//so we need extended scope
|
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
|
||||||
JSON.parse(config.get('service_account')) :
|
|
||||||
config.get('service_account');
|
|
||||||
|
|
||||||
const auth = google.auth.fromJSON(keys);
|
|
||||||
auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
|
||||||
|
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
fileId : brew.googleId,
|
fileId : brew.googleId,
|
||||||
resource : { properties : { views : brew.views + 1,
|
resource : {
|
||||||
lastViewed : new Date() } }
|
modifiedTime : brew.updatedAt,
|
||||||
|
properties : {
|
||||||
|
views : brew.views + 1,
|
||||||
|
lastViewed : new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error updating Google views');
|
console.log('Error updating Google views');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
//return res.status(500).send('Error while saving');
|
//return res.status(500).send('Error while saving');
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const router = require('express').Router();
|
|||||||
const zlib = require('zlib');
|
const zlib = require('zlib');
|
||||||
const GoogleActions = require('./googleActions.js');
|
const GoogleActions = require('./googleActions.js');
|
||||||
const Markdown = require('../shared/naturalcrit/markdown.js');
|
const Markdown = require('../shared/naturalcrit/markdown.js');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
// const getTopBrews = (cb) => {
|
// const getTopBrews = (cb) => {
|
||||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||||
@@ -11,6 +12,22 @@ const Markdown = require('../shared/naturalcrit/markdown.js');
|
|||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
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 MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
const getGoodBrewTitle = (text)=>{
|
const getGoodBrewTitle = (text)=>{
|
||||||
@@ -19,153 +36,200 @@ const getGoodBrewTitle = (text)=>{
|
|||||||
.slice(0, MAX_TITLE_LENGTH);
|
.slice(0, MAX_TITLE_LENGTH);
|
||||||
};
|
};
|
||||||
|
|
||||||
const mergeBrewText = (text, style)=>{
|
const excludePropsFromUpdate = (brew)=>{
|
||||||
if(typeof style !== 'undefined') {
|
// Remove undesired properties
|
||||||
text = `\`\`\`css\n` +
|
const propsToExclude = ['views', 'lastViewed'];
|
||||||
`${style}\n` +
|
for (const prop of propsToExclude) {
|
||||||
`\`\`\`\n\n` +
|
delete brew[prop];
|
||||||
`${text}`;
|
|
||||||
}
|
}
|
||||||
return text;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
const newBrew = (req, res)=>{
|
const beforeNewSave = (account, brew)=>{
|
||||||
const brew = req.body;
|
|
||||||
|
|
||||||
if(!brew.title) {
|
if(!brew.title) {
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
brew.title = getGoodBrewTitle(brew.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
brew.authors = (req.account) ? [req.account.username] : [];
|
brew.authors = (account) ? [account.username] : [];
|
||||||
brew.text = mergeBrewText(brew.text, brew.style);
|
brew.text = mergeBrewText(brew);
|
||||||
|
};
|
||||||
delete brew.editId;
|
|
||||||
delete brew.shareId;
|
|
||||||
delete brew.googleId;
|
|
||||||
|
|
||||||
|
const newLocalBrew = async (brew)=>{
|
||||||
const newHomebrew = new HomebrewModel(brew);
|
const newHomebrew = new HomebrewModel(brew);
|
||||||
// Compress brew text to binary before saving
|
// Compress brew text to binary before saving
|
||||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
newHomebrew.text = undefined;
|
newHomebrew.text = undefined;
|
||||||
|
|
||||||
newHomebrew.save((err, obj)=>{
|
let saved = await newHomebrew.save()
|
||||||
if(err) {
|
|
||||||
console.error(err, err.toString(), err.stack);
|
|
||||||
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
obj = obj.toObject();
|
|
||||||
obj.gDrive = false;
|
|
||||||
return res.status(200).send(obj);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBrew = (req, res)=>{
|
|
||||||
HomebrewModel.get({ editId: req.params.id })
|
|
||||||
.then((brew)=>{
|
|
||||||
brew = _.merge(brew, req.body);
|
|
||||||
brew.text = mergeBrewText(brew.text, brew.style);
|
|
||||||
|
|
||||||
// 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.markModified('authors');
|
|
||||||
brew.markModified('systems');
|
|
||||||
|
|
||||||
brew.save((err, obj)=>{
|
|
||||||
if(err) throw err;
|
|
||||||
return res.status(200).send(obj);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err, err.toString(), err.stack);
|
||||||
return res.status(500).send('Error while saving');
|
throw `Error while creating new brew, ${err.toString()}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
saved = saved.toObject();
|
||||||
|
saved.gDrive = false;
|
||||||
|
return saved;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteBrew = (req, res)=>{
|
const newGoogleBrew = async (account, brew, res)=>{
|
||||||
HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
if(!objs.length || err) {
|
|
||||||
return res.status(404).send('Can not find homebrew with that id');
|
|
||||||
}
|
|
||||||
|
|
||||||
const brew = objs[0];
|
return await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||||
|
|
||||||
if(req.account) {
|
|
||||||
// Remove current user as author
|
|
||||||
brew.authors = _.pull(brew.authors, req.account.username);
|
|
||||||
brew.markModified('authors');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(brew.authors.length === 0) {
|
|
||||||
// Delete brew if there are no authors left
|
|
||||||
brew.remove((err)=>{
|
|
||||||
if(err) return res.status(500).send('Error while removing');
|
|
||||||
return res.status(200).send();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Otherwise, save the brew with updated author list
|
|
||||||
brew.save((err, savedBrew)=>{
|
|
||||||
if(err) throw err;
|
|
||||||
return res.status(200).send(savedBrew);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const newGoogleBrew = async (req, res, next)=>{
|
const newBrew = async (req, res)=>{
|
||||||
let oAuth2Client;
|
|
||||||
|
|
||||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
|
||||||
|
|
||||||
const brew = req.body;
|
const brew = req.body;
|
||||||
|
const { transferToGoogle } = req.query;
|
||||||
if(!brew.title) {
|
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.authors = (req.account) ? [req.account.username] : [];
|
|
||||||
brew.text = mergeBrewText(brew.text, brew.style);
|
|
||||||
|
|
||||||
delete brew.editId;
|
delete brew.editId;
|
||||||
delete brew.shareId;
|
delete brew.shareId;
|
||||||
delete brew.googleId;
|
delete brew.googleId;
|
||||||
|
|
||||||
req.body = brew;
|
beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
let saved;
|
||||||
|
if(transferToGoogle) {
|
||||||
return res.status(200).send(newBrew);
|
saved = await newGoogleBrew(req.account, brew, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
saved = await newLocalBrew(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(!saved) return;
|
||||||
|
return res.status(200).send(saved);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateGoogleBrew = async (req, res, next)=>{
|
const updateBrew = async (req, res)=>{
|
||||||
let oAuth2Client;
|
let brew = excludePropsFromUpdate(req.body);
|
||||||
|
const { transferToGoogle, transferFromGoogle } = req.query;
|
||||||
|
|
||||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
let saved;
|
||||||
|
if(brew.googleId && transferFromGoogle) {
|
||||||
|
beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
const brew = req.body;
|
saved = await newLocalBrew(brew)
|
||||||
brew.text = mergeBrewText(brew.text, brew.style);
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
if(!saved) return;
|
||||||
|
|
||||||
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
|
await deleteGoogleBrew(req.account, `${brew.googleId}${brew.editId}`, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
} else if(!brew.googleId && transferToGoogle) {
|
||||||
|
saved = await newGoogleBrew(req.account, brew, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
if(!saved) return;
|
||||||
|
|
||||||
return res.status(200).send(updatedBrew);
|
await deleteLocalBrew(req.account, brew.editId)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status).send(err.message);
|
||||||
|
});
|
||||||
|
} else if(brew.googleId) {
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
|
saved = await GoogleActions.updateGoogleBrew(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.response?.status || 500).send(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const dbBrew = await HomebrewModel.get({ editId: req.params.id })
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).send('Error while saving');
|
||||||
|
});
|
||||||
|
|
||||||
|
brew = _.merge(dbBrew, brew);
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
|
||||||
|
brew.markModified('authors');
|
||||||
|
brew.markModified('systems');
|
||||||
|
|
||||||
|
saved = await brew.save();
|
||||||
|
}
|
||||||
|
if(!saved) return;
|
||||||
|
|
||||||
|
if(!res.headersSent) return res.status(200).send(saved);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBrew = async (req, res)=>{
|
||||||
|
if(req.params.id.length > 12) {
|
||||||
|
const deleted = await deleteGoogleBrew(req.account, req.params.id, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
if(deleted) return res.status(200).send();
|
||||||
|
} else {
|
||||||
|
const deleted = await deleteLocalBrew(req.account, req.params.id)
|
||||||
|
.catch((err)=>{
|
||||||
|
res.status(err.status).send(err.message);
|
||||||
|
});
|
||||||
|
if(deleted) return res.status(200).send(deleted);
|
||||||
|
return res.status(200).send();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteLocalBrew = async (account, id)=>{
|
||||||
|
const brew = await HomebrewModel.findOne({ editId: id });
|
||||||
|
if(!brew) {
|
||||||
|
throw { status: 404, message: 'Can not find homebrew with that id' };
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// Otherwise, save the brew with updated author list
|
||||||
|
return await brew.save()
|
||||||
|
.catch((err)=>{
|
||||||
|
throw { status: 500, message: err };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteGoogleBrew = async (account, id, res)=>{
|
||||||
|
const auth = await GoogleActions.authCheck(account, res);
|
||||||
|
await GoogleActions.deleteGoogleBrew(auth, id);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', newBrew);
|
router.post('/api', newBrew);
|
||||||
router.post('/api/newGoogle/', newGoogleBrew);
|
|
||||||
router.put('/api/:id', updateBrew);
|
router.put('/api/:id', updateBrew);
|
||||||
router.put('/api/update/:id', updateBrew);
|
router.put('/api/update/:id', updateBrew);
|
||||||
router.put('/api/updateGoogle/:id', updateGoogleBrew);
|
|
||||||
router.delete('/api/:id', deleteBrew);
|
router.delete('/api/:id', deleteBrew);
|
||||||
router.get('/api/remove/:id', deleteBrew);
|
router.get('/api/remove/:id', deleteBrew);
|
||||||
router.get('/api/removeGoogle/:id', (req, res)=>{GoogleActions.deleteGoogleBrew(req, res, req.params.id);});
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ const _ = require('lodash');
|
|||||||
const zlib = require('zlib');
|
const zlib = require('zlib');
|
||||||
|
|
||||||
const HomebrewSchema = mongoose.Schema({
|
const HomebrewSchema = mongoose.Schema({
|
||||||
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||||
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||||
title : { type: String, default: '' },
|
title : { type: String, default: '' },
|
||||||
text : { type: String, default: '' },
|
text : { type: String, default: '' },
|
||||||
textBin : { type: Buffer },
|
textBin : { type: Buffer },
|
||||||
|
pageCount : { type: Number, default: 1 },
|
||||||
|
|
||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : { type: String, default: '' },
|
tags : { type: String, default: '' },
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
const jwt = require('jwt-simple');
|
const jwt = require('jwt-simple');
|
||||||
|
|
||||||
// Load configuration values
|
// Load configuration values
|
||||||
const config = require('nconf')
|
const config = require('./config.js');
|
||||||
.argv()
|
|
||||||
.env({ lowerCase: true }) // Load environment variables
|
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
|
||||||
.file('defaults', { file: 'config/default.json' });
|
|
||||||
|
|
||||||
// Generate an Access Token for the given User ID
|
// Generate an Access Token for the given User ID
|
||||||
const generateAccessToken = (account)=>{
|
const generateAccessToken = (account)=>{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const cx = require('classnames');
|
|||||||
const DISMISS_KEY = 'dismiss_render_warning';
|
const DISMISS_KEY = 'dismiss_render_warning';
|
||||||
|
|
||||||
const RenderWarnings = createClass({
|
const RenderWarnings = createClass({
|
||||||
|
displayName : 'RenderWarnings',
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
warnings : {}
|
warnings : {}
|
||||||
|
|||||||
48
shared/naturalcrit/codeEditor/close-tag.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
const autoCloseCurlyBraces = function(CodeMirror, cm, typingClosingBrace) {
|
||||||
|
const ranges = cm.listSelections(), replacements = [];
|
||||||
|
for (let i = 0; i < ranges.length; i++) {
|
||||||
|
if(!ranges[i].empty()) return CodeMirror.Pass;
|
||||||
|
const pos = ranges[i].head, line = cm.getLine(pos.line), tok = cm.getTokenAt(pos);
|
||||||
|
if(!typingClosingBrace && (tok.type == 'string' || tok.string.charAt(0) != '{' || tok.start != pos.ch - 1))
|
||||||
|
return CodeMirror.Pass;
|
||||||
|
else if(typingClosingBrace) {
|
||||||
|
let hasUnclosedBraces = false, index = -1;
|
||||||
|
do {
|
||||||
|
index = line.indexOf('{{', index + 1);
|
||||||
|
if(index !== -1 && line.indexOf('}}', index + 1) === -1) {
|
||||||
|
hasUnclosedBraces = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (index !== -1);
|
||||||
|
if(!hasUnclosedBraces) return CodeMirror.Pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
replacements[i] = typingClosingBrace ? {
|
||||||
|
text : '}}',
|
||||||
|
newPos : CodeMirror.Pos(pos.line, pos.ch + 2)
|
||||||
|
} : {
|
||||||
|
text : '{}}',
|
||||||
|
newPos : CodeMirror.Pos(pos.line, pos.ch + 1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = ranges.length - 1; i >= 0; i--) {
|
||||||
|
const info = replacements[i];
|
||||||
|
cm.replaceRange(info.text, ranges[i].head, ranges[i].anchor, '+insert');
|
||||||
|
const sel = cm.listSelections().slice(0);
|
||||||
|
sel[i] = {
|
||||||
|
head : info.newPos,
|
||||||
|
anchor : info.newPos
|
||||||
|
};
|
||||||
|
cm.setSelections(sel);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
autoCloseCurlyBraces : function(CodeMirror, codeMirror) {
|
||||||
|
const map = { name: 'autoCloseCurlyBraces' };
|
||||||
|
map[`'{'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm); };
|
||||||
|
map[`'}'`] = function(cm) { return autoCloseCurlyBraces(CodeMirror, cm, true); };
|
||||||
|
codeMirror.addKeyMap(map);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
require('./codeEditor.less');
|
require('./codeEditor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
const closeTag = require('./close-tag');
|
||||||
|
|
||||||
let CodeMirror;
|
let CodeMirror;
|
||||||
if(typeof navigator !== 'undefined'){
|
if(typeof navigator !== 'undefined'){
|
||||||
@@ -13,54 +14,168 @@ if(typeof navigator !== 'undefined'){
|
|||||||
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
||||||
require('codemirror/mode/css/css.js');
|
require('codemirror/mode/css/css.js');
|
||||||
require('codemirror/mode/javascript/javascript.js');
|
require('codemirror/mode/javascript/javascript.js');
|
||||||
|
|
||||||
|
//Addons
|
||||||
|
//Code folding
|
||||||
|
require('codemirror/addon/fold/foldcode.js');
|
||||||
|
require('codemirror/addon/fold/foldgutter.js');
|
||||||
|
//Search and replace
|
||||||
|
require('codemirror/addon/search/search.js');
|
||||||
|
require('codemirror/addon/search/searchcursor.js');
|
||||||
|
require('codemirror/addon/search/jump-to-line.js');
|
||||||
|
require('codemirror/addon/search/match-highlighter.js');
|
||||||
|
require('codemirror/addon/search/matchesonscrollbar.js');
|
||||||
|
require('codemirror/addon/dialog/dialog.js');
|
||||||
|
//Trailing space highlighting
|
||||||
|
// require('codemirror/addon/edit/trailingspace.js');
|
||||||
|
//Active line highlighting
|
||||||
|
// require('codemirror/addon/selection/active-line.js');
|
||||||
|
//Auto-closing
|
||||||
|
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
||||||
|
require('codemirror/addon/fold/xml-fold.js');
|
||||||
|
require('codemirror/addon/edit/closetag.js');
|
||||||
|
|
||||||
|
const foldCode = require('./fold-code');
|
||||||
|
foldCode.registerHomebreweryHelper(CodeMirror);
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeEditor = createClass({
|
const CodeEditor = createClass({
|
||||||
|
displayName : 'CodeEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
language : '',
|
language : '',
|
||||||
value : '',
|
value : '',
|
||||||
wrap : true,
|
wrap : true,
|
||||||
onChange : ()=>{}
|
onChange : ()=>{},
|
||||||
|
enableFolding : true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
docs : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
this.buildEditor();
|
this.buildEditor();
|
||||||
|
const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
|
||||||
|
this.codeMirror.swapDoc(newDoc);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps) {
|
componentDidUpdate : function(prevProps) {
|
||||||
if(prevProps.language !== this.props.language){ //rebuild editor when switching tabs
|
if(prevProps.view !== this.props.view){ //view changed; swap documents
|
||||||
this.buildEditor();
|
let newDoc;
|
||||||
}
|
|
||||||
if(this.codeMirror && this.codeMirror.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
|
if(!this.state.docs[this.props.view]) {
|
||||||
|
newDoc = CodeMirror.Doc(this.props.value, this.props.language);
|
||||||
|
} else {
|
||||||
|
newDoc = this.state.docs[this.props.view];
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) };
|
||||||
|
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
docs : _.merge({}, prevState.docs, oldDoc)
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.props.rerenderParent();
|
||||||
|
} else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
|
||||||
this.codeMirror.setValue(this.props.value);
|
this.codeMirror.setValue(this.props.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.props.enableFolding) {
|
||||||
|
this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror));
|
||||||
|
} else {
|
||||||
|
this.codeMirror.setOption('foldOptions', false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buildEditor : function() {
|
buildEditor : function() {
|
||||||
this.codeMirror = CodeMirror(this.refs.editor, {
|
this.codeMirror = CodeMirror(this.refs.editor, {
|
||||||
value : this.props.value,
|
lineNumbers : true,
|
||||||
lineNumbers : true,
|
lineWrapping : this.props.wrap,
|
||||||
lineWrapping : this.props.wrap,
|
indentWithTabs : true,
|
||||||
mode : this.props.language, //TODO: CSS MODE DOESN'T SEEM TO LOAD PROPERLY
|
tabSize : 2,
|
||||||
indentWithTabs : true,
|
historyEventDelay : 250,
|
||||||
tabSize : 2,
|
extraKeys : {
|
||||||
extraKeys : {
|
'Ctrl-B' : this.makeBold,
|
||||||
'Ctrl-B' : this.makeBold,
|
'Cmd-B' : this.makeBold,
|
||||||
'Cmd-B' : this.makeBold,
|
'Ctrl-I' : this.makeItalic,
|
||||||
'Ctrl-I' : this.makeItalic,
|
'Cmd-I' : this.makeItalic,
|
||||||
'Cmd-I' : this.makeItalic,
|
'Ctrl-U' : this.makeUnderline,
|
||||||
'Ctrl-M' : this.makeSpan,
|
'Cmd-U' : this.makeUnderline,
|
||||||
'Cmd-M' : this.makeSpan,
|
'Ctrl-.' : this.makeNbsp,
|
||||||
}
|
'Cmd-.' : this.makeNbsp,
|
||||||
|
'Shift-Ctrl-.' : this.makeSpace,
|
||||||
|
'Shift-Cmd-.' : this.makeSpace,
|
||||||
|
'Shift-Ctrl-,' : this.removeSpace,
|
||||||
|
'Shift-Cmd-,' : this.removeSpace,
|
||||||
|
'Ctrl-M' : this.makeSpan,
|
||||||
|
'Cmd-M' : this.makeSpan,
|
||||||
|
'Shift-Ctrl-M' : this.makeDiv,
|
||||||
|
'Shift-Cmd-M' : this.makeDiv,
|
||||||
|
'Ctrl-/' : this.makeComment,
|
||||||
|
'Cmd-/' : this.makeComment,
|
||||||
|
'Ctrl-K' : this.makeLink,
|
||||||
|
'Cmd-K' : this.makeLink,
|
||||||
|
'Ctrl-L' : ()=>this.makeList('UL'),
|
||||||
|
'Cmd-L' : ()=>this.makeList('UL'),
|
||||||
|
'Shift-Ctrl-L' : ()=>this.makeList('OL'),
|
||||||
|
'Shift-Cmd-L' : ()=>this.makeList('OL'),
|
||||||
|
'Shift-Ctrl-1' : ()=>this.makeHeader(1),
|
||||||
|
'Shift-Ctrl-2' : ()=>this.makeHeader(2),
|
||||||
|
'Shift-Ctrl-3' : ()=>this.makeHeader(3),
|
||||||
|
'Shift-Ctrl-4' : ()=>this.makeHeader(4),
|
||||||
|
'Shift-Ctrl-5' : ()=>this.makeHeader(5),
|
||||||
|
'Shift-Ctrl-6' : ()=>this.makeHeader(6),
|
||||||
|
'Shift-Cmd-1' : ()=>this.makeHeader(1),
|
||||||
|
'Shift-Cmd-2' : ()=>this.makeHeader(2),
|
||||||
|
'Shift-Cmd-3' : ()=>this.makeHeader(3),
|
||||||
|
'Shift-Cmd-4' : ()=>this.makeHeader(4),
|
||||||
|
'Shift-Cmd-5' : ()=>this.makeHeader(5),
|
||||||
|
'Shift-Cmd-6' : ()=>this.makeHeader(6),
|
||||||
|
'Shift-Ctrl-Enter' : this.newColumn,
|
||||||
|
'Shift-Cmd-Enter' : this.newColumn,
|
||||||
|
'Ctrl-Enter' : this.newPage,
|
||||||
|
'Cmd-Enter' : this.newPage,
|
||||||
|
'Ctrl-F' : 'findPersistent',
|
||||||
|
'Cmd-F' : 'findPersistent',
|
||||||
|
'Shift-Enter' : 'findPersistentPrevious',
|
||||||
|
'Ctrl-[' : this.foldAllCode,
|
||||||
|
'Cmd-[' : this.foldAllCode,
|
||||||
|
'Ctrl-]' : this.unfoldAllCode,
|
||||||
|
'Cmd-]' : this.unfoldAllCode
|
||||||
|
},
|
||||||
|
foldGutter : true,
|
||||||
|
foldOptions : this.foldOptions(this.codeMirror),
|
||||||
|
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||||
|
autoCloseTags : true,
|
||||||
|
styleActiveLine : true,
|
||||||
|
showTrailingSpace : false,
|
||||||
|
// specialChars : / /,
|
||||||
|
// specialCharPlaceholder : function(char) {
|
||||||
|
// const el = document.createElement('span');
|
||||||
|
// el.className = 'cm-space';
|
||||||
|
// el.innerHTML = ' ';
|
||||||
|
// return el;
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
|
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||||
|
|
||||||
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
||||||
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||||
this.updateSize();
|
this.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
makeHeader : function (number) {
|
||||||
|
const selection = this.codeMirror.getSelection();
|
||||||
|
const header = Array(number).fill('#').join('');
|
||||||
|
this.codeMirror.replaceSelection(`${header} ${selection}`, 'around');
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch + selection.length + number + 1 });
|
||||||
|
},
|
||||||
|
|
||||||
makeBold : function() {
|
makeBold : function() {
|
||||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
|
||||||
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
|
||||||
@@ -71,14 +186,55 @@ const CodeEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
makeItalic : function() {
|
makeItalic : function() {
|
||||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '_' && selection.slice(-1) === '_';
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
|
||||||
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `_${selection}_`, 'around');
|
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
|
||||||
if(selection.length === 0){
|
if(selection.length === 0){
|
||||||
const cursor = this.codeMirror.getCursor();
|
const cursor = this.codeMirror.getCursor();
|
||||||
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
makeNbsp : function() {
|
||||||
|
this.codeMirror.replaceSelection(' ', 'end');
|
||||||
|
},
|
||||||
|
|
||||||
|
makeSpace : function() {
|
||||||
|
const selection = this.codeMirror.getSelection();
|
||||||
|
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
|
||||||
|
if(t){
|
||||||
|
const percent = parseInt(selection.slice(8, -4)) + 10;
|
||||||
|
this.codeMirror.replaceSelection(percent < 90 ? `{{width:${percent}% }}` : '{{width:100% }}', 'around');
|
||||||
|
} else {
|
||||||
|
this.codeMirror.replaceSelection(`{{width:10% }}`, 'around');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSpace : function() {
|
||||||
|
const selection = this.codeMirror.getSelection();
|
||||||
|
const t = selection.slice(0, 8) === '{{width:' && selection.slice(0 -4) === '% }}';
|
||||||
|
if(t){
|
||||||
|
const percent = parseInt(selection.slice(8, -4)) - 10;
|
||||||
|
this.codeMirror.replaceSelection(percent > 10 ? `{{width:${percent}% }}` : '', 'around');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
newColumn : function() {
|
||||||
|
this.codeMirror.replaceSelection('\n\\column\n\n', 'end');
|
||||||
|
},
|
||||||
|
|
||||||
|
newPage : function() {
|
||||||
|
this.codeMirror.replaceSelection('\n\\page\n\n', 'end');
|
||||||
|
},
|
||||||
|
|
||||||
|
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');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 4 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
makeSpan : function() {
|
makeSpan : function() {
|
||||||
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||||
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
|
||||||
@@ -88,6 +244,83 @@ const CodeEditor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
makeDiv : function() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{\n${selection}\n}}`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line - 1, ch: cursor.ch }); // set to -2? if wanting to enter classes etc. if so, get rid of first \n when replacing selection
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeComment : function() {
|
||||||
|
let regex;
|
||||||
|
let cursorPos;
|
||||||
|
let newComment;
|
||||||
|
const selection = this.codeMirror.getSelection();
|
||||||
|
if(this.props.language === 'gfm'){
|
||||||
|
regex = /^\s*(<!--\s?)(.*?)(\s?-->)\s*$/gs;
|
||||||
|
cursorPos = 4;
|
||||||
|
newComment = `<!-- ${selection} -->`;
|
||||||
|
} else {
|
||||||
|
regex = /^\s*(\/\*\s?)(.*?)(\s?\*\/)\s*$/gs;
|
||||||
|
cursorPos = 3;
|
||||||
|
newComment = `/* ${selection} */`;
|
||||||
|
}
|
||||||
|
this.codeMirror.replaceSelection(regex.test(selection) == true ? selection.replace(regex, '$2') : newComment, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - cursorPos });
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
makeLink : function() {
|
||||||
|
const isLink = /^\[(.*)\]\((.*)\)$/;
|
||||||
|
const selection = this.codeMirror.getSelection().trim();
|
||||||
|
let match;
|
||||||
|
if(match = isLink.exec(selection)){
|
||||||
|
const altText = match[1];
|
||||||
|
const url = match[2];
|
||||||
|
this.codeMirror.replaceSelection(`${altText} ${url}`);
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - url.length }, { line: cursor.line, ch: cursor.ch });
|
||||||
|
} else {
|
||||||
|
this.codeMirror.replaceSelection(`[${selection || 'alt text'}](url)`);
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setSelection({ line: cursor.line, ch: cursor.ch - 4 }, { line: cursor.line, ch: cursor.ch - 1 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeList : function(listType) {
|
||||||
|
const selectionStart = this.codeMirror.getCursor('from'), selectionEnd = this.codeMirror.getCursor('to');
|
||||||
|
this.codeMirror.setSelection(
|
||||||
|
{ line: selectionStart.line, ch: 0 },
|
||||||
|
{ line: selectionEnd.line, ch: this.codeMirror.getLine(selectionEnd.line).length }
|
||||||
|
);
|
||||||
|
const newSelection = this.codeMirror.getSelection();
|
||||||
|
|
||||||
|
const regex = /^\d+\.\s|^-\s/gm;
|
||||||
|
if(newSelection.match(regex) != null){ // if selection IS A LIST
|
||||||
|
this.codeMirror.replaceSelection(newSelection.replace(regex, ''), 'around');
|
||||||
|
} else { // if selection IS NOT A LIST
|
||||||
|
listType == 'UL' ? this.codeMirror.replaceSelection(newSelection.replace(/^/gm, `- `), 'around') :
|
||||||
|
this.codeMirror.replaceSelection(newSelection.replace(/^/gm, (()=>{
|
||||||
|
let n = 1;
|
||||||
|
return ()=>{
|
||||||
|
return `${n++}. `;
|
||||||
|
};
|
||||||
|
})()), 'around');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
foldAllCode : function() {
|
||||||
|
this.codeMirror.execCommand('foldAll');
|
||||||
|
},
|
||||||
|
|
||||||
|
unfoldAllCode : function() {
|
||||||
|
this.codeMirror.execCommand('unfoldAll');
|
||||||
|
},
|
||||||
|
|
||||||
//=-- Externally used -==//
|
//=-- Externally used -==//
|
||||||
setCursorPosition : function(line, char){
|
setCursorPosition : function(line, char){
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
@@ -101,10 +334,43 @@ const CodeEditor = createClass({
|
|||||||
updateSize : function(){
|
updateSize : function(){
|
||||||
this.codeMirror.refresh();
|
this.codeMirror.refresh();
|
||||||
},
|
},
|
||||||
|
redo : function(){
|
||||||
|
return this.codeMirror.redo();
|
||||||
|
},
|
||||||
|
undo : function(){
|
||||||
|
return this.codeMirror.undo();
|
||||||
|
},
|
||||||
|
historySize : function(){
|
||||||
|
return this.codeMirror.doc.historySize();
|
||||||
|
},
|
||||||
|
|
||||||
|
foldOptions : function(cm){
|
||||||
|
return {
|
||||||
|
scanUp : true,
|
||||||
|
rangeFinder : CodeMirror.fold.homebrewery,
|
||||||
|
widget : (from, to)=>{
|
||||||
|
let text = '';
|
||||||
|
let currentLine = from.line;
|
||||||
|
const maxLength = 50;
|
||||||
|
while (currentLine <= to.line && text.length <= maxLength) {
|
||||||
|
text += this.codeMirror.getLine(currentLine);
|
||||||
|
if(currentLine < to.line)
|
||||||
|
text += ' ';
|
||||||
|
currentLine += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.trim();
|
||||||
|
if(text.length > maxLength)
|
||||||
|
text = `${text.substr(0, maxLength)}...`;
|
||||||
|
|
||||||
|
return `\u21A4 ${text} \u21A6`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
//----------------------//
|
//----------------------//
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='codeEditor' ref='editor' />;
|
return <div className='codeEditor' ref='editor' style={this.props.style}/>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
@import (less) 'codemirror/lib/codemirror.css';
|
@import (less) 'codemirror/lib/codemirror.css';
|
||||||
|
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
||||||
|
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
||||||
|
@import (less) 'codemirror/addon/dialog/dialog.css';
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor{
|
||||||
|
.CodeMirror-foldmarker {
|
||||||
|
font-family: inherit;
|
||||||
|
text-shadow: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
//.cm-tab {
|
||||||
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
|
//}
|
||||||
|
|
||||||
|
//.cm-trailingspace {
|
||||||
|
// .cm-space {
|
||||||
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
26
shared/naturalcrit/codeEditor/fold-code.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
registerHomebreweryHelper : function(CodeMirror) {
|
||||||
|
CodeMirror.registerHelper('fold', 'homebrewery', function(cm, start) {
|
||||||
|
const matcher = /^\\page.*/;
|
||||||
|
const prevLine = cm.getLine(start.line - 1);
|
||||||
|
|
||||||
|
if(start.line === cm.firstLine() || prevLine.match(matcher)) {
|
||||||
|
const lastLineNo = cm.lastLine();
|
||||||
|
let end = start.line;
|
||||||
|
|
||||||
|
while (end < lastLineNo) {
|
||||||
|
if(cm.getLine(end + 1).match(matcher))
|
||||||
|
break;
|
||||||
|
++end;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from : CodeMirror.Pos(start.line, 0),
|
||||||
|
to : CodeMirror.Pos(end, cm.getLine(end).length)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const Markdown = require('marked');
|
const Marked = require('marked');
|
||||||
const renderer = new Markdown.Renderer();
|
const MarkedExtendedTables = require('marked-extended-tables');
|
||||||
|
const renderer = new Marked.Renderer();
|
||||||
|
|
||||||
//Processes the markdown within an HTML block if it's just a class-wrapper
|
//Processes the markdown within an HTML block if it's just a class-wrapper
|
||||||
renderer.html = function (html) {
|
renderer.html = function (html) {
|
||||||
@@ -9,7 +10,7 @@ renderer.html = function (html) {
|
|||||||
const openTag = html.substring(0, html.indexOf('>')+1);
|
const openTag = html.substring(0, html.indexOf('>')+1);
|
||||||
html = html.substring(html.indexOf('>')+1);
|
html = html.substring(html.indexOf('>')+1);
|
||||||
html = html.substring(0, html.lastIndexOf('</div>'));
|
html = html.substring(0, html.lastIndexOf('</div>'));
|
||||||
return `${openTag} ${Markdown(html)} </div>`;
|
return `${openTag} ${Marked.parse(html)} </div>`;
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
};
|
};
|
||||||
@@ -19,7 +20,7 @@ renderer.paragraph = function(text){
|
|||||||
let match;
|
let match;
|
||||||
if(text.startsWith('<div') || text.startsWith('</div'))
|
if(text.startsWith('<div') || text.startsWith('</div'))
|
||||||
return `${text}`;
|
return `${text}`;
|
||||||
else if(match = text.match(/(^|^.*?\n)<span class="inline(.*?<\/span>)$/)) {
|
else if(match = text.match(/(^|^.*?\n)<span class="inline-block(.*?<\/span>)$/)) {
|
||||||
return `${match[1].trim() ? `<p>${match[1]}</p>` : ''}<span class="inline-block${match[2]}`;
|
return `${match[1].trim() ? `<p>${match[1]}</p>` : ''}<span class="inline-block${match[2]}`;
|
||||||
} else
|
} else
|
||||||
return `<p>${text}</p>\n`;
|
return `<p>${text}</p>\n`;
|
||||||
@@ -65,13 +66,13 @@ const mustacheSpans = {
|
|||||||
raw : raw, // Text to consume from the source
|
raw : raw, // Text to consume from the source
|
||||||
text : text, // Additional custom properties
|
text : text, // Additional custom properties
|
||||||
tags : tags,
|
tags : tags,
|
||||||
tokens : this.inlineTokens(text) // inlineTokens to process **bold**, *italics*, etc.
|
tokens : this.lexer.inlineTokens(text) // inlineTokens to process **bold**, *italics*, etc.
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
return `<span class="inline${token.tags}>${this.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
return `<span class="inline-block${token.tags}>${this.parser.parseInline(token.tokens)}</span>`; // parseInline to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,13 +115,13 @@ const mustacheDivs = {
|
|||||||
raw : raw, // Text to consume from the source
|
raw : raw, // Text to consume from the source
|
||||||
text : text, // Additional custom properties
|
text : text, // Additional custom properties
|
||||||
tags : tags,
|
tags : tags,
|
||||||
tokens : this.inline(this.blockTokens(text))
|
tokens : this.lexer.blockTokens(text)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
return `<div class="block${token.tags}>${this.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML
|
return `<div class="block${token.tags}>${this.parser.parse(token.tokens)}</div>`; // parseInline to turn child tokens into HTML
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ const mustacheInjectInline = {
|
|||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
token.type = token.originalType;
|
token.type = token.originalType;
|
||||||
const text = this.parseInline([token]);
|
const text = this.parser.parseInline([token]);
|
||||||
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
||||||
if(openingTag) {
|
if(openingTag) {
|
||||||
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
||||||
@@ -174,15 +175,18 @@ const mustacheInjectBlock = {
|
|||||||
lastToken.originalType = 'mustacheInjectBlock';
|
lastToken.originalType = 'mustacheInjectBlock';
|
||||||
lastToken.tags = ` ${processStyleTags(match[1])}`;
|
lastToken.tags = ` ${processStyleTags(match[1])}`;
|
||||||
return {
|
return {
|
||||||
type : 'text', // Should match "name" above
|
type : 'mustacheInjectBlock', // Should match "name" above
|
||||||
raw : match[0], // Text to consume from the source
|
raw : match[0], // Text to consume from the source
|
||||||
text : ''
|
text : ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
|
if(!token.originalType){
|
||||||
|
return;
|
||||||
|
}
|
||||||
token.type = token.originalType;
|
token.type = token.originalType;
|
||||||
const text = this.parse([token]);
|
const text = this.parser.parse([token]);
|
||||||
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
const openingTag = /(<[^\s<>]+)([^\n<>]*>.*)/s.exec(text);
|
||||||
if(openingTag) {
|
if(openingTag) {
|
||||||
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
return `${openingTag[1]} class="${token.tags}${openingTag[2]}`;
|
||||||
@@ -205,14 +209,14 @@ const definitionLists = {
|
|||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/^.*?::.*/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const regex = /^([^\n]*?)::([^\n]*)/ym;
|
const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym;
|
||||||
let match;
|
let match;
|
||||||
let endIndex = 0;
|
let endIndex = 0;
|
||||||
const definitions = [];
|
const definitions = [];
|
||||||
while (match = regex.exec(src)) {
|
while (match = regex.exec(src)) {
|
||||||
definitions.push({
|
definitions.push({
|
||||||
dt : this.inlineTokens(match[1].trim()),
|
dt : this.lexer.inlineTokens(match[1].trim()),
|
||||||
dd : this.inlineTokens(match[2].trim())
|
dd : this.lexer.inlineTokens(match[2].trim())
|
||||||
});
|
});
|
||||||
endIndex = regex.lastIndex;
|
endIndex = regex.lastIndex;
|
||||||
}
|
}
|
||||||
@@ -225,18 +229,17 @@ const definitionLists = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
renderer(token) {
|
renderer(token) {
|
||||||
return `<dl>
|
return `<dl>${token.definitions.reduce((html, def)=>{
|
||||||
${token.definitions.reduce((html, def)=>{
|
return `${html}<dt>${this.parser.parseInline(def.dt)}</dt>`
|
||||||
return `${html}<dt>${this.parseInline(def.dt)}</dt>`
|
+ `<dd>${this.parser.parseInline(def.dd)}</dd>\n`;
|
||||||
+ `<dd>${this.parseInline(def.dd)}</dd>\n`;
|
}, '')}</dl>`;
|
||||||
}, '')}
|
|
||||||
</dl>`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Markdown.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
|
Marked.use({ extensions: [mustacheSpans, mustacheDivs, mustacheInjectInline, definitionLists] });
|
||||||
Markdown.use(mustacheInjectBlock);
|
Marked.use(MarkedExtendedTables());
|
||||||
Markdown.use({ smartypants: true });
|
Marked.use(mustacheInjectBlock);
|
||||||
|
Marked.use({ smartypants: true });
|
||||||
|
|
||||||
//Fix local links in the Preview iFrame to link inside the frame
|
//Fix local links in the Preview iFrame to link inside the frame
|
||||||
renderer.link = function (href, title, text) {
|
renderer.link = function (href, title, text) {
|
||||||
@@ -317,9 +320,15 @@ const sanatizeScriptTags = (content)=>{
|
|||||||
const tagTypes = ['div', 'span', 'a'];
|
const tagTypes = ['div', 'span', 'a'];
|
||||||
const tagRegex = new RegExp(`(${
|
const tagRegex = new RegExp(`(${
|
||||||
_.map(tagTypes, (type)=>{
|
_.map(tagTypes, (type)=>{
|
||||||
return `\\<${type}|\\</${type}>`;
|
return `\\<${type}\\b|\\</${type}>`;
|
||||||
}).join('|')})`, 'g');
|
}).join('|')})`, 'g');
|
||||||
|
|
||||||
|
// Special "void" tags that can be self-closed but don't need to be.
|
||||||
|
const voidTags = new Set([
|
||||||
|
'area', 'base', 'br', 'col', 'command', 'hr', 'img',
|
||||||
|
'input', 'keygen', 'link', 'meta', 'param', 'source'
|
||||||
|
]);
|
||||||
|
|
||||||
const processStyleTags = (string)=>{
|
const processStyleTags = (string)=>{
|
||||||
//split tags up. quotes can only occur right after colons.
|
//split tags up. quotes can only occur right after colons.
|
||||||
//TODO: can we simplify to just split on commas?
|
//TODO: can we simplify to just split on commas?
|
||||||
@@ -334,11 +343,11 @@ const processStyleTags = (string)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
marked : Markdown,
|
marked : Marked,
|
||||||
render : (rawBrewText)=>{
|
render : (rawBrewText)=>{
|
||||||
rawBrewText = rawBrewText.replace(/^\\column$/gm, `<div class='columnSplit'></div>`)
|
rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n<div class='columnSplit'></div>\n`)
|
||||||
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
.replace(/^(:+)$/gm, (match)=>`${`<div class='blank'></div>`.repeat(match.length)}\n`);
|
||||||
return Markdown(
|
return Marked.parse(
|
||||||
sanatizeScriptTags(rawBrewText),
|
sanatizeScriptTags(rawBrewText),
|
||||||
{ renderer: renderer }
|
{ renderer: renderer }
|
||||||
);
|
);
|
||||||
@@ -360,6 +369,13 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(match === `</${type}>`){
|
if(match === `</${type}>`){
|
||||||
|
// Closing tag: Check we expect it to be closed.
|
||||||
|
// The accumulator may contain a sequence of voidable opening tags,
|
||||||
|
// over which we skip before checking validity of the close.
|
||||||
|
while (acc.length && voidTags.has(_.last(acc).type) && _.last(acc).type != type) {
|
||||||
|
acc.pop();
|
||||||
|
}
|
||||||
|
// Now check that what remains in the accumulator is valid.
|
||||||
if(!acc.length){
|
if(!acc.length){
|
||||||
errors.push({
|
errors.push({
|
||||||
line : lineNumber,
|
line : lineNumber,
|
||||||
|
|||||||
@@ -99,9 +99,15 @@ const sanatizeScriptTags = (content)=>{
|
|||||||
const tagTypes = ['div', 'span', 'a'];
|
const tagTypes = ['div', 'span', 'a'];
|
||||||
const tagRegex = new RegExp(`(${
|
const tagRegex = new RegExp(`(${
|
||||||
_.map(tagTypes, (type)=>{
|
_.map(tagTypes, (type)=>{
|
||||||
return `\\<${type}|\\</${type}>`;
|
return `\\<${type}\\b|\\</${type}>`;
|
||||||
}).join('|')})`, 'g');
|
}).join('|')})`, 'g');
|
||||||
|
|
||||||
|
// Special "void" tags that can be self-closed but don't need to be.
|
||||||
|
const voidTags = new Set([
|
||||||
|
'area', 'base', 'br', 'col', 'command', 'hr', 'img',
|
||||||
|
'input', 'keygen', 'link', 'meta', 'param', 'source'
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
marked : Markdown,
|
marked : Markdown,
|
||||||
@@ -128,6 +134,13 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(match === `</${type}>`){
|
if(match === `</${type}>`){
|
||||||
|
// Closing tag: Check we expect it to be closed.
|
||||||
|
// The accumulator may contain a sequence of voidable opening tags,
|
||||||
|
// over which we skip before checking validity of the close.
|
||||||
|
while (acc.length && voidTags.has(_.last(acc).type) && _.last(acc).type != type) {
|
||||||
|
acc.pop();
|
||||||
|
}
|
||||||
|
// Now check that what remains in the accumulator is valid.
|
||||||
if(!acc.length){
|
if(!acc.length){
|
||||||
errors.push({
|
errors.push({
|
||||||
line : lineNumber,
|
line : lineNumber,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
|||||||
|
|
||||||
const Nav = {
|
const Nav = {
|
||||||
base : createClass({
|
base : createClass({
|
||||||
render : function(){
|
displayName : 'Nav.base',
|
||||||
|
render : function(){
|
||||||
return <nav>
|
return <nav>
|
||||||
<div className='navContent'>
|
<div className='navContent'>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
@@ -26,7 +27,8 @@ const Nav = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
section : createClass({
|
section : createClass({
|
||||||
render : function(){
|
displayName : 'Nav.section',
|
||||||
|
render : function(){
|
||||||
return <div className='navSection'>
|
return <div className='navSection'>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>;
|
</div>;
|
||||||
@@ -34,6 +36,7 @@ const Nav = {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
item : createClass({
|
item : createClass({
|
||||||
|
displayName : 'Nav.item',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
icon : null,
|
icon : null,
|
||||||
@@ -68,6 +71,47 @@ const Nav = {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
dropdown : createClass({
|
||||||
|
displayName : 'Nav.dropdown',
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
showDropdown : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDropdown : function(show){
|
||||||
|
this.setState({
|
||||||
|
showDropdown : show
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDropdown : function(dropdownChildren){
|
||||||
|
if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='navDropdown'>
|
||||||
|
{dropdownChildren}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function () {
|
||||||
|
const dropdownChildren = React.Children.map(this.props.children, (child, i)=>{
|
||||||
|
// Ignore the first child
|
||||||
|
if(i < 1) return;
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className='navDropdownContainer'
|
||||||
|
onMouseEnter={()=>this.handleDropdown(true)}
|
||||||
|
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||||
|
{this.props.children[0]}
|
||||||
|
{this.renderDropdown(dropdownChildren)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
@import '../styles/colors';
|
||||||
|
@keyframes glideDropDown {
|
||||||
|
0% {transform : translate(0px, -100%);
|
||||||
|
opacity : 0;
|
||||||
|
background-color: #333;}
|
||||||
|
100% {transform : translate(0px, 0px);
|
||||||
|
opacity : 1;
|
||||||
|
background-color: #333;}
|
||||||
|
}
|
||||||
nav{
|
nav{
|
||||||
background-color : #333;
|
background-color : #333;
|
||||||
.navContent{
|
.navContent{
|
||||||
@@ -41,6 +50,7 @@ nav{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navItem{
|
.navItem{
|
||||||
|
#backgroundColors;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
padding : 8px 12px;
|
padding : 8px 12px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
@@ -50,32 +60,35 @@ nav{
|
|||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
|
line-height : 13px;
|
||||||
i{
|
i{
|
||||||
margin-left : 5px;
|
margin-left : 5px;
|
||||||
font-size : 13px;
|
font-size : 13px;
|
||||||
|
float : right;
|
||||||
}
|
}
|
||||||
&.tealLight:hover{ background-color : @tealLight };
|
|
||||||
&.teal:hover{ background-color : @teal };
|
|
||||||
&.greenLight:hover{ background-color : @greenLight };
|
|
||||||
&.green:hover{ background-color : @green };
|
|
||||||
&.blueLight:hover{ background-color : @blueLight };
|
|
||||||
&.blue:hover{ background-color : @blue };
|
|
||||||
&.purpleLight:hover{ background-color : @purpleLight };
|
|
||||||
&.purple:hover{ background-color : @purple };
|
|
||||||
&.steelLight:hover{ background-color : @steelLight };
|
|
||||||
&.steel:hover{ background-color : @steel };
|
|
||||||
&.yellowLight:hover{ background-color : @yellowLight };
|
|
||||||
&.yellow:hover{ background-color : @yellow };
|
|
||||||
&.orangeLight:hover{ background-color : @orangeLight };
|
|
||||||
&.orange:hover{ background-color : @orange };
|
|
||||||
&.redLight:hover{ background-color : @redLight };
|
|
||||||
&.red:hover{ background-color : @red };
|
|
||||||
&.silverLight:hover{ background-color : @silverLight };
|
|
||||||
&.silver:hover{ background-color : @silver };
|
|
||||||
&.greyLight:hover{ background-color : @greyLight };
|
|
||||||
&.grey:hover{ background-color : @grey };
|
|
||||||
}
|
}
|
||||||
.navSection:last-child .navItem{
|
.navSection:last-child .navItem{
|
||||||
border-left : 1px solid #666;
|
border-left : 1px solid #666;
|
||||||
}
|
}
|
||||||
|
.navDropdownContainer{
|
||||||
|
position: relative;
|
||||||
|
.navDropdown {
|
||||||
|
position : absolute;
|
||||||
|
top : 28px;
|
||||||
|
left : 0px;
|
||||||
|
z-index : 10000;
|
||||||
|
width : 100%;
|
||||||
|
.navItem{
|
||||||
|
animation-name: glideDropDown;
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
position : relative;
|
||||||
|
display : block;
|
||||||
|
width : 100%;
|
||||||
|
vertical-align : middle;
|
||||||
|
padding : 8px 5px;
|
||||||
|
border : 1px solid #888;
|
||||||
|
border-bottom : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,43 +5,74 @@ const _ = require('lodash');
|
|||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
const SplitPane = createClass({
|
const SplitPane = createClass({
|
||||||
|
displayName : 'SplitPane',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
storageKey : 'naturalcrit-pane-split',
|
storageKey : 'naturalcrit-pane-split',
|
||||||
onDragFinish : function(){} //fires when dragging
|
onDragFinish : function(){} //fires when dragging
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
size : null,
|
currentDividerPos : null,
|
||||||
isDragging : false
|
windowWidth : 0,
|
||||||
|
isDragging : false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
const paneSize = window.localStorage.getItem(this.props.storageKey);
|
const dividerPos = window.localStorage.getItem(this.props.storageKey);
|
||||||
if(paneSize){
|
if(dividerPos){
|
||||||
this.setState({
|
this.setState({
|
||||||
size : paneSize
|
currentDividerPos : this.limitPosition(dividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13)),
|
||||||
|
userSetDividerPos : dividerPos,
|
||||||
|
windowWidth : window.innerWidth
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
window.addEventListener('resize', this.handleWindowResize);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount : function() {
|
||||||
|
window.removeEventListener('resize', this.handleWindowResize);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleWindowResize : function() {
|
||||||
|
// Allow divider to increase in size to last user-set position
|
||||||
|
// Limit current position to between 10% and 90% of visible space
|
||||||
|
const newLoc = this.limitPosition(this.state.userSetDividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
currentDividerPos : newLoc,
|
||||||
|
windowWidth : window.innerWidth
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
limitPosition : function(x, min = 1, max = window.innerWidth - 13) {
|
||||||
|
const result = Math.round(Math.min(max, Math.max(min, x)));
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUp : function(){
|
handleUp : function(){
|
||||||
if(this.state.isDragging){
|
if(this.state.isDragging){
|
||||||
this.props.onDragFinish(this.state.size);
|
this.props.onDragFinish(this.state.currentDividerPos);
|
||||||
window.localStorage.setItem(this.props.storageKey, this.state.size);
|
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||||
}
|
}
|
||||||
this.setState({ isDragging: false });
|
this.setState({ isDragging: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDown : function(){
|
handleDown : function(){
|
||||||
this.setState({ isDragging: true });
|
this.setState({ isDragging: true });
|
||||||
//this.unFocus()
|
//this.unFocus()
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMove : function(e){
|
handleMove : function(e){
|
||||||
if(!this.state.isDragging) return;
|
if(!this.state.isDragging) return;
|
||||||
|
|
||||||
|
const newSize = this.limitPosition(e.pageX);
|
||||||
this.setState({
|
this.setState({
|
||||||
size : e.pageX
|
currentDividerPos : newSize,
|
||||||
|
userSetDividerPos : newSize
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
@@ -65,7 +96,7 @@ const SplitPane = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
||||||
<Pane ref='pane1' width={this.state.size}>{this.props.children[0]}</Pane>
|
<Pane ref='pane1' width={this.state.currentDividerPos}>{this.props.children[0]}</Pane>
|
||||||
{this.renderDivider()}
|
{this.renderDivider()}
|
||||||
<Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
|
<Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
|
||||||
</div>;
|
</div>;
|
||||||
@@ -73,6 +104,7 @@ const SplitPane = createClass({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const Pane = createClass({
|
const Pane = createClass({
|
||||||
|
displayName : 'Pane',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
width : null
|
width : null
|
||||||
|
|||||||
@@ -28,5 +28,8 @@
|
|||||||
color : #666;
|
color : #666;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:hover{
|
||||||
|
background-color: #999;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,27 @@
|
|||||||
@silverLight : #ECF0F1;
|
@silverLight : #ECF0F1;
|
||||||
@silver : #BDC3C7;
|
@silver : #BDC3C7;
|
||||||
@greyLight : #95A5A6;
|
@greyLight : #95A5A6;
|
||||||
@grey : #7F8C8D;
|
@grey : #7F8C8D;
|
||||||
|
|
||||||
|
#backgroundColors {
|
||||||
|
&.tealLight:hover{ background-color : @tealLight };
|
||||||
|
&.teal:hover{ background-color : @teal };
|
||||||
|
&.greenLight:hover{ background-color : @greenLight };
|
||||||
|
&.green:hover{ background-color : @green };
|
||||||
|
&.blueLight:hover{ background-color : @blueLight };
|
||||||
|
&.blue:hover{ background-color : @blue };
|
||||||
|
&.purpleLight:hover{ background-color : @purpleLight };
|
||||||
|
&.purple:hover{ background-color : @purple };
|
||||||
|
&.steelLight:hover{ background-color : @steelLight };
|
||||||
|
&.steel:hover{ background-color : @steel };
|
||||||
|
&.yellowLight:hover{ background-color : @yellowLight };
|
||||||
|
&.yellow:hover{ background-color : @yellow };
|
||||||
|
&.orangeLight:hover{ background-color : @orangeLight };
|
||||||
|
&.orange:hover{ background-color : @orange };
|
||||||
|
&.redLight:hover{ background-color : @redLight };
|
||||||
|
&.red:hover{ background-color : @red };
|
||||||
|
&.silverLight:hover{ background-color : @silverLight };
|
||||||
|
&.silver:hover{ background-color : @silver };
|
||||||
|
&.greyLight:hover{ background-color : @greyLight };
|
||||||
|
&.grey:hover{ background-color : @grey };
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
const test = require('pico-check');
|
|
||||||
|
|
||||||
test('Just setting up a spot for future tests', (t)=>{
|
|
||||||
t.pass();
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = test;
|
|
||||||
15
tests/markdown/basic.test.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
test('Escapes <script> tag', function() {
|
||||||
|
const source = '<script></script>';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toMatch('<script></script>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||||
|
const source = '<div>*Bold text*</div>';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<div> <p><em>Bold text</em></p>\n </div>');
|
||||||
|
});
|
||||||
128
tests/markdown/mustache-span.test.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
test('Renders a mustache span with text only', function() {
|
||||||
|
const source = '{{ text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block ">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text only, but with spaces', function() {
|
||||||
|
const source = '{{ this is a text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block ">this is a text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders an empty mustache span', function() {
|
||||||
|
const source = '{{}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with just a space', function() {
|
||||||
|
const source = '{{ }}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with a few spaces only', function() {
|
||||||
|
const source = '{{ }}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and class', function() {
|
||||||
|
const source = '{{my-class text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
// FIXME: why do we have those two extra spaces after closing "?
|
||||||
|
expect(rendered).toBe('<span class="inline-block my-class" >text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and two classes', function() {
|
||||||
|
const source = '{{my-class,my-class2 text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
// FIXME: why do we have those two extra spaces after closing "?
|
||||||
|
expect(rendered).toBe('<span class="inline-block my-class my-class2" >text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text with spaces and class', function() {
|
||||||
|
const source = '{{my-class this is a text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
// FIXME: why do we have those two extra spaces after closing "?
|
||||||
|
expect(rendered).toBe('<span class="inline-block my-class" >this is a text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and id', function() {
|
||||||
|
const source = '{{#my-span text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
// FIXME: why do we have that one extra space after closing "?
|
||||||
|
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and two ids', function() {
|
||||||
|
const source = '{{#my-span,#my-favorite-span text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
// FIXME: do we need to report an error here somehow?
|
||||||
|
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and css property', function() {
|
||||||
|
const source = '{{color:red text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block " style="color:red;">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and two css properties', function() {
|
||||||
|
const source = '{{color:red,padding:5px text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block " style="color:red; padding:5px;">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and css property which contains quotes', function() {
|
||||||
|
const source = '{{font:"trebuchet ms" text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
// FIXME: is it correct to remove quotes surrounding css property value?
|
||||||
|
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text and two css properties which contains quotes', function() {
|
||||||
|
const source = '{{font:"trebuchet ms",padding:"5px 10px" text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms; padding:5px 10px;">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
||||||
|
const source = '{{font:"trebuchet ms" text "with quotes"}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text “with quotes”</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders a mustache span with text, id, class and a couple of css properties', function() {
|
||||||
|
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
|
||||||
|
const rendered = Markdown.render(source);
|
||||||
|
expect(rendered).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: add tests for ID with accordance to CSS spec:
|
||||||
|
//
|
||||||
|
// From https://drafts.csswg.org/selectors/#id-selectors:
|
||||||
|
//
|
||||||
|
// > An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier.
|
||||||
|
//
|
||||||
|
// From: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier:
|
||||||
|
//
|
||||||
|
// > In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9]
|
||||||
|
// > and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_);
|
||||||
|
// > they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
|
||||||
|
// > Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item).
|
||||||
|
// > For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
|
||||||
|
// > Note that Unicode is code-by-code equivalent to ISO 10646 (see [UNICODE] and [ISO10646]).
|
||||||
|
|
||||||
|
// TODO: add tests for class with accordance to CSS spec:
|
||||||
|
//
|
||||||
|
// From: https://drafts.csswg.org/selectors/#class-html:
|
||||||
|
//
|
||||||
|
// > The class selector is given as a full stop (. U+002E) immediately followed by an identifier.
|
||||||
|
|
||||||
27
tests/routes/static-pages.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
const supertest = require('supertest');
|
||||||
|
|
||||||
|
// Mimic https responses to avoid being redirected all the time
|
||||||
|
const app = supertest.agent(require('app.js').app)
|
||||||
|
.set('X-Forwarded-Proto', 'https');
|
||||||
|
|
||||||
|
describe('Tests for static pages', ()=>{
|
||||||
|
it('Home page works', ()=>{
|
||||||
|
return app.get('/').expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Home page v3 works', ()=>{
|
||||||
|
return app.get('/v3_preview').expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Changelog page works', ()=>{
|
||||||
|
return app.get('/changelog').expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('FAQ page works', ()=>{
|
||||||
|
return app.get('/faq').expect(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('robots.txt works', ()=>{
|
||||||
|
return app.get('/robots.txt').expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1 +0,0 @@
|
|||||||
//Set up configs and DB connectiosna nd what not in here
|
|
||||||
@@ -2,12 +2,14 @@
|
|||||||
@import (less) './themes/assets/assets.less';
|
@import (less) './themes/assets/assets.less';
|
||||||
|
|
||||||
//Colors
|
//Colors
|
||||||
@background : #EEE5CE;
|
@background : #EEE5CE; // Light parchment
|
||||||
@noteGreen : #e0e5c1;
|
@noteGreen : #e0e5c1; // Pastel green
|
||||||
@headerUnderline : #c9ad6a;
|
@headerUnderline : #c9ad6a; // Gold
|
||||||
@horizontalRule : #9c2b1b;
|
@horizontalRule : #9c2b1b; // Maroon
|
||||||
@headerText : #58180D;
|
@headerText : #58180D; // Dark maroon
|
||||||
@monsterStatBackground : #EEDBAB;
|
@monsterStatBackground : #EEDBAB; // Light orange parchment
|
||||||
|
@captionText : #766649; // Brown
|
||||||
|
@watercolorStain : #BBAD82; // Light brown
|
||||||
@page { margin: 0; }
|
@page { margin: 0; }
|
||||||
body {
|
body {
|
||||||
counter-reset : phb-page-numbers;
|
counter-reset : phb-page-numbers;
|
||||||
@@ -17,7 +19,7 @@ body {
|
|||||||
}
|
}
|
||||||
.useSansSerif(){
|
.useSansSerif(){
|
||||||
font-family : ScalySansRemake;
|
font-family : ScalySansRemake;
|
||||||
font-size : 0.325cm;
|
font-size : 0.318cm;
|
||||||
line-height : 1.2em;
|
line-height : 1.2em;
|
||||||
p,dl,ul,ol {
|
p,dl,ul,ol {
|
||||||
line-height : 1.2em;
|
line-height : 1.2em;
|
||||||
@@ -32,10 +34,13 @@ body {
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
letter-spacing : -0.02em;
|
letter-spacing : -0.02em;
|
||||||
}
|
}
|
||||||
|
h5 + * {
|
||||||
|
margin-top : 0.1cm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.useColumns(@multiplier : 1){
|
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||||
column-count : 2;
|
column-count : 2;
|
||||||
column-fill : auto;
|
column-fill : @fillMode;
|
||||||
column-gap : 0.9cm;
|
column-gap : 0.9cm;
|
||||||
column-width : 8cm * @multiplier;
|
column-width : 8cm * @multiplier;
|
||||||
-webkit-column-count : 2;
|
-webkit-column-count : 2;
|
||||||
@@ -45,6 +50,12 @@ body {
|
|||||||
-webkit-column-gap : 0.9cm;
|
-webkit-column-gap : 0.9cm;
|
||||||
-moz-column-gap : 0.9cm;
|
-moz-column-gap : 0.9cm;
|
||||||
}
|
}
|
||||||
|
.columnWrapper{
|
||||||
|
max-height : 100%;
|
||||||
|
column-span : all;
|
||||||
|
columns : inherit;
|
||||||
|
column-gap : inherit;
|
||||||
|
}
|
||||||
.page{
|
.page{
|
||||||
.useColumns();
|
.useColumns();
|
||||||
counter-increment : phb-page-numbers;
|
counter-increment : phb-page-numbers;
|
||||||
@@ -54,9 +65,9 @@ body {
|
|||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
height : 279.4mm;
|
height : 279.4mm;
|
||||||
width : 215.9mm;
|
width : 215.9mm;
|
||||||
padding : 1.4cm 1.9cm 1.7cm;
|
|
||||||
background-color : @background;
|
background-color : @background;
|
||||||
background-image : @backgroundImage;
|
background-image : @backgroundImage;
|
||||||
|
padding : 1.4cm 1.9cm 1.7cm;
|
||||||
font-family : BookInsanityRemake;
|
font-family : BookInsanityRemake;
|
||||||
font-size : 0.34cm;
|
font-size : 0.34cm;
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
@@ -67,23 +78,26 @@ body {
|
|||||||
// *****************************/
|
// *****************************/
|
||||||
p{
|
p{
|
||||||
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
|
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
|
||||||
margin-bottom : 0.8em;
|
display : block;
|
||||||
line-height : 1.3em;
|
line-height : 1.25em;
|
||||||
|
&+* {
|
||||||
|
margin-top : 0.325cm;
|
||||||
|
}
|
||||||
&+p{
|
&+p{
|
||||||
margin-top : -0.8em;
|
margin-top : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul{
|
ul{
|
||||||
margin-bottom : 0.8em;
|
margin-bottom : 0.8em;
|
||||||
padding-left : 1.4em;
|
padding-left : 1.4em;
|
||||||
line-height : 1.3em;
|
line-height : 1.25em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
}
|
}
|
||||||
ol{
|
ol{
|
||||||
margin-bottom : 0.8em;
|
margin-bottom : 0.8em;
|
||||||
padding-left : 1.4em;
|
padding-left : 1.4em;
|
||||||
line-height : 1.3em;
|
line-height : 1.25em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : decimal;
|
list-style-type : decimal;
|
||||||
}
|
}
|
||||||
@@ -120,53 +134,59 @@ body {
|
|||||||
color : @headerText;
|
color : @headerText;
|
||||||
}
|
}
|
||||||
h1{
|
h1{
|
||||||
margin-bottom : 0.18cm;
|
margin-bottom : 0.18cm; //Margin-bottom only because this is WIDE
|
||||||
column-span : all;
|
column-span : all;
|
||||||
font-size : 0.89cm;
|
font-size : 0.89cm;
|
||||||
-webkit-column-span : all;
|
-webkit-column-span : all;
|
||||||
-moz-column-span : all;
|
-moz-column-span : all;
|
||||||
&+p::first-letter{
|
&+p::first-letter{
|
||||||
float : left;
|
float : left;
|
||||||
font-family : SolberaImitationRemake;
|
font-family : SolberaImitationRemake;
|
||||||
line-height : 0.8em;
|
line-height : 1em;
|
||||||
font-size: 3.5cm;
|
font-size : 3.5cm;
|
||||||
padding-left: 40px;
|
padding-left : 40px; //Allow background color to extend into margins
|
||||||
margin-left: -40px;
|
margin-left : -40px;
|
||||||
padding-top:10px;
|
margin-top : -0.3cm;
|
||||||
margin-top:-8px;
|
padding-bottom : 2px;
|
||||||
padding-bottom:10px;
|
margin-bottom : -20px;
|
||||||
margin-bottom:-20px;
|
background-image : linear-gradient(-45deg, #322814, #998250, #322814);
|
||||||
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
background-clip : text;
|
||||||
background-clip: text;
|
-webkit-background-clip : text;
|
||||||
-webkit-background-clip: text;
|
color : rgba(0, 0, 0, 0);
|
||||||
color: rgba(0, 0, 0, 0);
|
|
||||||
}
|
}
|
||||||
&+p::first-line{
|
&+p::first-line{
|
||||||
font-variant : small-caps;
|
font-variant : small-caps;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h2{
|
h2{
|
||||||
margin-top : 0px;
|
//margin-top : 0px; //Font is misaligned. Shift up slightly
|
||||||
margin-bottom : 0.05cm;
|
//margin-bottom : 0.05cm;
|
||||||
font-size : 0.75cm;
|
font-size : 0.75cm;
|
||||||
|
line-height : 0.988em; //Font is misaligned. Shift up slightly
|
||||||
}
|
}
|
||||||
h3{
|
h3{
|
||||||
margin-top : -0.1cm;
|
//margin-top : -0.1cm; //Font is misaligned. Shift up slightly
|
||||||
margin-bottom : 0.1cm;
|
//margin-bottom : 0.1cm;
|
||||||
font-size : 0.575cm;
|
font-size : 0.575cm;
|
||||||
border-bottom : 2px solid @headerUnderline;
|
border-bottom : 2px solid @headerUnderline;
|
||||||
|
line-height : 0.995em; //Font is misaligned. Shift up slightly
|
||||||
}
|
}
|
||||||
h4{
|
h4{
|
||||||
margin-top : -0.02cm;
|
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||||
margin-bottom : 0.02cm;
|
//margin-bottom : 0.02cm;
|
||||||
font-size : 0.458cm;
|
font-size : 0.458cm;
|
||||||
|
line-height : 0.971em; //Font is misaligned. Shift up slightly
|
||||||
}
|
}
|
||||||
h5{
|
h5{
|
||||||
margin-top : -0.02cm;
|
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||||
margin-bottom : 0.02cm;
|
//margin-bottom : 0.02cm;
|
||||||
font-family : ScalySansSmallCapsRemake;
|
font-family : ScalySansSmallCapsRemake;
|
||||||
font-size : 0.423cm;
|
font-size : 0.423cm;
|
||||||
font-weight : 900;
|
font-weight : 900;
|
||||||
|
line-height : 0.951em; //Font is misaligned. Shift up slightly
|
||||||
|
& + * {
|
||||||
|
margin-top : 0.2cm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * TABLE
|
// * TABLE
|
||||||
@@ -174,7 +194,9 @@ body {
|
|||||||
table{
|
table{
|
||||||
.useSansSerif();
|
.useSansSerif();
|
||||||
width : 100%;
|
width : 100%;
|
||||||
margin-bottom : 1em;
|
& + * {
|
||||||
|
margin-top : 0.325cm;
|
||||||
|
}
|
||||||
thead{
|
thead{
|
||||||
display: table-row-group;
|
display: table-row-group;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
@@ -198,39 +220,30 @@ body {
|
|||||||
// * NOTE
|
// * NOTE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.note{
|
.note{
|
||||||
&::before{
|
|
||||||
content : "";
|
|
||||||
box-sizing : border-box;
|
|
||||||
border-style : solid;
|
|
||||||
border-width : 11px;
|
|
||||||
border-image : @noteBorderImage 12;
|
|
||||||
border-image-outset : 9px 0px;
|
|
||||||
box-shadow : 1px 4px 14px #888;
|
|
||||||
position : absolute;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
top : 0;
|
|
||||||
left : 0;
|
|
||||||
}
|
|
||||||
.useSansSerif();
|
.useSansSerif();
|
||||||
position : relative;
|
|
||||||
margin-top : 1.3em;
|
|
||||||
margin-left : -0.1em;
|
|
||||||
margin-right : -0.1em;
|
|
||||||
background-color : @noteGreen;
|
background-color : @noteGreen;
|
||||||
padding : 0.5em 0.6em;
|
border-style : solid;
|
||||||
|
border-width : 1px;
|
||||||
|
border-image : @noteBorderImage 12 stretch;
|
||||||
|
border-image-outset : 9px 0px;
|
||||||
|
border-image-width : 11px;
|
||||||
|
padding : 0.13cm 0.16cm;
|
||||||
|
filter : drop-shadow(1px 4px 6px #888);
|
||||||
|
.page :where(&) {
|
||||||
|
margin-top : 9px; //Prevent top border getting cut off on colbreak
|
||||||
|
}
|
||||||
& + * {
|
& + * {
|
||||||
margin-top : 1.3em;
|
margin-top : 0.45cm;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size : 0.375cm;
|
||||||
}
|
}
|
||||||
p{
|
p{
|
||||||
display : block;
|
display : block;
|
||||||
padding-bottom : 0px;
|
padding-bottom : 0px;
|
||||||
}
|
}
|
||||||
p + p {
|
|
||||||
padding-top : .8em;
|
|
||||||
}
|
|
||||||
:last-child {
|
:last-child {
|
||||||
margin-bottom : 0em;
|
margin-bottom : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//************************************
|
//************************************
|
||||||
@@ -238,35 +251,118 @@ body {
|
|||||||
// ************************************/
|
// ************************************/
|
||||||
.descriptive{
|
.descriptive{
|
||||||
.useSansSerif();
|
.useSansSerif();
|
||||||
display : block-inline;
|
|
||||||
margin-top : 1.4em;
|
|
||||||
background-color : #faf7ea;
|
background-color : #faf7ea;
|
||||||
font-family : ScalySansRemake;
|
|
||||||
border-style : solid;
|
border-style : solid;
|
||||||
border-width : 7px;
|
border-width : 7px;
|
||||||
border-image : @descriptiveBoxImage 12 stretch;
|
border-image : @descriptiveBoxImage 12 stretch;
|
||||||
border-image-outset : 4px;
|
border-image-outset : 4px;
|
||||||
box-shadow : 0px 0px 6px #faf7ea;
|
|
||||||
padding : 0.1em;
|
padding : 0.1em;
|
||||||
|
filter : drop-shadow(0 0 3px #faf7ea);
|
||||||
|
.page :where(&) {
|
||||||
|
margin-top : 4px; //Prevent top border getting cut off on colbreak
|
||||||
|
}
|
||||||
& + * {
|
& + * {
|
||||||
margin-top : 1.4em;
|
margin-top : 0.45cm;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size : 0.375cm;
|
||||||
}
|
}
|
||||||
p{
|
p{
|
||||||
display : block;
|
display : block;
|
||||||
padding-bottom : 0px;
|
padding-bottom : 0px;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
}
|
}
|
||||||
p + p {
|
|
||||||
padding-top : .8em;
|
|
||||||
}
|
|
||||||
:last-child {
|
:last-child {
|
||||||
margin-bottom : 0em;
|
margin-bottom : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//*****************************
|
||||||
|
// * Images Snippets
|
||||||
|
// *****************************/
|
||||||
|
|
||||||
|
/* Arist Credit */
|
||||||
|
.artist {
|
||||||
|
position : absolute;
|
||||||
|
width : auto;
|
||||||
|
text-align : center;
|
||||||
|
font-family : WalterTurncoat;
|
||||||
|
font-size : 0.27cm;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Watermark */
|
||||||
|
.watermark {
|
||||||
|
display : grid !important;
|
||||||
|
place-items : center;
|
||||||
|
justify-content : center;
|
||||||
|
position : absolute;
|
||||||
|
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 : @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; }
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * MONSTER STAT BLOCK
|
// * MONSTER STAT BLOCK
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.monster {
|
.monster {
|
||||||
|
.useSansSerif();
|
||||||
&.frame {
|
&.frame {
|
||||||
border-style : solid;
|
border-style : solid;
|
||||||
border-width : 7px 6px;
|
border-width : 7px 6px;
|
||||||
@@ -276,26 +372,16 @@ body {
|
|||||||
border-image-outset : 0px 2px;
|
border-image-outset : 0px 2px;
|
||||||
background-blend-mode : overlay;
|
background-blend-mode : overlay;
|
||||||
background-attachment : fixed;
|
background-attachment : fixed;
|
||||||
box-shadow : 1px 4px 14px #888;
|
filter : drop-shadow(1px 4px 6px #888);
|
||||||
padding : 4px 2px;
|
padding : 4px 2px;
|
||||||
margin : 0px -6px 1em;
|
margin-left : -0.16cm;
|
||||||
|
margin-right : -0.16cm;
|
||||||
|
width : calc(100% + 0.32cm);
|
||||||
}
|
}
|
||||||
.useSansSerif();
|
|
||||||
//-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns, but breaks internal columns...
|
|
||||||
position : relative;
|
position : relative;
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin-bottom : 1em;
|
margin-bottom : 0.325cm;
|
||||||
|
|
||||||
p{
|
|
||||||
margin-bottom : 0.3cm;
|
|
||||||
}
|
|
||||||
p+p {
|
|
||||||
margin-top : 0; //May not be needed
|
|
||||||
text-indent : 0;
|
|
||||||
}
|
|
||||||
p:last-of-type {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Headers
|
//Headers
|
||||||
h2{
|
h2{
|
||||||
@@ -312,7 +398,7 @@ body {
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
font-variant : small-caps;
|
font-variant : small-caps;
|
||||||
border-bottom : 2px solid @headerText;
|
border-bottom : 2px solid @headerText;
|
||||||
margin-top : 0.05cm;
|
// margin-top : 0.05cm; //Font is misaligned. Shift up slightly
|
||||||
padding-bottom : 0.05cm;
|
padding-bottom : 0.05cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,20 +412,28 @@ body {
|
|||||||
border : none;
|
border : none;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Attribute Lists
|
//Attribute Lists - All text between HRs is red
|
||||||
dl {
|
hr ~ :is(dl,p) {
|
||||||
color : @headerText;
|
color : @headerText;
|
||||||
}
|
}
|
||||||
|
hr:last-of-type {
|
||||||
|
& ~ :is(dl,p) {
|
||||||
|
color : inherit; // After the HRs, reset text to black
|
||||||
|
}
|
||||||
|
& + * {
|
||||||
|
margin-top : 0.325cm; // Space after last HR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Monster Ability table
|
// Monster Ability table
|
||||||
hr + table:first-of-type{
|
hr + table:first-of-type{
|
||||||
margin : 0;
|
margin : 0;
|
||||||
column-span : 1;
|
column-span : none;
|
||||||
color : @headerText;
|
color : @headerText;
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
border-style : none;
|
border-style : none;
|
||||||
border-image : none;
|
border-image : none;
|
||||||
-webkit-column-span : 1;
|
-webkit-column-span : none;
|
||||||
tr {
|
tr {
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
}
|
}
|
||||||
@@ -347,16 +441,17 @@ body {
|
|||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
margin-bottom : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Full Width
|
//Full Width
|
||||||
.monster.wide{
|
.monster.wide{
|
||||||
.useColumns(0.96);
|
.useColumns(0.96, @fillMode: balance);
|
||||||
}
|
}
|
||||||
|
|
||||||
hr+hr+blockquote{
|
|
||||||
.useColumns(0.96);
|
|
||||||
}
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * FOOTER
|
// * FOOTER
|
||||||
// *****************************/
|
// *****************************/
|
||||||
@@ -410,22 +505,33 @@ body {
|
|||||||
// * CODE BLOCKS
|
// * CODE BLOCKS
|
||||||
// ************************************/
|
// ************************************/
|
||||||
code{
|
code{
|
||||||
font-family: "Courier New", Courier, monospace;
|
font-family : "Courier New", Courier, monospace;
|
||||||
font-size: 0.325;
|
font-size : 0.325;
|
||||||
padding: 2px 4px;
|
padding : 0px 4px;
|
||||||
color: #58180d;
|
color : #58180d;
|
||||||
background-color: #faf7ea;
|
background-color : #faf7ea;
|
||||||
border-radius: 4px;
|
border-radius : 4px;
|
||||||
|
white-space : pre-wrap;
|
||||||
|
overflow-wrap : break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre code{
|
pre code{
|
||||||
width : 100%;
|
width : 100%;
|
||||||
display : block;
|
display : inline-block;
|
||||||
border : 4px solid;
|
border-style : solid;
|
||||||
|
border-width : 1px;
|
||||||
border-image : @codeBorderImage 26 stretch;
|
border-image : @codeBorderImage 26 stretch;
|
||||||
border-image-width : 10px;
|
border-image-width : 10px;
|
||||||
border-image-outset : 2px;
|
border-image-outset : 2px;
|
||||||
border-radius : 12px;
|
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
|
// * EXTRAS
|
||||||
@@ -434,13 +540,6 @@ body {
|
|||||||
visibility : hidden;
|
visibility : hidden;
|
||||||
margin : 0px;
|
margin : 0px;
|
||||||
}
|
}
|
||||||
//Modified unorder list, used in spells
|
|
||||||
hr+ul{
|
|
||||||
margin-bottom : 0.5em;
|
|
||||||
padding-left : 1em;
|
|
||||||
text-indent : -1em;
|
|
||||||
list-style-type : none;
|
|
||||||
}
|
|
||||||
.columnSplit {
|
.columnSplit {
|
||||||
visibility : hidden;
|
visibility : hidden;
|
||||||
-webkit-column-break-after : always;
|
-webkit-column-break-after : always;
|
||||||
@@ -449,16 +548,12 @@ body {
|
|||||||
break-before : column;
|
break-before : column;
|
||||||
}
|
}
|
||||||
//Avoid breaking up
|
//Avoid breaking up
|
||||||
p,blockquote,table{
|
blockquote,table{
|
||||||
z-index : 15;
|
z-index : 15;
|
||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
}
|
}
|
||||||
//Better spacing for spell blocks
|
|
||||||
h4+p+hr+ul{
|
|
||||||
margin-top : -0.5em
|
|
||||||
}
|
|
||||||
//Text indent right after table
|
//Text indent right after table
|
||||||
table+p{
|
table+p{
|
||||||
text-indent : 1em;
|
text-indent : 1em;
|
||||||
@@ -479,16 +574,13 @@ body {
|
|||||||
// *****************************/
|
// *****************************/
|
||||||
.page .spellList{
|
.page .spellList{
|
||||||
.useSansSerif();
|
.useSansSerif();
|
||||||
column-count : 4;
|
column-count : 2;
|
||||||
column-span : all;
|
|
||||||
-webkit-column-span : all;
|
|
||||||
-moz-column-span : all;
|
|
||||||
ul+h5{
|
ul+h5{
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
}
|
}
|
||||||
p, ul{
|
p, ul{
|
||||||
font-size : 0.352cm;
|
font-size : 0.352cm;
|
||||||
line-height : 1.3em;
|
line-height : 1.265em;
|
||||||
}
|
}
|
||||||
ul{
|
ul{
|
||||||
margin-bottom : 0.5em;
|
margin-bottom : 0.5em;
|
||||||
@@ -499,32 +591,59 @@ body {
|
|||||||
page-break-inside : auto;
|
page-break-inside : auto;
|
||||||
break-inside : auto;
|
break-inside : auto;
|
||||||
}
|
}
|
||||||
|
&.wide{
|
||||||
|
column-count : 4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//*****************************
|
|
||||||
// * WIDE
|
|
||||||
// *****************************/
|
|
||||||
.page .wide{
|
|
||||||
column-span : all;
|
|
||||||
-webkit-column-span : all;
|
|
||||||
-moz-column-span : all;
|
|
||||||
}
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * CLASS TABLE
|
// * CLASS TABLE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page .classTable{
|
.page .classTable{
|
||||||
margin-top : 25px;
|
th[colspan]:not([rowspan]) {
|
||||||
margin-bottom : 40px;
|
white-space : nowrap;
|
||||||
border-collapse : separate;
|
}
|
||||||
background-color : white;
|
&.frame {
|
||||||
border : initial;
|
margin-top : 0.66cm;
|
||||||
border-style : solid;
|
margin-bottom : 1.05cm;
|
||||||
border-image-outset : 25px 17px;
|
margin-left : -0.1cm;
|
||||||
border-image-repeat : stretch;
|
margin-right : -0.1cm;
|
||||||
border-image-slice : 150 200 150 200;
|
width : calc(100% + 0.2cm);
|
||||||
border-image-source : @frameBorderImage;
|
border-collapse : separate;
|
||||||
border-image-width : 47px;
|
background-color : white;
|
||||||
h5{
|
border : initial;
|
||||||
margin-bottom : 10px;
|
border-style : solid;
|
||||||
|
border-image-outset : 0.55cm 0.3cm;
|
||||||
|
border-image-repeat : stretch;
|
||||||
|
border-image-slice : 200;
|
||||||
|
border-image-source : @frameBorderImage;
|
||||||
|
border-image-width : 47px;
|
||||||
|
}
|
||||||
|
&.decoration {
|
||||||
|
transform-style : preserve-3d;
|
||||||
|
z-index: -1;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
&.decoration::before {
|
||||||
|
content :'';
|
||||||
|
position : absolute;
|
||||||
|
background-image : @classTableDecoration;
|
||||||
|
background-size : contain;
|
||||||
|
background-repeat : space;
|
||||||
|
width : 7.75cm;
|
||||||
|
height : calc(100% + 3.3cm);
|
||||||
|
top : 50%;
|
||||||
|
left : 50%;
|
||||||
|
transform : translateY(-50%) translateX(-50%) translateZ(-1px);
|
||||||
|
filter : drop-shadow(0px 0px 1px #C8C5C080)
|
||||||
|
}
|
||||||
|
&.decoration.wide::before {
|
||||||
|
width : calc(100% + 3.3cm);
|
||||||
|
height : 7.75cm;
|
||||||
|
top : calc(50% + 0.4cm);
|
||||||
|
}
|
||||||
|
h5 + table{
|
||||||
|
margin-top : 0.2cm;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
@@ -536,10 +655,10 @@ body {
|
|||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
h1 {
|
h1 {
|
||||||
text-align : center;
|
text-align : center;
|
||||||
margin-bottom : 0.1cm;
|
margin-bottom : 0.3cm;
|
||||||
}
|
}
|
||||||
a{
|
a{
|
||||||
display : table;
|
display : inline;
|
||||||
color : inherit;
|
color : inherit;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
&:hover{
|
&:hover{
|
||||||
@@ -547,7 +666,11 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
h4 {
|
h4 {
|
||||||
margin-top : 0.1cm;
|
margin-top : 0.2cm;
|
||||||
|
line-height : 0.4cm;
|
||||||
|
& + ul li {
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ul{
|
ul{
|
||||||
padding-left : 0;
|
padding-left : 0;
|
||||||
@@ -567,22 +690,22 @@ body {
|
|||||||
&::after {
|
&::after {
|
||||||
content : "";
|
content : "";
|
||||||
position : absolute;
|
position : absolute;
|
||||||
bottom : 0.08cm; /* Set as you want */
|
bottom : 0.08cm;
|
||||||
margin-left : 0.06cm; /* Spacing before dot leaders */
|
margin-left : 0.06cm; /* Spacing before dot leaders */
|
||||||
width : 100%;
|
width : 100%;
|
||||||
border-bottom : 0.05cm dotted #000;
|
border-bottom : 0.05cm dotted #000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:last-child {
|
&:last-child {
|
||||||
font-family : BookInsanityRemake;
|
font-family : BookInsanityRemake;
|
||||||
font-size : 0.34cm;
|
font-size : 0.34cm;
|
||||||
font-weight : normal;
|
font-weight : normal;
|
||||||
color : black;
|
color : black;
|
||||||
text-align : right;
|
text-align : right;
|
||||||
vertical-align : bottom; /* Keep Price text bottom-aligned */
|
vertical-align : bottom; /* Keep page number bottom-aligned */
|
||||||
width : 1%;
|
width : 1%;
|
||||||
padding-left : 0.06cm; /* Spacing after dot leaders */
|
padding-left : 0.06cm; /* Spacing after dot leaders */
|
||||||
/*white-space: nowrap; /* Uncomment if needed */
|
/*white-space : nowrap; /* Uncomment if needed */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ul { /*List indent*/
|
ul { /*List indent*/
|
||||||
@@ -590,7 +713,7 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.wide{
|
&.wide{
|
||||||
.useColumns(0.96);
|
.useColumns(0.96, @fillMode: balance);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,16 +722,16 @@ body {
|
|||||||
// *****************************/
|
// *****************************/
|
||||||
.page {
|
.page {
|
||||||
.block {
|
.block {
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
display : inline-block;
|
||||||
|
.page :where(&) {
|
||||||
|
width : 100%;
|
||||||
|
}
|
||||||
|
//-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
||||||
}
|
}
|
||||||
.inline-block {
|
.inline-block {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
text-indent : initial;
|
text-indent : initial;
|
||||||
line-height : 1.3em;
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
column-gap : 0.5cm; //Default spacing if a div uses multicolumns
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,22 +740,26 @@ body {
|
|||||||
// *****************************/
|
// *****************************/
|
||||||
.page {
|
.page {
|
||||||
dl {
|
dl {
|
||||||
line-height : 1.3em;
|
line-height : 1.25em;
|
||||||
padding-left : 1em;
|
padding-left : 1em;
|
||||||
text-indent : -1em;
|
white-space : pre-line;
|
||||||
|
& + * {
|
||||||
|
margin-top : 0.28cm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dl + p {
|
dl + * {
|
||||||
margin-top: 0.5em;
|
margin-top : 0.17cm;
|
||||||
}
|
}
|
||||||
p + dl {
|
p + dl {
|
||||||
margin-top: -0.5em;
|
margin-top: 0.17cm;
|
||||||
}
|
}
|
||||||
dt {
|
dt {
|
||||||
float: left;
|
display : inline;
|
||||||
//clear: left; //Doesn't seem necessary
|
margin-right : 5px;
|
||||||
margin-right: 5px;
|
margin-left : -1em;
|
||||||
}
|
}
|
||||||
dd {
|
dd {
|
||||||
|
display : inline;
|
||||||
margin-left : 0px;
|
margin-left : 0px;
|
||||||
text-indent : 0px;
|
text-indent : 0px;
|
||||||
}
|
}
|
||||||
@@ -643,9 +770,21 @@ body {
|
|||||||
// *****************************/
|
// *****************************/
|
||||||
.page {
|
.page {
|
||||||
.blank {
|
.blank {
|
||||||
height: 0.75em;
|
height : 1em;
|
||||||
}
|
margin-top : 0;
|
||||||
p + .blank {
|
}
|
||||||
margin-top: -1em;
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
// * WIDE
|
||||||
|
// *****************************/
|
||||||
|
.page .wide{
|
||||||
|
column-span : all;
|
||||||
|
-webkit-column-span : all;
|
||||||
|
-moz-column-span : all;
|
||||||
|
display : block;
|
||||||
|
margin-bottom : 0.34cm;
|
||||||
|
&+* {
|
||||||
|
margin-top : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
@import (less) './themes/assets/assets.less';
|
@import (less) './themes/assets/assets.less';
|
||||||
@import (less) './themes/phb.depricated.less';
|
@import (less) './themes/phb.depricated.less';
|
||||||
//Colors
|
//Colors
|
||||||
@background : #EEE5CE;
|
@background : #EEE5CE; // Light parchment
|
||||||
@noteGreen : #e0e5c1;
|
@noteGreen : #e0e5c1; // Pastel green
|
||||||
@headerUnderline : #c9ad6a;
|
@headerUnderline : #c9ad6a; // Gold
|
||||||
@horizontalRule : #9c2b1b;
|
@horizontalRule : #9c2b1b; // Maroon
|
||||||
@headerText : #58180D;
|
@headerText : #58180D; // Dark maroon
|
||||||
@monsterStatBackground : #FDF1DC;
|
@monsterStatBackground : #FDF1DC; // Lighter parchment
|
||||||
|
@captionText : #766649; // Brown
|
||||||
@page { margin: 0; }
|
@page { margin: 0; }
|
||||||
body {
|
body {
|
||||||
counter-reset : phb-page-numbers;
|
counter-reset : phb-page-numbers;
|
||||||
@@ -62,7 +63,7 @@ body {
|
|||||||
// *****************************/
|
// *****************************/
|
||||||
p{
|
p{
|
||||||
padding-bottom : 0.8em;
|
padding-bottom : 0.8em;
|
||||||
line-height : 1.3em;
|
line-height : 1.269em;
|
||||||
&+p{
|
&+p{
|
||||||
margin-top : -0.8em;
|
margin-top : -0.8em;
|
||||||
}
|
}
|
||||||
@@ -70,14 +71,14 @@ body {
|
|||||||
ul{
|
ul{
|
||||||
margin-bottom : 0.8em;
|
margin-bottom : 0.8em;
|
||||||
padding-left : 1.4em;
|
padding-left : 1.4em;
|
||||||
line-height : 1.3em;
|
line-height : 1.269em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
}
|
}
|
||||||
ol{
|
ol{
|
||||||
margin-bottom : 0.8em;
|
margin-bottom : 0.8em;
|
||||||
padding-left : 1.4em;
|
padding-left : 1.4em;
|
||||||
line-height : 1.3em;
|
line-height : 1.269em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : decimal;
|
list-style-type : decimal;
|
||||||
}
|
}
|
||||||
@@ -125,7 +126,7 @@ body {
|
|||||||
font-family : Solberry;
|
font-family : Solberry;
|
||||||
font-size : 10em;
|
font-size : 10em;
|
||||||
color : #222;
|
color : #222;
|
||||||
line-height : 0.8em;
|
line-height : 0.795em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
h2{
|
h2{
|
||||||
@@ -190,7 +191,7 @@ body {
|
|||||||
box-shadow : 1px 4px 14px #888;
|
box-shadow : 1px 4px 14px #888;
|
||||||
p, ul{
|
p, ul{
|
||||||
font-size : 0.352cm;
|
font-size : 0.352cm;
|
||||||
line-height : 1.1em;
|
line-height : 1.083em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//If a note starts a column, give it space at the top to render border
|
//If a note starts a column, give it space at the top to render border
|
||||||
@@ -230,11 +231,9 @@ body {
|
|||||||
// Monster Ability table
|
// Monster Ability table
|
||||||
hr+table{
|
hr+table{
|
||||||
margin : 0;
|
margin : 0;
|
||||||
column-span : 1;
|
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
border-style : none;
|
border-style : none;
|
||||||
border-image : none;
|
border-image : none;
|
||||||
-webkit-column-span : 1;
|
|
||||||
tbody{
|
tbody{
|
||||||
tr:nth-child(odd), tr:nth-child(even){
|
tr:nth-child(odd), tr:nth-child(even){
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
@@ -372,7 +371,7 @@ body {
|
|||||||
}
|
}
|
||||||
p, ul{
|
p, ul{
|
||||||
font-size : 0.352cm;
|
font-size : 0.352cm;
|
||||||
line-height : 1.3em;
|
line-height : 1.263em;
|
||||||
}
|
}
|
||||||
ul{
|
ul{
|
||||||
margin-bottom : 0.5em;
|
margin-bottom : 0.5em;
|
||||||
@@ -415,7 +414,6 @@ body {
|
|||||||
// * DESCRIPTIVE TEXT BOX
|
// * DESCRIPTIVE TEXT BOX
|
||||||
// ************************************/
|
// ************************************/
|
||||||
.phb .descriptive{
|
.phb .descriptive{
|
||||||
display : block-inline;
|
|
||||||
margin-bottom : 1em;
|
margin-bottom : 1em;
|
||||||
background-color : #faf7ea;
|
background-color : #faf7ea;
|
||||||
font-family : ScalySans;
|
font-family : ScalySans;
|
||||||
@@ -427,7 +425,7 @@ body {
|
|||||||
p{
|
p{
|
||||||
display : block;
|
display : block;
|
||||||
padding-bottom : 0px;
|
padding-bottom : 0px;
|
||||||
line-height : 1.5em;
|
line-height : 1.47em;
|
||||||
}
|
}
|
||||||
p + p {
|
p + p {
|
||||||
padding-top : .8em;
|
padding-top : .8em;
|
||||||
@@ -445,6 +443,35 @@ body {
|
|||||||
.phb pre+.descriptive{
|
.phb pre+.descriptive{
|
||||||
margin-top : 8px;
|
margin-top : 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
// * ARTIST CREDIT BLOCK
|
||||||
|
// *****************************/
|
||||||
|
.phb {
|
||||||
|
.artist {
|
||||||
|
position : absolute;
|
||||||
|
text-align : center;
|
||||||
|
font-family : WalterTurncoat;
|
||||||
|
font-size : 0.27cm;
|
||||||
|
color : @captionText;
|
||||||
|
p, p + p {
|
||||||
|
margin : unset;
|
||||||
|
text-indent : unset;
|
||||||
|
line-height : 0.941em;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
font-size : 1.3em;
|
||||||
|
font-family : WalterTurncoat;
|
||||||
|
}
|
||||||
|
a{
|
||||||
|
color : inherit;
|
||||||
|
text-decoration : unset;
|
||||||
|
&:hover {
|
||||||
|
text-decoration : underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * TABLE OF CONTENTS
|
// * TABLE OF CONTENTS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// PHB
|
||||||
@footerAccentImage : data-uri('./themes/assets/footerAccent.png');
|
@footerAccentImage : data-uri('./themes/assets/footerAccent.png');
|
||||||
@frameBorderImage : data-uri('./themes/assets/frameBorder.png');
|
@frameBorderImage : data-uri('./themes/assets/frameBorder.png');
|
||||||
@backgroundImage : data-uri('./themes/assets/parchmentBackground.jpg');
|
@backgroundImage : data-uri('./themes/assets/parchmentBackground.jpg');
|
||||||
@@ -8,3 +9,18 @@
|
|||||||
@monsterBlockBackground : data-uri('./themes/assets/parchmentBackgroundGrayscale.jpg');
|
@monsterBlockBackground : data-uri('./themes/assets/parchmentBackgroundGrayscale.jpg');
|
||||||
@monsterBorderImage : data-uri('./themes/assets/monsterBorderFancy.png');
|
@monsterBorderImage : data-uri('./themes/assets/monsterBorderFancy.png');
|
||||||
@codeBorderImage : data-uri('./themes/assets/codeBorder.png');
|
@codeBorderImage : data-uri('./themes/assets/codeBorder.png');
|
||||||
|
@classTableDecoration : data-uri('./themes/assets/classTableDecoration.png');
|
||||||
|
|
||||||
|
// Watercolor Images
|
||||||
|
@watercolor1 : data-uri('./themes/assets/watercolor/watercolor1.png');
|
||||||
|
@watercolor2 : data-uri('./themes/assets/watercolor/watercolor2.png');
|
||||||
|
@watercolor3 : data-uri('./themes/assets/watercolor/watercolor3.png');
|
||||||
|
@watercolor4 : data-uri('./themes/assets/watercolor/watercolor4.png');
|
||||||
|
@watercolor5 : data-uri('./themes/assets/watercolor/watercolor5.png');
|
||||||
|
@watercolor6 : data-uri('./themes/assets/watercolor/watercolor6.png');
|
||||||
|
@watercolor7 : data-uri('./themes/assets/watercolor/watercolor7.png');
|
||||||
|
@watercolor8 : data-uri('./themes/assets/watercolor/watercolor8.png');
|
||||||
|
@watercolor9 : data-uri('./themes/assets/watercolor/watercolor9.png');
|
||||||
|
@watercolor10 : data-uri('./themes/assets/watercolor/watercolor10.png');
|
||||||
|
@watercolor11 : data-uri('./themes/assets/watercolor/watercolor11.png');
|
||||||
|
@watercolor12 : data-uri('./themes/assets/watercolor/watercolor12.png');
|
||||||
|
|||||||
BIN
themes/assets/classTableDecoration.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
themes/assets/discord.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
1
themes/assets/discordOfManyThings.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_3" data-name="Layer 3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350.028 390.552"><defs><style>.cls-1{opacity:0.9;}.cls-2{fill:#001013;}.cls-3{fill:#fff;}.cls-4{fill:#aa2a29;}</style></defs><title>Discord of Many Things blank</title><g class="cls-1"><path d="M100.991,82.743c-1.649,3.324-3.395,5.768-4.1,8.482-1.49,5.753-3.44,11.644-3.455,17.482q-.332,125.259-.141,250.519c.007,20.122,12.708,36.368,31.407,40.239a38.328,38.328,0,0,0,7.722.761q103.611,0,207.222-.088h6.314c-3.256-11.462-9.664-34.015-9.664-34.015l2.431,2.023s41.561,37.755,62.33,56.6c2.489,2.26,3.694,4.462,3.593,7.885-.241,8.186-.078,16.383-.078,25.492-6.333-5.527-11.928-10.35-17.457-15.247-9.586-8.493-19.2-16.957-28.644-25.6a10.032,10.032,0,0,0-7.39-2.774q-117.228-.1-234.457-.334c-20.632-.031-33.959-9.189-41.021-28.578-.852-2.339-.783-5.091-.784-7.653q-.057-116.5-.031-233.007V119.145C74.791,102.517,84.2,89.068,100.991,82.743Z" transform="translate(-74.79 -67.573)"/></g><path class="cls-2" d="M338.234,371.589s5.556,19.924,8.513,30.53h-5.075c-69.335,0-138.67-.1-208,.074-16.925.043-35.116-9.515-40.505-33.247a26.538,26.538,0,0,1-.379-5.827q-.027-128.172.025-256.344c.021-19.276,15.226-36.749,34.427-38.688a82.669,82.669,0,0,1,8.279-.447q123.288-.034,246.578-.067c13.265-.016,24.7,3.79,33.278,14.235a39.273,39.273,0,0,1,9.447,25.5q-.015,167.477-.012,334.955c0,1.245-.1,2.49-.2,4.645-4.075-3.565-7.6-6.552-11.018-9.655q-35.236-31.992-70.44-64.017c-1.2-1.091-3.713-3.147-3.713-3.147l-2-1.775Z" transform="translate(-74.79 -67.573)"/><path class="cls-3" d="M419.838,435.934c-7.848-6.892-15.026-12.977-21.961-19.329q-28.235-25.86-56.253-51.954c-2.314-2.157-4.59-3.247-7.356-1.784-3.036,1.607-2.77,4.676-2.022,7.359,2.286,8.2,4.817,16.338,7.24,24.5a17.108,17.108,0,0,1,.294,1.851c-1.486.084-2.872.233-4.258.233-67.363.012-134.726-.155-202.088.123-18.267.075-32.955-13.095-34.769-29.353a92.257,92.257,0,0,1-.886-10.108Q97.7,233.488,97.793,109.5c.013-10.518,3.19-20.063,11.156-27.471a31.92,31.92,0,0,1,20.439-8.917c1.948-.109,3.9-.181,5.852-.181q123.5-.014,247-.063c9.957-.012,18.894,2.145,26.329,9.1,7.956,7.441,11.177,16.934,11.214,27.457.142,40.84.057,81.681.057,122.522V435.934Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M311.029,170.975c-11.8,45.005-23.315,88.959-35.123,134.016l-97.168-98.883Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M380.8,265.712l-95.97,40.316c11.438-43.808,22.639-86.709,33.839-129.609l1.08-.272Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.3,322.955c2.91-36.463,5.761-72.179,8.709-109.116l94.711,96.472Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M223.458,107.926l83.127,55.063L178.71,196.919Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M366.285,281.366l-72.5,73.414c-3.524-13.182-6.849-25.617-10.306-38.549Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M274.874,318.478c3.445,12.8,6.765,25.143,10.376,38.559l-98.635-26.556.032-1.153Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M374.888,241.261l-51.71-75.57,26.8-21.529,25.831,96.619Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.492,207.523c-2.39,30.748-4.736,60.942-7.083,91.135l-1.063.159c-8.5-31.936-17-63.872-25.705-96.581Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M317.039,159.316l-68.251-45.24.307-.794,93.794,25.212Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M136.517,193.952,203.5,125.03l1.086.832L167.794,198.9Z" transform="translate(-74.79 -67.573)"/></svg>
|
||||||
|
After Width: | Height: | Size: 3.5 KiB |
BIN
themes/assets/github.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
themes/assets/patreon.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
themes/assets/reddit.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
themes/assets/watercolor/watercolor1.png
Normal file
|
After Width: | Height: | Size: 161 KiB |