mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 07:43:01 +00:00
Compare commits
1272 Commits
v2.9.0
...
TestReactF
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f3388c687 | ||
|
|
87c28c76f3 | ||
|
|
b2ec0d4a0c | ||
|
|
02560d82ab | ||
|
|
2db127d805 | ||
|
|
6fc176e616 | ||
|
|
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 | ||
|
|
28793e06fc | ||
|
|
ba74b5aa13 | ||
|
|
01bceca7df | ||
|
|
ccca313a15 | ||
|
|
c463eedc50 | ||
|
|
7f49d6f08b | ||
|
|
78d4487c58 | ||
|
|
8a3f52b704 | ||
|
|
6e04535eff | ||
|
|
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 | ||
|
|
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 | ||
|
|
cd18692a53 | ||
|
|
220316ec7e | ||
|
|
bbf6f7fb06 | ||
|
|
82fc581125 | ||
|
|
6be4fcefdb | ||
|
|
f6eab47ab8 | ||
|
|
dd887e9a4f | ||
|
|
a8f5f71b32 | ||
|
|
dd93c4cdd4 | ||
|
|
7eded57d79 | ||
|
|
3de1d3afb0 | ||
|
|
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 | ||
|
|
fbcd4036f5 | ||
|
|
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 | ||
|
|
0a742e8c2f | ||
|
|
e14c42761d | ||
|
|
b6f7dc048f | ||
|
|
4efd89627d | ||
|
|
6f6c4acf7e | ||
|
|
30745c2be3 | ||
|
|
e257776852 | ||
|
|
502b0c4cc5 | ||
|
|
0d8c3a1e60 | ||
|
|
7254fbcd74 | ||
|
|
4d087f4aa9 | ||
|
|
874c8a9fd1 | ||
|
|
4d61670f38 | ||
|
|
00f90d1084 | ||
|
|
3cb50bc7fc | ||
|
|
213ef9d94b | ||
|
|
046b6266b3 | ||
|
|
8a03062e3d | ||
|
|
2a40f05e90 | ||
|
|
ce73e9293d | ||
|
|
f469a7e360 | ||
|
|
3c2feeb2aa | ||
|
|
fdb294bad9 | ||
|
|
56975f9375 | ||
|
|
cb74c0d389 | ||
|
|
33abe05737 | ||
|
|
61ca7fd0f6 | ||
|
|
21223cbcd4 | ||
|
|
d02d51717d | ||
|
|
004f3f184f | ||
|
|
99d2f6d48d | ||
|
|
11d1f5c00e | ||
|
|
ebd28f41a2 | ||
|
|
2397fcaa21 | ||
|
|
5b039b82a3 | ||
|
|
264f5d5068 | ||
|
|
eff5660f12 | ||
|
|
98915e158d | ||
|
|
9be71a5159 | ||
|
|
6b61bb05c0 | ||
|
|
4c389a4077 | ||
|
|
e6ebdd5be3 | ||
|
|
22eb7de7ea | ||
|
|
c4c5e21ce0 | ||
|
|
0c0ba0b6ca | ||
|
|
295a4cd1cd | ||
|
|
db3bec9e2b | ||
|
|
577a434e17 | ||
|
|
cac5aa2475 | ||
|
|
85fa73b9bf | ||
|
|
fdfea36614 | ||
|
|
2f663e0ea7 | ||
|
|
5d05af089b | ||
|
|
e237cd8be4 | ||
|
|
8bd09e58cb | ||
|
|
4e2a3cc5be | ||
|
|
d9c83379fe | ||
|
|
0818a3485a | ||
|
|
7fa1e16b5a | ||
|
|
acb750c18a | ||
|
|
72d8b5ea16 | ||
|
|
6238ed6b77 | ||
|
|
fa5bd92406 | ||
|
|
189fdb4555 | ||
|
|
caf151a0dd | ||
|
|
d35769dceb | ||
|
|
1031e8a55a | ||
|
|
a71dca1487 | ||
|
|
b80a249cf7 | ||
|
|
54d0e2c483 | ||
|
|
c91e5784ac | ||
|
|
48e80803f7 | ||
|
|
495a68893d | ||
|
|
41e1ed7bd1 | ||
|
|
7eb63db502 | ||
|
|
c6d0a2e2ad | ||
|
|
1a2da712ed | ||
|
|
36627bc188 | ||
|
|
f31fe6cbf0 | ||
|
|
9f8a857cef | ||
|
|
fbf053ac2b | ||
|
|
c77338c65e | ||
|
|
42b0ea173d | ||
|
|
7c9defb85c | ||
|
|
6e5d183bf6 | ||
|
|
0ab00c24c5 | ||
|
|
c23763a2cf | ||
|
|
84b2d86054 | ||
|
|
ba766254f8 | ||
|
|
a02e36e13f | ||
|
|
8f34e8bb2d | ||
|
|
38cca54b7f | ||
|
|
7b44b5b7db | ||
|
|
3ed4ceb7a3 | ||
|
|
76e4023b37 | ||
|
|
7ff6d9e825 | ||
|
|
64d133f8f6 | ||
|
|
324d0e265e | ||
|
|
cec4addcad | ||
|
|
43605df266 | ||
|
|
4f03df097c | ||
|
|
72dc62e5dd | ||
|
|
3520c03797 | ||
|
|
fcbbe46861 | ||
|
|
4a398143e3 | ||
|
|
bbaaf74302 | ||
|
|
d3dd3c3d5d | ||
|
|
4f2ddfa020 | ||
|
|
428ec8412f | ||
|
|
50991dfe92 | ||
|
|
63ba9f4fb9 | ||
|
|
efd0fd1f4a | ||
|
|
5a7767cf0e | ||
|
|
3948e17da2 | ||
|
|
4e1e6bd69a | ||
|
|
9333bc73ea | ||
|
|
3540a35a6c | ||
|
|
ee67ba729a | ||
|
|
8414961b15 | ||
|
|
f8de983e2b | ||
|
|
d40afa619b | ||
|
|
55e1d0fb6e | ||
|
|
2661e2cfa0 | ||
|
|
d4cb5c73aa | ||
|
|
9a2d7d1a19 | ||
|
|
017bccc937 | ||
|
|
dea683da7c | ||
|
|
496ab26972 | ||
|
|
c18eb948b4 | ||
|
|
0cd4b730d7 | ||
|
|
63ea5a3e5f | ||
|
|
33f5e8838b | ||
|
|
3660f3827f | ||
|
|
ac4cce1f9b | ||
|
|
532d2428b7 | ||
|
|
205ed8e30e | ||
|
|
4119626cb7 | ||
|
|
94fdca084a | ||
|
|
599c69c9bb | ||
|
|
7843691c4b | ||
|
|
d9effacb20 | ||
|
|
3efb0bf189 | ||
|
|
00eb927538 | ||
|
|
0616ce62eb | ||
|
|
a171de32d8 | ||
|
|
cf7680bc86 | ||
|
|
e07bb1b3c2 | ||
|
|
1f830b96b5 | ||
|
|
ff7585b69d | ||
|
|
715ee88f38 | ||
|
|
142c9ad3b7 | ||
|
|
e2280197b9 | ||
|
|
a74916d593 | ||
|
|
ad0e4a2099 | ||
|
|
2613d43f3c | ||
|
|
09c7f45c69 | ||
|
|
0eaeb748f4 | ||
|
|
b72191ae68 | ||
|
|
cf4bfc35ea | ||
|
|
69231ba57a | ||
|
|
d61fda9cff | ||
|
|
6ecf546baf | ||
|
|
ea8aa84009 | ||
|
|
353f1ca42c | ||
|
|
20053ad548 | ||
|
|
9b97e0dd87 | ||
|
|
8e304fa483 | ||
|
|
0f5e2e5a60 | ||
|
|
f5bd7db388 | ||
|
|
70832be810 | ||
|
|
68ed6019f6 | ||
|
|
4638c3e1d9 | ||
|
|
53cb9a35ee | ||
|
|
9d80f21ae7 | ||
|
|
4d5653854a | ||
|
|
70cc8577e8 | ||
|
|
f80d5e6b52 | ||
|
|
19456e8be0 | ||
|
|
c98cedc20f | ||
|
|
2b1063c34d | ||
|
|
fc8be9c8fb | ||
|
|
70bdb07c1e | ||
|
|
51aba937f5 | ||
|
|
9363a15daa | ||
|
|
1ef5bfed94 | ||
|
|
e67fadef02 | ||
|
|
99825d10c4 | ||
|
|
a7b52f9a96 | ||
|
|
ef9d4d8525 | ||
|
|
2f751285ed | ||
|
|
4504a25272 | ||
|
|
aefc4698ab | ||
|
|
28af7353ea | ||
|
|
22a078b628 | ||
|
|
d8a8275723 | ||
|
|
d13b478c56 | ||
|
|
5ee146b6be | ||
|
|
d666bacf1f | ||
|
|
81662bf86b | ||
|
|
99901ed0ea | ||
|
|
18a96890ee | ||
|
|
3a4c72f1b8 | ||
|
|
19866010df | ||
|
|
e3e00bbd7c | ||
|
|
c4e3bfee6c | ||
|
|
d1c9f6f5dd | ||
|
|
58ccec1b46 | ||
|
|
8faa45b19f | ||
|
|
b2595e55cc | ||
|
|
f309df5971 | ||
|
|
7cdd90973b | ||
|
|
ccdbffb376 | ||
|
|
2eeb2a4454 | ||
|
|
1f894094c7 | ||
|
|
5f06de03a9 | ||
|
|
23e773ce64 | ||
|
|
3b34fe72b9 | ||
|
|
34f620c59b | ||
|
|
a5a5127088 | ||
|
|
b939d936e9 | ||
|
|
1b5e27a9b4 | ||
|
|
789c18307a | ||
|
|
1bc0964aff | ||
|
|
ce663155c4 | ||
|
|
1ad46c1ba9 | ||
|
|
9901c8c3f5 | ||
|
|
b20b981a01 | ||
|
|
ff860df5c3 | ||
|
|
69072f8e50 | ||
|
|
53bf47f7cb | ||
|
|
61032710e8 | ||
|
|
00527e7cf3 | ||
|
|
0423a43650 | ||
|
|
2ba10655a8 | ||
|
|
c5989ea95d | ||
|
|
3f6c7a9c25 | ||
|
|
a95e3552ff | ||
|
|
ef707a9b30 | ||
|
|
be51ab52fb | ||
|
|
e0a25ea918 | ||
|
|
72ae258fa5 | ||
|
|
33d124e3f3 | ||
|
|
bc87f61bdc | ||
|
|
fe03cca72b | ||
|
|
2007113ed8 | ||
|
|
f89b08a577 | ||
|
|
288705950c | ||
|
|
3240e0c348 | ||
|
|
185c02f4ac | ||
|
|
f382aaf73c | ||
|
|
be88c992fa | ||
|
|
85ff25a63b | ||
|
|
4e65c62881 | ||
|
|
6d035f2a2d | ||
|
|
7a35f6bb24 | ||
|
|
c00e956909 | ||
|
|
cf3bf459f4 | ||
|
|
e82d109840 | ||
|
|
c9a84a1813 | ||
|
|
7186a94c27 | ||
|
|
45e4e98cb5 | ||
|
|
9fc31e7f39 | ||
|
|
983a37c77f | ||
|
|
a3b6a90fde | ||
|
|
b771d82100 | ||
|
|
9fa179ed9c | ||
|
|
14d83d4263 | ||
|
|
73ccad8a76 | ||
|
|
488dbbb336 | ||
|
|
08c8b69f4d | ||
|
|
cabb9b6c3b | ||
|
|
6697aa096a | ||
|
|
582725e7d7 | ||
|
|
476d618286 | ||
|
|
c186b6677b | ||
|
|
ea9ba84dc2 | ||
|
|
bf616494f1 | ||
|
|
0b54bc046d | ||
|
|
c8c1966b8a | ||
|
|
9ad1c91472 | ||
|
|
d8525f0eba | ||
|
|
7ae419716a | ||
|
|
b0185a9ae4 | ||
|
|
d2cdb18a57 | ||
|
|
f04df5e297 | ||
|
|
b90caaba85 | ||
|
|
d15bec08a3 | ||
|
|
ab473b12da | ||
|
|
83c444ce11 | ||
|
|
3ade40f2d9 | ||
|
|
0debd2bbf0 | ||
|
|
1a3afc9661 | ||
|
|
ac4ebbe548 | ||
|
|
089414c9ff | ||
|
|
a1dbf0f2e5 | ||
|
|
712824d8a6 | ||
|
|
7491f463b4 | ||
|
|
8f08591ab9 | ||
|
|
b98586150f | ||
|
|
2f094801ca | ||
|
|
dd35f101fe | ||
|
|
8a7513afd0 | ||
|
|
2628ec00dc | ||
|
|
778e27a374 | ||
|
|
dd41eddd72 | ||
|
|
5872452a6a | ||
|
|
af05403846 | ||
|
|
3a55755721 | ||
|
|
24957c653d | ||
|
|
6a12518ac1 | ||
|
|
318e2924ca | ||
|
|
0da5d00f9c | ||
|
|
7612702d73 | ||
|
|
9ba91b2dcc | ||
|
|
c4db94e86f | ||
|
|
08492b943b | ||
|
|
a1bf8ca945 | ||
|
|
6d97eb308e | ||
|
|
64fe595b5f | ||
|
|
d82b385904 | ||
|
|
95201eb757 | ||
|
|
a387907604 | ||
|
|
f16eba4855 | ||
|
|
efdd68c2b8 | ||
|
|
e927b675a4 | ||
|
|
5d9373026b | ||
|
|
48922e5293 | ||
|
|
a55548d471 | ||
|
|
f5d5f8cf67 | ||
|
|
0060691b50 | ||
|
|
5b242989da | ||
|
|
3358094319 | ||
|
|
2f9bd00d70 | ||
|
|
ed23578dcf | ||
|
|
41ecbb62a2 | ||
|
|
32ef36d7f7 | ||
|
|
50936253de | ||
|
|
3c7b6eb5c3 | ||
|
|
c28fed0893 | ||
|
|
36910a0a8e | ||
|
|
5824ab6eb5 | ||
|
|
e6ae1ddec6 | ||
|
|
2213d23115 | ||
|
|
6393cdec9b | ||
|
|
a10f573a30 | ||
|
|
9dcce15790 | ||
|
|
481c9f067c | ||
|
|
1e64e49dc3 | ||
|
|
19a2ecd281 | ||
|
|
03b02669a4 | ||
|
|
c979f02ce4 | ||
|
|
bc86c1b8fc | ||
|
|
37d0a4aad2 | ||
|
|
ff70b5c546 | ||
|
|
7daec673ba | ||
|
|
f2d07a699a | ||
|
|
721511e484 | ||
|
|
2942660201 | ||
|
|
68811eb3fc | ||
|
|
468b7319d1 | ||
|
|
5c4da77357 | ||
|
|
009a11a9f5 | ||
|
|
7057422077 | ||
|
|
ecae16b5d4 | ||
|
|
d57df84a59 | ||
|
|
146da57ba3 | ||
|
|
fd94d162ea | ||
|
|
b5abd472b0 | ||
|
|
ee4ecc0b41 | ||
|
|
04fb1f243d | ||
|
|
e5ccfa3a50 | ||
|
|
c642a35fb3 | ||
|
|
2fe353377b | ||
|
|
de1017a20a | ||
|
|
e2cd7d9f07 | ||
|
|
c3bfd1e8bf | ||
|
|
051773a084 | ||
|
|
6a2e39355c | ||
|
|
e367cb2152 | ||
|
|
bcbf596aa8 | ||
|
|
d88b04783d | ||
|
|
6d219aa701 | ||
|
|
da32845dd1 | ||
|
|
4073536d96 | ||
|
|
21f08c97a1 | ||
|
|
06223d576d | ||
|
|
5c7a9c92d1 | ||
|
|
0e8348f360 | ||
|
|
8060ed5f8e | ||
|
|
e8135fcbb4 | ||
|
|
7fccb7e03e | ||
|
|
715ddf2b8c | ||
|
|
717a5886cf | ||
|
|
407232c708 | ||
|
|
edd902397e | ||
|
|
24f5fcb5a0 | ||
|
|
8f6270723e | ||
|
|
9cccd2d74e | ||
|
|
ed34b65dbd | ||
|
|
4484cc7d16 | ||
|
|
8677994fb7 | ||
|
|
ea555eb410 | ||
|
|
e140b656a6 | ||
|
|
6423d909d7 | ||
|
|
8887961d09 | ||
|
|
4ee891a3ba | ||
|
|
96b976fd4a | ||
|
|
1b9d46f834 | ||
|
|
ba600f5da6 | ||
|
|
03e74afe80 | ||
|
|
b0c1a5a6b1 | ||
|
|
3af43164f4 | ||
|
|
2a340b7a65 | ||
|
|
2f27aeb77f | ||
|
|
e394539742 | ||
|
|
1a0f29b6ef | ||
|
|
39b160e202 | ||
|
|
aa065fa4d8 | ||
|
|
19ca1db674 | ||
|
|
bd416233eb | ||
|
|
7ca9d601a0 | ||
|
|
ac8988ad41 | ||
|
|
90fdc71279 | ||
|
|
5d126ff14d | ||
|
|
38e098f6c4 | ||
|
|
d3fa8a54ae | ||
|
|
a58384d8d1 | ||
|
|
8e1951ba67 | ||
|
|
3dba731dd7 | ||
|
|
f8f19efcaa | ||
|
|
980fdf5ad1 | ||
|
|
443094d282 | ||
|
|
e727f1749f | ||
|
|
1224a54884 | ||
|
|
897e7dccc6 | ||
|
|
88631ed7a8 | ||
|
|
65f4094b5a | ||
|
|
99656357b1 | ||
|
|
d33ae2a50a | ||
|
|
f419430c6b | ||
|
|
e127855d84 | ||
|
|
8ffea70b2f | ||
|
|
3fbddd2e41 | ||
|
|
5a17697e7e | ||
|
|
6f66fdc6d6 | ||
|
|
a29fdb43c9 | ||
|
|
7462e66858 | ||
|
|
d9364cf60a | ||
|
|
b0375bddd1 | ||
|
|
56795afabb | ||
|
|
acf9f464f0 | ||
|
|
74c615f156 | ||
|
|
133af4ea2c | ||
|
|
4182c79354 | ||
|
|
759d986188 | ||
|
|
600ca90fc0 | ||
|
|
3b52888877 | ||
|
|
e23120a4c6 | ||
|
|
38d47f6aa1 | ||
|
|
3a25123d7b | ||
|
|
19c04e125a | ||
|
|
8a13387874 | ||
|
|
6c813ddab1 | ||
|
|
965870f8ed | ||
|
|
8add76fb50 | ||
|
|
af4ec3d096 | ||
|
|
b908cd7cbd | ||
|
|
6309ec0bfa | ||
|
|
45d1bef302 | ||
|
|
7d9e1aad83 | ||
|
|
aa2d1f3bc9 | ||
|
|
f6bd1ef513 | ||
|
|
c75ac3c0f5 | ||
|
|
ac2d6fe9a8 | ||
|
|
40d120d875 | ||
|
|
5e2fdcf1e9 | ||
|
|
57c8c24b20 | ||
|
|
460d3fe111 | ||
|
|
1d50cbf684 | ||
|
|
3a250d3da4 | ||
|
|
d05b819ff2 | ||
|
|
fcb3f9ca26 | ||
|
|
69b42ee6e0 | ||
|
|
77973f0037 | ||
|
|
7f8f39916d | ||
|
|
cc8bf6744b | ||
|
|
bf17d6894f | ||
|
|
900f5b136f | ||
|
|
194a9c0c40 | ||
|
|
f6f9b768cc | ||
|
|
8fe0148821 | ||
|
|
28ed2fe8f2 | ||
|
|
131df2d82a | ||
|
|
0f5ec6c40c | ||
|
|
39cbadb100 | ||
|
|
0afb503860 | ||
|
|
ed1c589e2d | ||
|
|
864cc7a7bb | ||
|
|
657a374895 | ||
|
|
35e1ce0df2 | ||
|
|
2065ff80ff | ||
|
|
b24bba87d9 | ||
|
|
5583fc76f3 | ||
|
|
e810445ac9 | ||
|
|
5afbb4ee4e | ||
|
|
6ae4cd143c | ||
|
|
978329fdc9 |
@@ -6,8 +6,8 @@ version: 2
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:12.16.3
|
- image: circleci/node:16.10.0
|
||||||
- image: circleci/mongo:3.4-jessie
|
- image: circleci/mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/repo
|
working_directory: ~/repo
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
root : true,
|
root : true,
|
||||||
parserOptions : {
|
parserOptions : {
|
||||||
ecmaVersion : 9,
|
ecmaVersion : 2021,
|
||||||
sourceType : 'module',
|
sourceType : 'module',
|
||||||
ecmaFeatures : {
|
ecmaFeatures : {
|
||||||
jsx : true
|
jsx : true
|
||||||
@@ -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',
|
||||||
@@ -55,7 +55,7 @@ module.exports = {
|
|||||||
'array-bracket-spacing' : ['warn', 'never'],
|
'array-bracket-spacing' : ['warn', 'never'],
|
||||||
'arrow-spacing' : ['warn', { before: false, after: false }],
|
'arrow-spacing' : ['warn', { before: false, after: false }],
|
||||||
'comma-spacing' : ['warn', { before: false, after: true }],
|
'comma-spacing' : ['warn', { before: false, after: true }],
|
||||||
'indent' : ['warn', 'tab'],
|
'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }],
|
||||||
'keyword-spacing' : ['warn', {
|
'keyword-spacing' : ['warn', {
|
||||||
before : true,
|
before : true,
|
||||||
after : true,
|
after : true,
|
||||||
|
|||||||
69
.github/dependabot.yml
vendored
Normal file
69
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
open-pull-requests-limit: 99
|
||||||
|
ignore:
|
||||||
|
- dependency-name: eslint
|
||||||
|
versions:
|
||||||
|
- 7.19.0
|
||||||
|
- 7.22.0
|
||||||
|
- 7.23.0
|
||||||
|
- 7.24.0
|
||||||
|
- dependency-name: "@babel/core"
|
||||||
|
versions:
|
||||||
|
- 7.12.13
|
||||||
|
- 7.12.16
|
||||||
|
- 7.12.17
|
||||||
|
- 7.13.13
|
||||||
|
- 7.13.14
|
||||||
|
- 7.13.15
|
||||||
|
- dependency-name: googleapis
|
||||||
|
versions:
|
||||||
|
- 68.0.0
|
||||||
|
- 70.0.0
|
||||||
|
- 71.0.0
|
||||||
|
- dependency-name: "@babel/preset-env"
|
||||||
|
versions:
|
||||||
|
- 7.12.13
|
||||||
|
- 7.12.16
|
||||||
|
- 7.12.17
|
||||||
|
- 7.13.0
|
||||||
|
- 7.13.12
|
||||||
|
- 7.13.8
|
||||||
|
- dependency-name: mongoose
|
||||||
|
versions:
|
||||||
|
- 5.11.14
|
||||||
|
- 5.11.15
|
||||||
|
- 5.11.16
|
||||||
|
- 5.11.17
|
||||||
|
- 5.11.18
|
||||||
|
- 5.11.19
|
||||||
|
- 5.12.1
|
||||||
|
- 5.12.2
|
||||||
|
- 5.12.3
|
||||||
|
- dependency-name: eslint-plugin-react
|
||||||
|
versions:
|
||||||
|
- 7.23.0
|
||||||
|
- 7.23.1
|
||||||
|
- dependency-name: query-string
|
||||||
|
versions:
|
||||||
|
- 7.0.0
|
||||||
|
- dependency-name: nanoid
|
||||||
|
versions:
|
||||||
|
- 3.1.22
|
||||||
|
- dependency-name: "@babel/preset-react"
|
||||||
|
versions:
|
||||||
|
- 7.13.13
|
||||||
|
- dependency-name: codemirror
|
||||||
|
versions:
|
||||||
|
- 5.59.3
|
||||||
|
- 5.60.0
|
||||||
|
- dependency-name: classnames
|
||||||
|
versions:
|
||||||
|
- 2.3.0
|
||||||
|
- dependency-name: marked
|
||||||
|
versions:
|
||||||
|
- 1.2.8
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ storage
|
|||||||
*.log
|
*.log
|
||||||
build/*
|
build/*
|
||||||
config/local.*
|
config/local.*
|
||||||
|
config/docker.*
|
||||||
|
|
||||||
todo.md
|
todo.md
|
||||||
startDB.bat
|
startDB.bat
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM node:8
|
FROM node:16.11-alpine
|
||||||
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
|
|
||||||
|
|||||||
103
README.md
103
README.md
@@ -1,45 +1,93 @@
|
|||||||
# The Homebrewery
|
# The Homebrewery
|
||||||
The Homebrewery is a tool for making authentic looking [D&D content](https://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook) using [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). It is distributed under the terms of the [MIT License](./license).
|
|
||||||
|
[](https://app.circleci.com/pipelines/github/naturalcrit/homebrewery?branch=master)
|
||||||
|
|
||||||
|
The Homebrewery is a tool for making authentic looking [D&D content][dnd-content-url]
|
||||||
|
using [Markdown][markdown-url]. It is distributed under the terms of the [MIT License](./license).
|
||||||
|
|
||||||
|
[dnd-content-url]: https://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook
|
||||||
|
[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 [on our website](https://homebrewery.naturalcrit.com). 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 your own local version for testing by following the installation instructions below.
|
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
|
||||||
|
clone it, 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
|
||||||
|
below.
|
||||||
|
|
||||||
|
[homebrewery-url]: https://homebrewery.naturalcrit.com
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
First, install two programs that the Homebrewery requires to run.
|
First, install three programs that the Homebrewery requires to run and retrieve
|
||||||
|
updates:
|
||||||
|
|
||||||
1. install [node](https://nodejs.org/en/)
|
1. install [node](https://nodejs.org/en/)
|
||||||
1. install [mongodb](https://www.mongodb.com/)
|
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
||||||
|
|
||||||
Second, download a copy of the repository. If you have git you can do so with
|
For easiest installation, follow these steps:
|
||||||
|
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. Go to the C drive and create a folder called "data"
|
||||||
|
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. 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. 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 `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. 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 "OK", "OK", "OK" 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)
|
||||||
|
|
||||||
|
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
||||||
```
|
```
|
||||||
git clone https://github.com/naturalcrit/homebrewery.git
|
git clone https://github.com/naturalcrit/homebrewery.git
|
||||||
```
|
```
|
||||||
|
|
||||||
Third, you will need to add the environment variable `NODE_ENV = local` to allow the project to run locally.
|
[github-clone-repo-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/cloning-a-repository
|
||||||
|
|
||||||
|
Second, you will need to add the environment variable `NODE_ENV=local` to allow
|
||||||
|
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 / OSX: `export NODE_ENV=local`
|
||||||
|
|
||||||
Fourth, you will need to install the program and run it using the two commands:
|
Third, you will need to install the Node dependencies, compile the app, and run
|
||||||
|
it using the two commands:
|
||||||
|
|
||||||
1. `npm install`
|
1. `npm install`
|
||||||
1. `npm start`
|
1. `npm start`
|
||||||
|
|
||||||
You should now be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use the Homebrewery offline.
|
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
||||||
|
in your browser and use the Homebrewery offline.
|
||||||
|
|
||||||
### Running the application via Docker
|
### Running the application via Docker
|
||||||
|
|
||||||
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
||||||
|
|
||||||
### Standalone PHB Stylesheet
|
### Running the application on FreeBSD or FreeNAS
|
||||||
If you just want the stylesheet that is generated to make pages look like they are from the Player's Handbook, you will find it in the [phb.standalone.css](./phb.standalone.css) file.
|
|
||||||
|
|
||||||
If you are developing locally and would like to generate your own, follow the above steps and then run `npm run phb`.
|
Please see the docs here: [README.FreeBSD.md](./README.FREEBSD.md)
|
||||||
|
|
||||||
|
### Standalone PHB Stylesheet
|
||||||
|
If you just want the stylesheet that is generated to make pages look like they
|
||||||
|
are from the Player's Handbook, you will find it in the
|
||||||
|
[phb.standalone.css](./phb.standalone.css) file.
|
||||||
|
|
||||||
|
If you are developing locally and would like to generate your own, follow the
|
||||||
|
above steps and then run `npm run phb`.
|
||||||
|
|
||||||
## Issues, Suggestions, and Bugs
|
## Issues, Suggestions, and Bugs
|
||||||
If you run into any issues using The Homebrewery or have suggestions for improvement, please submit an issue [on GitHub](/issues). You can also get help for issues on the subreddit [r/homebrewery](https://www.reddit.com/r/homebrewery)
|
If you run into any issues using The Homebrewery or have suggestions for
|
||||||
|
improvement, please submit an issue [on GitHub][repo-issues-url].
|
||||||
|
You can also get help for issues on the subreddit [r/homebrewery][subreddit-url]
|
||||||
|
|
||||||
|
[repo-issues-url]: https://github.com/naturalcrit/homebrewery/issues
|
||||||
|
[subreddit-url]: https://www.reddit.com/r/homebrewery
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
@@ -47,6 +95,33 @@ You can check out the [changelog](./changelog.md).
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
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 that you made it yourself.
|
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
|
||||||
|
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's created on this site,
|
||||||
|
it's your responsibility to ensure you have the proper licenses/rights for any
|
||||||
|
images or resources used.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
You are welcome to contribute to the development and maintenance of the
|
||||||
|
project! There are several ways of doing that:
|
||||||
|
- 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
|
||||||
|
to help you can [mark duplicates][github-mark-duplicate-url], try to
|
||||||
|
reproduce some complex or weird issues, try with finding a workaround for a
|
||||||
|
reported bug or just mention issue managers team to let them know about
|
||||||
|
outdated issue via `@naturalcrit/issue-managers`.
|
||||||
|
- Our [subreddit][subreddit-url] is constantly growing and there are number of
|
||||||
|
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
|
||||||
|
feature by yourself, we are waiting for your
|
||||||
|
[pull requests][github-pr-docs-url]!
|
||||||
|
|
||||||
|
Anyway, if you would like to get in touch with the team and discuss/coordinate
|
||||||
|
your contribution to the project, please join our [gitter chat][gitter-url].
|
||||||
|
|
||||||
|
[github-mark-duplicate-url]: https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/about-duplicate-issues-and-pull-requests
|
||||||
|
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
|
||||||
|
[gitter-url]: https://gitter.im/naturalcrit/Lobby
|
||||||
|
|||||||
566
changelog.md
566
changelog.md
@@ -1,4 +1,529 @@
|
|||||||
# changelog
|
```css
|
||||||
|
h5 {
|
||||||
|
font-size: .35cm !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## changelog
|
||||||
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### 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 `CTRL F / CTRL SHIFT 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
|
||||||
|
|
||||||
|
- Background work to allow new themes in the future
|
||||||
|
- Fixed cursor getting stuck when resizing divider bar
|
||||||
|
|
||||||
|
##### G-Ambatte :
|
||||||
|
- Fix Style tab not copying when Cloned To New
|
||||||
|
- Basic brew sorting on User page
|
||||||
|
- Reduced data sent on each request from server
|
||||||
|
|
||||||
|
##### Gazook89 :
|
||||||
|
- Cleaned up styling on menus
|
||||||
|
|
||||||
|
### Saturday, 28/6/2021 - v2.13.1
|
||||||
|
|
||||||
|
- Fixed the issue with new brews not saving!
|
||||||
|
|
||||||
|
### Saturday, 26/6/2021 - v2.13.0
|
||||||
|
|
||||||
|
- "Share to Reddit" button now works with Google brews
|
||||||
|
- Downloading or viewing the source of your brew will now show the contents of the Style tab at the top of the document in a backtick code fence like this:
|
||||||
|
|
||||||
|
\`\`\`css
|
||||||
|
|
||||||
|
myStyle {color: black}
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
##### G-Ambatte :
|
||||||
|
- New **Download**, **View**, and **Clone to New** buttons in the "Source" dropdown on the Share page.
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Thursday, 10/6/2021 - v2.12.0
|
||||||
|
|
||||||
|
- New "style" tab to better organize custom CSS in preparation for new themes and sharable styles.
|
||||||
|
- Your own Google brews will no longer show up in the list when viewing someone else's profile.
|
||||||
|
|
||||||
|
### Saturday, 02/5/2021 - v2.11.2
|
||||||
|
|
||||||
|
- Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery.
|
||||||
|
- Move cursor to end of snippet after insertion
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Saturday, 20/3/2021 - v2.11.1
|
||||||
|
|
||||||
|
- Warning when opening brew in your Google Drive trash
|
||||||
|
|
||||||
|
##### G-Ambatte :
|
||||||
|
- Snippet to remove drop caps (fancy first letter after title)
|
||||||
|
|
||||||
|
### Saturday, 13/3/2021 - v2.11.0
|
||||||
|
|
||||||
|
- Many background things for upcoming v3. Get pumped.
|
||||||
|
|
||||||
|
##### G-Ambatte :
|
||||||
|
- Fixed new brews failing to save when auto-generated file name is too long.
|
||||||
|
- "New" button added to the Nav bar.
|
||||||
|
- "Download" button to download your brew as a text file.
|
||||||
|
- Reduced download size and improved caching.
|
||||||
|
|
||||||
|
##### RKuerten :
|
||||||
|
- Bold and Italics hotkeys for Mac users (Cmd+B, Cmd+I)
|
||||||
|
|
||||||
|
### Friday, 25/1/2021 - v2.10.7
|
||||||
|
- Cover Page snippet now flips left-right page numbering.
|
||||||
|
- Added instructions for [installing on a FreeBSD Jail](https://github.com/naturalcrit/homebrewery/blob/master/README.FREEBSD.md).
|
||||||
|
- Fix for box-shadows breaking across columns. <br>(Thanks G-Ambatte for all of these!)
|
||||||
|
- Small user interface tweaks (Thanks Ericsheid)
|
||||||
|
|
||||||
|
### Friday, 02/1/2021 - v2.10.6
|
||||||
|
- Fixed punctuation for usernames ending with 's' on the user page. (Thanks AlexeySachkov)
|
||||||
|
- Fixed server crashes due to excessive long lines in brews
|
||||||
|
- Fixed "automated request" lockouts from Google
|
||||||
|
|
||||||
|
### Friday, 18/12/2020 - v2.10.5
|
||||||
|
- Brews now immediately save when transferring between Google Drive and Homebrewery storage.
|
||||||
|
- Added confirmation popup to clarify the transfer process.
|
||||||
|
- Brews transferred or deleted from Google will be found in your Google Drive trash.
|
||||||
|
- Dependency updates.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Thursday, 22/10/2020 - v2.10.3
|
||||||
|
- Fixed brews with broken code crashing the edit page when loaded (the "blue screen of death" bug).
|
||||||
|
|
||||||
|
### Monday, 19/10/2020 - v2.10.2
|
||||||
|
- Fixed issue with "recent" item links not updating when transferring between Google Drive.
|
||||||
|
|
||||||
|
### Monday, 12/10/2020 - v2.10.1
|
||||||
|
- Fixed issue with users unable to create new brews
|
||||||
|
- Fixing brews being lost when loaded via back button
|
||||||
|
|
||||||
|
### 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!
|
||||||
|
|
||||||
|
### Friday, 28/08/2020 - v2.9.2
|
||||||
|
- Many dependency updates
|
||||||
|
- Finally fixed this changelog page to not run off the edge :P
|
||||||
|
|
||||||
|
### Sunday, 19/07/2020 - v2.9.1
|
||||||
|
- Fixed paragraphs appearing blank on new columns
|
||||||
|
|
||||||
|
### Wednesday, 20/05/2020 - v2.9.0
|
||||||
|
- Major refactoring of site backend to work with updated dependencies for security (should be invisible to users)
|
||||||
|
|
||||||
### Wednesday, 11/03/2020 - v2.8.2
|
### Wednesday, 11/03/2020 - v2.8.2
|
||||||
- Fixed delete button removing everyone's copy for brews with multiple authors
|
- Fixed delete button removing everyone's copy for brews with multiple authors
|
||||||
@@ -16,8 +541,10 @@
|
|||||||
- "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
|
||||||
@@ -33,24 +560,17 @@
|
|||||||
### Saturday, 18/02/2017 - v2.7.2
|
### Saturday, 18/02/2017 - v2.7.2
|
||||||
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
|
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
|
||||||
|
|
||||||
## BIG NEWS
|
### Thursday, 19/01/2017 - v2.7.1
|
||||||
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues.
|
|
||||||
|
|
||||||
All brews made previous to the release of v3.0.0 will still render normally.
|
|
||||||
|
|
||||||
### Thursday, 19/01/2017 - v2.7.0
|
|
||||||
- Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282)
|
- Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282)
|
||||||
- Adding in line highlight for new pages
|
- Adding in line highlight for new pages
|
||||||
- Added in a simple brew lookup for admin
|
- Added in a simple brew lookup for admin
|
||||||
|
|
||||||
|
|
||||||
### Saturday, 14/01/2017 - v2.7.0
|
### Saturday, 14/01/2017 - v2.7.0
|
||||||
- Added a new Render Warning overlay. It detects situations where the brew may not be rendering correctly (wrong browser, browser is zoomed in...) and let's the user know
|
- Added a new Render Warning overlay. It detects situations where the brew may not be rendering correctly (wrong browser, browser is zoomed in...) and let's the user know
|
||||||
|
|
||||||
|
|
||||||
### 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
|
||||||
@@ -60,7 +580,7 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- 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
|
||||||
@@ -73,13 +593,10 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- Added a table of contents snippet (thanks u/tullisar)
|
- Added a table of contents snippet (thanks u/tullisar)
|
||||||
- Added a multicolumn snippet
|
- Added a multicolumn snippet
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Thursday, 01/12/2016
|
### Thursday, 01/12/2016
|
||||||
- Added in a snippet for a split table
|
- Added in a snippet for a split table
|
||||||
- Added an account nav item to new page
|
- Added an account nav item to new page
|
||||||
|
|
||||||
|
|
||||||
### Sunday, 27/11/2016 - v2.5.1
|
### Sunday, 27/11/2016 - v2.5.1
|
||||||
- Fixed the column rendering on the new user page. Really should have tested that better
|
- Fixed the column rendering on the new user page. Really should have tested that better
|
||||||
- Added a hover tooltip to fully read the brew description
|
- Added a hover tooltip to fully read the brew description
|
||||||
@@ -95,7 +612,6 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- Added a new user page to see others published brews, as well as all of your own brews.
|
- Added a new user page to see others published brews, as well as all of your own brews.
|
||||||
- Added a new nav item for accessing your profile and logging in
|
- Added a new nav item for accessing your profile and logging in
|
||||||
|
|
||||||
|
|
||||||
### Monday, 14/11/2016
|
### Monday, 14/11/2016
|
||||||
- Updated snippet bar style
|
- Updated snippet bar style
|
||||||
- You can now print from a new page without saving
|
- You can now print from a new page without saving
|
||||||
@@ -105,6 +621,8 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- 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.
|
||||||
|
|
||||||
@@ -120,7 +638,6 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- Fixed the noteblock overlapping into titles (thanks u/dsompura!)
|
- Fixed the noteblock overlapping into titles (thanks u/dsompura!)
|
||||||
- Fixed a bad search route in the admin panel (thanks u/SnappyTom!)
|
- Fixed a bad search route in the admin panel (thanks u/SnappyTom!)
|
||||||
|
|
||||||
|
|
||||||
### Friday, 29/07/2016 - v2.2.7
|
### Friday, 29/07/2016 - v2.2.7
|
||||||
- Adding in descriptive note blocks. (Thanks calculuschild!)
|
- Adding in descriptive note blocks. (Thanks calculuschild!)
|
||||||
|
|
||||||
@@ -133,7 +650,7 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- 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.
|
||||||
@@ -141,11 +658,10 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- 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.
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### 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!
|
||||||
@@ -161,6 +677,8 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
### Wednesday, 25/05/2016 -v2.0.5
|
### Wednesday, 25/05/2016 -v2.0.5
|
||||||
- The class table generators have the proper ability score improvement progression.
|
- The class table generators have the proper ability score improvement progression.
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Tuesday, 24/05/2016 - v2.0.4
|
### Tuesday, 24/05/2016 - v2.0.4
|
||||||
- Fixed extra wide monster stat blocks sometimes only being one column
|
- Fixed extra wide monster stat blocks sometimes only being one column
|
||||||
- The class table generators now follow the proper progression from the PHB (thakns u/IrishBandit)
|
- The class table generators now follow the proper progression from the PHB (thakns u/IrishBandit)
|
||||||
@@ -171,8 +689,6 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
|||||||
- Bumped up the allowed entity size for extra-large brew (Thanks for reporting it dickboner93)
|
- Bumped up the allowed entity size for extra-large brew (Thanks for reporting it dickboner93)
|
||||||
- Added a little error box when a save fails with a custom link to reporting the issue on github.
|
- Added a little error box when a save fails with a custom link to reporting the issue on github.
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Saturday, 14/05/2016 - v2.0.0 (finally!)
|
### Saturday, 14/05/2016 - v2.0.0 (finally!)
|
||||||
|
|
||||||
I've been working on v2 for a *very* long time. I want to thank you guys for being paitent.
|
I've been working on v2 for a *very* long time. I want to thank you guys for being paitent.
|
||||||
@@ -212,8 +728,6 @@ Massive changelog incoming:
|
|||||||
- Source now opens to it's own route `/source/:sharedId` instead of just a window. Now easier to share, and won't be blocked by some browsers.
|
- Source now opens to it's own route `/source/:sharedId` instead of just a window. Now easier to share, and won't be blocked by some browsers.
|
||||||
- Print page now auto-opens print dialog. If you want to share your print page link, just remove the `?dialog=true` parameter and it won't open the dialog.
|
- Print page now auto-opens print dialog. If you want to share your print page link, just remove the `?dialog=true` parameter and it won't open the dialog.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
\page
|
\page
|
||||||
|
|
||||||
### Wednesday, 20/04/2016
|
### Wednesday, 20/04/2016
|
||||||
@@ -273,7 +787,6 @@ Massive changelog incoming:
|
|||||||
* Increased padding on table cells
|
* Increased padding on table cells
|
||||||
* Raw html now shows in view source
|
* Raw html now shows in view source
|
||||||
|
|
||||||
|
|
||||||
## v1.0.0 - Release
|
## v1.0.0 - Release
|
||||||
|
|
||||||
### Wednesday, 3/01/2016
|
### Wednesday, 3/01/2016
|
||||||
@@ -281,4 +794,3 @@ Massive changelog incoming:
|
|||||||
* Added `phb.standalone.css` plus a build system for creating it
|
* Added `phb.standalone.css` plus a build system for creating it
|
||||||
* Added page numbers and footer text
|
* Added page numbers and footer text
|
||||||
* Page accent now flips each page
|
* Page accent now flips each page
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const Admin = createClass({
|
|||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<i className='fa fa-rocket' />
|
<i className='fas fa-rocket' />
|
||||||
homebrewery admin
|
homebrewery admin
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ const BrewCleanup = createClass({
|
|||||||
return <div className='removeBox'>
|
return <div className='removeBox'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: <span><i className='fa fa-times' /> Remove</span>
|
: <span><i className='fas fa-times' /> Remove</span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
<span>Found {this.state.count} Brews that could be removed. </span>
|
<span>Found {this.state.count} Brews that could be removed. </span>
|
||||||
@@ -59,7 +59,7 @@ const BrewCleanup = createClass({
|
|||||||
|
|
||||||
<button onClick={this.prime} className='query'>
|
<button onClick={this.prime} className='query'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: 'Query Brews'
|
: 'Query Brews'
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ const BrewCompress = createClass({
|
|||||||
return <div className='removeBox'>
|
return <div className='removeBox'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: <span><i className='fa fa-compress' /> compress </span>
|
: <span><i className='fas fa-compress' /> compress </span>
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
@@ -76,7 +76,7 @@ const BrewCompress = createClass({
|
|||||||
|
|
||||||
<button onClick={this.prime} className='query'>
|
<button onClick={this.prime} className='query'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fa fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
: 'Query Brews'
|
: 'Query Brews'
|
||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const BrewLookup = createClass({
|
|||||||
<h2>Brew Lookup</h2>
|
<h2>Brew Lookup</h2>
|
||||||
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
|
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
|
||||||
<button onClick={this.lookup}>
|
<button onClick={this.lookup}>
|
||||||
<i className={cx('fa', {
|
<i className={cx('fas', {
|
||||||
'fa-search' : !this.state.searching,
|
'fa-search' : !this.state.searching,
|
||||||
'fa-spin fa-spinner' : this.state.searching,
|
'fa-spin fa-spinner' : this.state.searching,
|
||||||
})} />
|
})} />
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const Stats = createClass({
|
|||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{this.state.fetching
|
{this.state.fetching
|
||||||
&& <div className='pending'><i className='fa fa-spin fa-spinner' /></div>
|
&& <div className='pending'><i className='fas fa-spin fa-spinner' /></div>
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,58 +4,77 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||||
|
|
||||||
//TODO: move to the brew renderer
|
//TODO: move to the brew renderer
|
||||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||||
|
const Frame = require('react-frame-component').default;
|
||||||
|
|
||||||
const PAGE_HEIGHT = 1056;
|
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 : '',
|
||||||
errors : []
|
style : '',
|
||||||
|
renderer : 'legacy',
|
||||||
|
errors : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
const pages = this.props.text.split('\\page');
|
let pages;
|
||||||
|
if(this.props.renderer == 'legacy') {
|
||||||
|
pages = this.props.text.split('\\page');
|
||||||
|
} else {
|
||||||
|
pages = this.props.text.split(/^\\page$/gm);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
viewablePageNumber : 0,
|
viewablePageNumber : 0,
|
||||||
height : 0,
|
height : 0,
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
|
|
||||||
pages : pages,
|
pages : pages,
|
||||||
usePPR : pages.length >= PPR_THRESHOLD,
|
usePPR : pages.length >= PPR_THRESHOLD,
|
||||||
|
visibility : 'hidden',
|
||||||
|
initialContent : `<!DOCTYPE html><html><head>
|
||||||
|
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||||
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
|
<link href='/homebrew/bundle.css' rel='stylesheet' />
|
||||||
|
<base target=_blank>
|
||||||
|
</head><body style='overflow: hidden'><div></div></body></html>`
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
height : 0,
|
height : 0,
|
||||||
lastRender : <div></div>,
|
lastRender : <div></div>,
|
||||||
|
|
||||||
componentDidMount : function() {
|
|
||||||
this.updateSize();
|
|
||||||
window.addEventListener('resize', this.updateSize);
|
|
||||||
},
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
window.removeEventListener('resize', this.updateSize);
|
window.removeEventListener('resize', this.updateSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps : function(nextProps) {
|
componentDidUpdate : function(prevProps) {
|
||||||
const pages = nextProps.text.split('\\page');
|
if(prevProps.text !== this.props.text) {
|
||||||
this.setState({
|
let pages;
|
||||||
pages : pages,
|
if(this.props.renderer == 'legacy') {
|
||||||
usePPR : pages.length >= PPR_THRESHOLD
|
pages = this.props.text.split('\\page');
|
||||||
});
|
} else {
|
||||||
|
pages = this.props.text.split(/^\\page$/gm);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
pages : pages,
|
||||||
|
usePPR : pages.length >= PPR_THRESHOLD
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSize : function() {
|
updateSize : function() {
|
||||||
this.setState({
|
this.setState({
|
||||||
height : this.refs.main.parentNode.clientHeight,
|
height : this.refs.main.parentNode.clientHeight,
|
||||||
isMounted : true
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -85,7 +104,7 @@ const BrewRenderer = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderPageInfo : function(){
|
renderPageInfo : function(){
|
||||||
return <div className='pageInfo'>
|
return <div className='pageInfo' ref='main'>
|
||||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
@@ -99,19 +118,33 @@ 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='fa fa-spinner fa-spin' />
|
<i className='fas fa-spinner fa-spin' />
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderStyle : function() {
|
||||||
|
if(!this.props.style) return;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />;
|
||||||
|
},
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
renderPage : function(pageText, index){
|
||||||
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
|
if(this.props.renderer == 'legacy')
|
||||||
|
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
||||||
|
else {
|
||||||
|
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(){
|
||||||
if(this.state.usePPR){
|
if(this.state.usePPR){
|
||||||
return _.map(this.state.pages, (page, index)=>{
|
return _.map(this.state.pages, (page, index)=>{
|
||||||
if(this.shouldRender(page, index)){
|
if(this.shouldRender(page, index) && typeof window !== 'undefined'){
|
||||||
return this.renderPage(page, index);
|
return this.renderPage(page, index);
|
||||||
} else {
|
} else {
|
||||||
return this.renderDummyPage(index);
|
return this.renderDummyPage(index);
|
||||||
@@ -120,29 +153,66 @@ const BrewRenderer = createClass({
|
|||||||
}
|
}
|
||||||
if(this.props.errors && this.props.errors.length) return this.lastRender;
|
if(this.props.errors && this.props.errors.length) return this.lastRender;
|
||||||
this.lastRender = _.map(this.state.pages, (page, index)=>{
|
this.lastRender = _.map(this.state.pages, (page, index)=>{
|
||||||
return this.renderPage(page, index);
|
if(typeof window !== 'undefined') {
|
||||||
|
return this.renderPage(page, index);
|
||||||
|
} else {
|
||||||
|
return this.renderDummyPage(index);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return this.lastRender;
|
return this.lastRender;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
frameDidMount : function(){ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
|
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||||
|
this.updateSize();
|
||||||
|
window.addEventListener('resize', this.updateSize);
|
||||||
|
this.renderPages(); //Make sure page is renderable before showing
|
||||||
|
this.setState({
|
||||||
|
isMounted : true,
|
||||||
|
visibility : 'visible'
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
|
//render in iFrame so broken code doesn't crash the site.
|
||||||
|
//Also render dummy page while iframe is mounting.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<div className='brewRenderer'
|
{!this.state.isMounted
|
||||||
onScroll={this.handleScroll}
|
? <div className='brewRenderer' onScroll={this.handleScroll}>
|
||||||
ref='main'
|
<div className='pages' ref='pages'>
|
||||||
style={{ height: this.state.height }}>
|
{this.renderDummyPage(1)}
|
||||||
|
</div>
|
||||||
<ErrorBar errors={this.props.errors} />
|
|
||||||
<div className='popups'>
|
|
||||||
<RenderWarnings />
|
|
||||||
<NotificationPopup />
|
|
||||||
</div>
|
</div>
|
||||||
|
: null}
|
||||||
|
|
||||||
<div className='pages' ref='pages'>
|
<Frame initialContent={this.state.initialContent}
|
||||||
{this.renderPages()}
|
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
||||||
|
contentDidMount={this.frameDidMount}>
|
||||||
|
<div className={'brewRenderer'}
|
||||||
|
onScroll={this.handleScroll}
|
||||||
|
style={{ height: this.state.height }}>
|
||||||
|
|
||||||
|
<ErrorBar errors={this.props.errors} />
|
||||||
|
<div className='popups'>
|
||||||
|
<RenderWarnings />
|
||||||
|
<NotificationPopup />
|
||||||
|
</div>
|
||||||
|
<link href={`${this.props.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||||
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
|
{this.state.isMounted
|
||||||
|
&&
|
||||||
|
<>
|
||||||
|
{this.renderStyle()}
|
||||||
|
<div className='pages' ref='pages'>
|
||||||
|
{this.renderPages()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</Frame>
|
||||||
{this.renderPageInfo()}
|
{this.renderPageInfo()}
|
||||||
{this.renderPPRmsg()}
|
{this.renderPPRmsg()}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
|
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||||
|
|
||||||
@import (less) './client/homebrew/phbStyle/phb.style.less';
|
|
||||||
.pane{
|
|
||||||
position : relative;
|
|
||||||
}
|
|
||||||
.brewRenderer{
|
.brewRenderer{
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
.pages{
|
.pages{
|
||||||
margin : 30px 0px;
|
margin : 30px 0px;
|
||||||
&>.phb{
|
&>.page{
|
||||||
margin-right : auto;
|
margin-right : auto;
|
||||||
margin-bottom : 30px;
|
margin-bottom : 30px;
|
||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
@@ -16,6 +13,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.pane{
|
||||||
|
position : relative;
|
||||||
|
}
|
||||||
.pageInfo{
|
.pageInfo{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
right : 17px;
|
right : 17px;
|
||||||
|
|||||||
@@ -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 : []
|
||||||
@@ -62,7 +63,7 @@ const ErrorBar = createClass({
|
|||||||
if(!this.props.errors.length) return null;
|
if(!this.props.errors.length) return null;
|
||||||
|
|
||||||
return <div className='errorBar'>
|
return <div className='errorBar'>
|
||||||
<i className='fa fa-exclamation-triangle' />
|
<i className='fas fa-exclamation-triangle' />
|
||||||
<h3> There are HTML errors in your markup</h3>
|
<h3> There are HTML errors in your markup</h3>
|
||||||
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
||||||
{this.renderErrors()}
|
{this.renderErrors()}
|
||||||
|
|||||||
@@ -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-24-19';
|
const DISMISS_KEY = 'dismiss_notification09-9-21';
|
||||||
|
|
||||||
const NotificationPopup = createClass({
|
const NotificationPopup = createClass({
|
||||||
|
displayName : 'NotificationPopup',
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
notifications : {}
|
notifications : {}
|
||||||
@@ -22,17 +23,39 @@ const NotificationPopup = createClass({
|
|||||||
notifications : {
|
notifications : {
|
||||||
psa : function(){
|
psa : function(){
|
||||||
return <li key='psa'>
|
return <li key='psa'>
|
||||||
<em>Known bug: Grey Shadow Boxes </em> <br />
|
<em>V3.0.0 Released!</em> <br />
|
||||||
The shadows around certain brew elements such as notes and statblocks might appear as a solid grey box when generating a PDF.
|
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
|
||||||
<a target='_blank' href='https://old.reddit.com/r/homebrewery/comments/ch3v0d/psa_grey_boxesshadows_around_notes_stat_blocks_etc/'>
|
brief overview and see how to opt-in to the new features here:
|
||||||
See this Reddit post
|
<a target='_blank' href='https://homebrewery.naturalcrit.com/v3_preview'>V3 Welcome Page</a> and
|
||||||
</a> for updates and possible workarounds.
|
<a target='_blank' href='https://homebrewery.naturalcrit.com/changelog'>the Changelog</a>.
|
||||||
|
<br /><br />
|
||||||
|
<em>BE WARNED:</em> 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. 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 />
|
||||||
|
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.
|
||||||
|
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 />
|
||||||
At the moment we do not save a history of your projects, so 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!
|
||||||
@@ -55,10 +78,12 @@ const NotificationPopup = createClass({
|
|||||||
if(_.isEmpty(this.state.notifications)) return null;
|
if(_.isEmpty(this.state.notifications)) return null;
|
||||||
|
|
||||||
return <div className='notificationPopup'>
|
return <div className='notificationPopup'>
|
||||||
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
|
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||||
<i className='fa 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,29 +1,28 @@
|
|||||||
.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 : 20px;
|
padding : 15px;
|
||||||
padding-bottom : 10px;
|
padding-bottom : 10px;
|
||||||
padding-left : 85px;
|
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{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 24px;
|
top : 12px;
|
||||||
left : 24px;
|
left : 12px;
|
||||||
opacity : 0.8;
|
opacity : 0.8;
|
||||||
font-size : 2.5em;
|
font-size : 2.5em;
|
||||||
}
|
}
|
||||||
@@ -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,97 +1,179 @@
|
|||||||
|
/*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');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||||
|
|
||||||
|
const SNIPPETBAR_HEIGHT = 25;
|
||||||
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
|
/*=======--- Example CSS styling ---=======*/
|
||||||
|
/* Any CSS here will apply to your document! */
|
||||||
|
|
||||||
|
.myExampleClass {
|
||||||
|
color: black;
|
||||||
|
}`;
|
||||||
|
|
||||||
const splice = function(str, index, inject){
|
const splice = function(str, index, inject){
|
||||||
return str.slice(0, index) + inject + str.slice(index);
|
return str.slice(0, index) + inject + str.slice(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
|
||||||
|
|
||||||
const Editor = createClass({
|
const Editor = createClass({
|
||||||
|
displayName : 'Editor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
value : '',
|
brew : {
|
||||||
onChange : ()=>{},
|
text : '',
|
||||||
|
style : ''
|
||||||
|
},
|
||||||
|
|
||||||
metadata : {},
|
onTextChange : ()=>{},
|
||||||
onMetadataChange : ()=>{},
|
onStyleChange : ()=>{},
|
||||||
|
onMetaChange : ()=>{},
|
||||||
|
|
||||||
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
showMetadataEditor : false
|
view : 'text' //'text', 'style', 'meta'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
cursorPosition : {
|
|
||||||
line : 0,
|
isText : function() {return this.state.view == 'text';},
|
||||||
ch : 0
|
isStyle : function() {return this.state.view == 'style';},
|
||||||
},
|
isMeta : function() {return this.state.view == 'meta';},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
this.updateEditorSize();
|
this.updateEditorSize();
|
||||||
this.highlightPageLines();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
window.addEventListener('resize', this.updateEditorSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
window.removeEventListener('resize', this.updateEditorSize);
|
window.removeEventListener('resize', this.updateEditorSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidUpdate : function() {
|
||||||
|
this.highlightCustomMarkdown();
|
||||||
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
if(this.refs.codeEditor) {
|
||||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
||||||
|
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleTextChange : function(text){
|
|
||||||
this.props.onChange(text);
|
|
||||||
},
|
|
||||||
handleCursorActivty : function(curpos){
|
|
||||||
this.cursorPosition = curpos;
|
|
||||||
},
|
|
||||||
handleInject : function(injectText){
|
handleInject : function(injectText){
|
||||||
const lines = this.props.value.split('\n');
|
let text;
|
||||||
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
|
if(this.isText()) text = this.props.brew.text;
|
||||||
|
if(this.isStyle()) text = this.props.brew.style ?? DEFAULT_STYLE_TEXT;
|
||||||
|
|
||||||
this.handleTextChange(lines.join('\n'));
|
const lines = text.split('\n');
|
||||||
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
|
const cursorPos = this.refs.codeEditor.getCursorPosition();
|
||||||
|
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
|
||||||
|
|
||||||
|
const injectLines = injectText.split('\n');
|
||||||
|
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectLines.length, cursorPos.ch + injectLines[injectLines.length - 1].length);
|
||||||
|
|
||||||
|
if(this.isText()) this.props.onTextChange(lines.join('\n'));
|
||||||
|
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
|
||||||
},
|
},
|
||||||
handgleToggle : function(){
|
|
||||||
|
handleViewChange : function(newView){
|
||||||
this.setState({
|
this.setState({
|
||||||
showMetadataEditor : !this.state.showMetadataEditor
|
view : newView
|
||||||
});
|
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentPage : function(){
|
getCurrentPage : function(){
|
||||||
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1);
|
const lines = this.props.brew.text.split('\n').slice(0, this.cursorPosition.line + 1);
|
||||||
return _.reduce(lines, (r, line)=>{
|
return _.reduce(lines, (r, line)=>{
|
||||||
if(line.indexOf('\\page') !== -1) r++;
|
if(line.indexOf('\\page') !== -1) r++;
|
||||||
return r;
|
return r;
|
||||||
}, 1);
|
}, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
highlightPageLines : function(){
|
highlightCustomMarkdown : function(){
|
||||||
if(!this.refs.codeEditor) return;
|
if(!this.refs.codeEditor) return;
|
||||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
if(this.state.view === 'text') {
|
||||||
|
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||||
|
|
||||||
const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
|
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||||
if(line.indexOf('\\page') !== -1){
|
//reset custom text styles
|
||||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
|
||||||
r.push(lineNumber);
|
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||||
}
|
|
||||||
return r;
|
let editorPageCount = 2; // start page count from page 2
|
||||||
}, []);
|
|
||||||
return lineNumbers;
|
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
||||||
|
|
||||||
|
//reset custom line styles
|
||||||
|
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
||||||
|
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');
|
||||||
|
const pageCountElement = Object.assign(document.createElement('span'), {
|
||||||
|
className : 'editor-page-count',
|
||||||
|
textContent : editorPageCount
|
||||||
|
});
|
||||||
|
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||||
|
|
||||||
|
editorPageCount += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// New Codemirror styling for V3 renderer
|
||||||
|
if(this.props.renderer == 'V3') {
|
||||||
|
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])* *$|^ *}}$/);
|
||||||
|
if(match)
|
||||||
|
endCh = match.index+match[0].length;
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
brewJump : function(){
|
brewJump : function(){
|
||||||
const currentPage = this.getCurrentPage();
|
const currentPage = this.getCurrentPage();
|
||||||
window.location.hash = `p${currentPage}`;
|
window.location.hash = `p${currentPage}`;
|
||||||
@@ -99,40 +181,78 @@ const Editor = createClass({
|
|||||||
|
|
||||||
//Called when there are changes to the editor's dimensions
|
//Called when there are changes to the editor's dimensions
|
||||||
update : function(){
|
update : function(){
|
||||||
this.refs.codeEditor.updateSize();
|
this.refs.codeEditor?.updateSize();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderMetadataEditor : function(){
|
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
||||||
if(!this.state.showMetadataEditor) return;
|
rerenderParent : function (){
|
||||||
return <MetadataEditor
|
this.forceUpdate();
|
||||||
metadata={this.props.metadata}
|
},
|
||||||
onChange={this.props.onMetadataChange}
|
|
||||||
/>;
|
renderEditor : function(){
|
||||||
|
if(this.isText()){
|
||||||
|
return <>
|
||||||
|
<CodeEditor key='codeEditor'
|
||||||
|
ref='codeEditor'
|
||||||
|
language='gfm'
|
||||||
|
view={this.state.view}
|
||||||
|
value={this.props.brew.text}
|
||||||
|
onChange={this.props.onTextChange}
|
||||||
|
rerenderParent={this.rerenderParent} />
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
if(this.isStyle()){
|
||||||
|
return <>
|
||||||
|
<CodeEditor key='codeEditor'
|
||||||
|
ref='codeEditor'
|
||||||
|
language='css'
|
||||||
|
view={this.state.view}
|
||||||
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
|
onChange={this.props.onStyleChange}
|
||||||
|
enableFolding={false}
|
||||||
|
rerenderParent={this.rerenderParent} />
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
if(this.isMeta()){
|
||||||
|
return <>
|
||||||
|
<CodeEditor key='codeEditor'
|
||||||
|
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.highlightPageLines();
|
|
||||||
return (
|
return (
|
||||||
<div className='editor' ref='main'>
|
<div className='editor' ref='main'>
|
||||||
<SnippetBar
|
<SnippetBar
|
||||||
brew={this.props.value}
|
brew={this.props.brew}
|
||||||
|
view={this.state.view}
|
||||||
|
onViewChange={this.handleViewChange}
|
||||||
onInject={this.handleInject}
|
onInject={this.handleInject}
|
||||||
onToggle={this.handgleToggle}
|
showEditButtons={this.props.showEditButtons}
|
||||||
showmeta={this.state.showMetadataEditor} />
|
renderer={this.props.renderer}
|
||||||
{this.renderMetadataEditor()}
|
undo={this.undo}
|
||||||
<CodeEditor
|
redo={this.redo}
|
||||||
ref='codeEditor'
|
historySize={this.historySize()} />
|
||||||
wrap={true}
|
|
||||||
language='gfm'
|
|
||||||
value={this.props.value}
|
|
||||||
onChange={this.handleTextChange}
|
|
||||||
onCursorActivity={this.handleCursorActivty} />
|
|
||||||
|
|
||||||
{/*
|
{this.renderEditor()}
|
||||||
<div className='brewJump' onClick={this.brewJump}>
|
|
||||||
<i className='fa fa-arrow-right' />
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +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{
|
||||||
|
font-style : italic;
|
||||||
|
color : grey;
|
||||||
|
background-color : fade(#299, 15%);
|
||||||
|
border-bottom : #299 solid 1px;
|
||||||
|
}
|
||||||
|
.block{
|
||||||
|
color : purple;
|
||||||
|
font-weight : bold;
|
||||||
|
//font-style: italic;
|
||||||
|
}
|
||||||
|
.inline-block{
|
||||||
|
color : red;
|
||||||
|
font-weight : bold;
|
||||||
|
//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 : {
|
||||||
@@ -17,7 +18,8 @@ const MetadataEditor = createClass({
|
|||||||
tags : '',
|
tags : '',
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : []
|
systems : [],
|
||||||
|
renderer : 'legacy'
|
||||||
},
|
},
|
||||||
onChange : ()=>{}
|
onChange : ()=>{}
|
||||||
};
|
};
|
||||||
@@ -36,6 +38,12 @@ const MetadataEditor = createClass({
|
|||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
|
handleRenderer : function(renderer, e){
|
||||||
|
if(e.target.checked){
|
||||||
|
this.props.metadata.renderer = renderer;
|
||||||
|
}
|
||||||
|
this.props.onChange(this.props.metadata);
|
||||||
|
},
|
||||||
handlePublish : function(val){
|
handlePublish : function(val){
|
||||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
this.props.onChange(_.merge({}, this.props.metadata, {
|
||||||
published : val
|
published : val
|
||||||
@@ -43,7 +51,7 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleDelete : function(){
|
handleDelete : function(){
|
||||||
if(this.props.metadata.authors.length <= 1){
|
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
|
||||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||||
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||||
} else {
|
} else {
|
||||||
@@ -58,16 +66,6 @@ const MetadataEditor = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getRedditLink : function(){
|
|
||||||
const meta = this.props.metadata;
|
|
||||||
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](http://homebrewery.naturalcrit.com/share/${meta.shareId})**`;
|
|
||||||
|
|
||||||
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}>
|
||||||
@@ -83,11 +81,11 @@ const MetadataEditor = createClass({
|
|||||||
renderPublish : function(){
|
renderPublish : function(){
|
||||||
if(this.props.metadata.published){
|
if(this.props.metadata.published){
|
||||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||||
<i className='fa fa-ban' /> unpublish
|
<i className='fas fa-ban' /> unpublish
|
||||||
</button>;
|
</button>;
|
||||||
} else {
|
} else {
|
||||||
return <button className='publish' onClick={()=>this.handlePublish(true)}>
|
return <button className='publish' onClick={()=>this.handlePublish(true)}>
|
||||||
<i className='fa fa-globe' /> publish
|
<i className='fas fa-globe' /> publish
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -99,7 +97,7 @@ const MetadataEditor = createClass({
|
|||||||
<label>delete</label>
|
<label>delete</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<button className='publish' onClick={this.handleDelete}>
|
<button className='publish' onClick={this.handleDelete}>
|
||||||
<i className='fa fa-trash' /> delete brew
|
<i className='fas fa-trash-alt' /> delete brew
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
@@ -107,7 +105,7 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
renderAuthors : function(){
|
renderAuthors : function(){
|
||||||
let text = 'None.';
|
let text = 'None.';
|
||||||
if(this.props.metadata.authors.length){
|
if(this.props.metadata.authors && this.props.metadata.authors.length){
|
||||||
text = this.props.metadata.authors.join(', ');
|
text = this.props.metadata.authors.join(', ');
|
||||||
}
|
}
|
||||||
return <div className='field authors'>
|
return <div className='field authors'>
|
||||||
@@ -118,16 +116,34 @@ const MetadataEditor = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderShareToReddit : function(){
|
renderRenderOptions : function(){
|
||||||
if(!this.props.metadata.shareId) return;
|
if(!global.enable_v3) return;
|
||||||
|
|
||||||
return <div className='field reddit'>
|
return <div className='field systems'>
|
||||||
<label>reddit</label>
|
<label>Renderer</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
|
<label key='legacy'>
|
||||||
<button className='publish'>
|
<input
|
||||||
<i className='fa fa-reddit-alien' /> share to reddit
|
type='radio'
|
||||||
</button>
|
value = 'legacy'
|
||||||
|
name = 'renderer'
|
||||||
|
checked={this.props.metadata.renderer === 'legacy'}
|
||||||
|
onChange={(e)=>this.handleRenderer('legacy', e)} />
|
||||||
|
Legacy
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label key='V3'>
|
||||||
|
<input
|
||||||
|
type='radio'
|
||||||
|
value = 'V3'
|
||||||
|
name = 'renderer'
|
||||||
|
checked={this.props.metadata.renderer === 'V3'}
|
||||||
|
onChange={(e)=>this.handleRenderer('V3', e)} />
|
||||||
|
V3
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<a href='/v3_preview' target='_blank' rel='noopener noreferrer'>
|
||||||
|
Click here for a quick intro to V3!
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
@@ -154,6 +170,8 @@ const MetadataEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
|
{this.renderAuthors()}
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -161,7 +179,7 @@ const MetadataEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderRenderOptions()}
|
||||||
|
|
||||||
<div className='field publish'>
|
<div className='field publish'>
|
||||||
<label>publish</label>
|
<label>publish</label>
|
||||||
@@ -171,8 +189,6 @@ const MetadataEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this.renderShareToReddit()}
|
|
||||||
|
|
||||||
{this.renderDelete()}
|
{this.renderDelete()}
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.8em;
|
line-height : 1.8em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
flex-grow : 0;
|
flex : 0 0 auto;
|
||||||
}
|
}
|
||||||
&>.value{
|
&>.value{
|
||||||
flex-grow : 1;
|
flex : 1 1 auto;
|
||||||
|
min-width : 200px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.description.field textarea.value{
|
.description.field textarea.value{
|
||||||
@@ -38,15 +39,27 @@
|
|||||||
font-size : 0.7em;
|
font-size : 0.7em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
user-select : none;
|
user-select : none;
|
||||||
|
white-space : nowrap;
|
||||||
|
display : inline-flex;
|
||||||
|
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;
|
||||||
|
margin : 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.publish.field .value{
|
.publish.field .value{
|
||||||
position : relative;
|
position : relative;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
|
button{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
button.publish{
|
button.publish{
|
||||||
.button(@blueLight);
|
.button(@blueLight);
|
||||||
}
|
}
|
||||||
@@ -54,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;
|
||||||
}
|
}
|
||||||
@@ -67,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;
|
||||||
|
|||||||
@@ -5,22 +5,34 @@ const _ = require('lodash');
|
|||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
|
||||||
const Snippets = require('./snippets/snippets.js');
|
const SnippetsLegacy = require('./snippetsLegacy/snippets.js');
|
||||||
|
const SnippetsV3 = require('./snippets/snippets.js');
|
||||||
|
|
||||||
const execute = function(val, brew){
|
const execute = function(val, brew){
|
||||||
if(_.isFunction(val)) return val(brew);
|
if(_.isFunction(val)) return val(brew);
|
||||||
return val;
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Snippetbar = createClass({
|
const Snippetbar = createClass({
|
||||||
|
displayName : 'SnippetBar',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : '',
|
brew : {},
|
||||||
onInject : ()=>{},
|
view : 'text',
|
||||||
onToggle : ()=>{},
|
onViewChange : ()=>{},
|
||||||
showmeta : false
|
onInject : ()=>{},
|
||||||
|
onToggle : ()=>{},
|
||||||
|
showEditButtons : true,
|
||||||
|
renderer : 'legacy',
|
||||||
|
undo : ()=>{},
|
||||||
|
redo : ()=>{},
|
||||||
|
historySize : ()=>{}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
renderer : this.props.renderer
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -29,7 +41,14 @@ const Snippetbar = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
return _.map(Snippets, (snippetGroup)=>{
|
let snippets = [];
|
||||||
|
|
||||||
|
if(this.props.renderer === 'V3')
|
||||||
|
snippets = SnippetsV3.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
|
else
|
||||||
|
snippets = SnippetsLegacy.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
|
|
||||||
|
return _.map(snippets, (snippetGroup)=>{
|
||||||
return <SnippetGroup
|
return <SnippetGroup
|
||||||
brew={this.props.brew}
|
brew={this.props.brew}
|
||||||
groupName={snippetGroup.groupName}
|
groupName={snippetGroup.groupName}
|
||||||
@@ -41,13 +60,38 @@ const Snippetbar = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderEditorButtons : function(){
|
||||||
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
|
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' })}
|
||||||
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
|
<i className='fa fa-beer' />
|
||||||
|
</div>
|
||||||
|
<div className={cx('style', { selected: this.props.view === 'style' })}
|
||||||
|
onClick={()=>this.props.onViewChange('style')}>
|
||||||
|
<i className='fa fa-paint-brush' />
|
||||||
|
</div>
|
||||||
|
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
||||||
|
onClick={()=>this.props.onViewChange('meta')}>
|
||||||
|
<i className='fas fa-info-circle' />
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='snippetBar'>
|
return <div className='snippetBar'>
|
||||||
{this.renderSnippetGroups()}
|
{this.renderSnippetGroups()}
|
||||||
<div className={cx('toggleMeta', { selected: this.props.showmeta })}
|
{this.renderEditorButtons()}
|
||||||
onClick={this.props.onToggle}>
|
|
||||||
<i className='fa fa-bars' />
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -60,11 +104,12 @@ module.exports = Snippetbar;
|
|||||||
|
|
||||||
|
|
||||||
const SnippetGroup = createClass({
|
const SnippetGroup = createClass({
|
||||||
|
displayName : 'SnippetGroup',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : '',
|
brew : {},
|
||||||
groupName : '',
|
groupName : '',
|
||||||
icon : 'fa-rocket',
|
icon : 'fas fa-rocket',
|
||||||
snippets : [],
|
snippets : [],
|
||||||
onSnippetClick : function(){},
|
onSnippetClick : function(){},
|
||||||
};
|
};
|
||||||
@@ -75,16 +120,16 @@ const SnippetGroup = createClass({
|
|||||||
renderSnippets : function(){
|
renderSnippets : function(){
|
||||||
return _.map(this.props.snippets, (snippet)=>{
|
return _.map(this.props.snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
|
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
|
||||||
<i className={`fa fa-fw ${snippet.icon}`} />
|
<i className={snippet.icon} />
|
||||||
{snippet.name}
|
{snippet.name}
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='snippetGroup'>
|
return <div className='snippetGroup snippetBarButton'>
|
||||||
<div className='text'>
|
<div className='text'>
|
||||||
<i className={`fa fa-fw ${this.props.icon}`} />
|
<i className={this.props.icon} />
|
||||||
<span className='groupName'>{this.props.groupName}</span>
|
<span className='groupName'>{this.props.groupName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='dropdown'>
|
<div className='dropdown'>
|
||||||
|
|||||||
@@ -1,47 +1,87 @@
|
|||||||
|
|
||||||
.snippetBar{
|
.snippetBar{
|
||||||
@height : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
height : @height;
|
height : @menuHeight;
|
||||||
background-color : #ddd;
|
background-color : #ddd;
|
||||||
.toggleMeta{
|
.editors{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 0px;
|
display : flex;
|
||||||
right : 0px;
|
top : 0px;
|
||||||
height : @height;
|
right : 0px;
|
||||||
width : @height;
|
height : @menuHeight;
|
||||||
cursor : pointer;
|
width : 125px;
|
||||||
line-height : @height;
|
justify-content : space-between;
|
||||||
text-align : center;
|
&>div{
|
||||||
.tooltipLeft("Edit Brew Metadata");
|
height : @menuHeight;
|
||||||
|
width : @menuHeight;
|
||||||
|
cursor : pointer;
|
||||||
|
line-height : @menuHeight;
|
||||||
|
text-align : center;
|
||||||
|
&:hover,&.selected{
|
||||||
|
background-color : #999;
|
||||||
|
}
|
||||||
|
&.text{
|
||||||
|
.tooltipLeft('Brew Editor');
|
||||||
|
}
|
||||||
|
&.style{
|
||||||
|
.tooltipLeft('Style Editor');
|
||||||
|
}
|
||||||
|
&.meta{
|
||||||
|
.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{
|
||||||
|
height : @menuHeight;
|
||||||
|
line-height : @menuHeight;
|
||||||
|
display : inline-block;
|
||||||
|
padding : 0px 5px;
|
||||||
|
font-weight : 800;
|
||||||
|
font-size : 0.625em;
|
||||||
|
text-transform : uppercase;
|
||||||
|
cursor : pointer;
|
||||||
&:hover, &.selected{
|
&:hover, &.selected{
|
||||||
background-color : #999;
|
background-color : #999;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.snippetGroup{
|
|
||||||
display : inline-block;
|
|
||||||
height : @height;
|
|
||||||
padding : 0px 5px;
|
|
||||||
cursor : pointer;
|
|
||||||
font-size : 0.6em;
|
|
||||||
font-weight : 800;
|
|
||||||
line-height : @height;
|
|
||||||
text-transform : uppercase;
|
|
||||||
border-right : 1px solid black;
|
|
||||||
i{
|
i{
|
||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
margin-right : 3px;
|
margin-right : 3px;
|
||||||
font-size : 1.2em;
|
font-size : 1.4em;
|
||||||
}
|
|
||||||
&:hover, &.selected{
|
|
||||||
background-color : #999;
|
|
||||||
}
|
|
||||||
.text{
|
|
||||||
line-height : @height;
|
|
||||||
.groupName{
|
|
||||||
font-size : 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.toggleMeta{
|
||||||
|
position : absolute;
|
||||||
|
top : 0px;
|
||||||
|
right : 0px;
|
||||||
|
border-left : 1px solid black;
|
||||||
|
.tooltipLeft("Edit Brew Properties");
|
||||||
|
}
|
||||||
|
.snippetGroup{
|
||||||
|
border-right : 1px solid black;
|
||||||
&:hover{
|
&:hover{
|
||||||
.dropdown{
|
.dropdown{
|
||||||
visibility : visible;
|
visibility : visible;
|
||||||
@@ -62,7 +102,7 @@
|
|||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
i{
|
i{
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 13px;
|
font-size : 1.2em;
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color : #999;
|
background-color : #999;
|
||||||
|
|||||||
@@ -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,18 +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; }
|
||||||
|
.page:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
|
||||||
|
.page:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
|
||||||
|
.page:nth-child(2n)::after { transform: scaleX(1); }
|
||||||
|
.page:nth-child(2n+1)::after { transform: scaleX(-1); }
|
||||||
|
.page:nth-child(2n) .footnote { left: inherit; text-align: right; }
|
||||||
|
.page: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`;
|
||||||
};
|
};
|
||||||
@@ -47,20 +47,26 @@ const spellNames = [
|
|||||||
'Ultimate Rite of the Confetti Angel',
|
'Ultimate Rite of the Confetti Angel',
|
||||||
'Ultimate Ritual of Mouthwash',
|
'Ultimate Ritual of Mouthwash',
|
||||||
];
|
];
|
||||||
|
const itemNames = [
|
||||||
|
'Doorknob of Niceness',
|
||||||
|
'Paper Armor of Folding',
|
||||||
|
'Mixtape of Sadness',
|
||||||
|
'Staff of Endless Confetti',
|
||||||
|
];
|
||||||
|
|
||||||
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(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 `<div class='spellList'>\n${content}\n</div>`;
|
return `{{spellList,wide\n${content}\n}}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
spell : function(){
|
spell : function(){
|
||||||
@@ -76,16 +82,28 @@ module.exports = {
|
|||||||
return [
|
return [
|
||||||
`#### ${_.sample(spellNames)}`,
|
`#### ${_.sample(spellNames)}`,
|
||||||
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
|
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
|
||||||
'___',
|
|
||||||
'- **Casting Time:** 1 action',
|
|
||||||
`- **Range:** ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
|
|
||||||
`- **Components:** ${components}`,
|
|
||||||
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
|
|
||||||
'',
|
'',
|
||||||
'A flame, equivalent in brightness to a torch, springs from from an object that you touch. ',
|
'**Casting Time:** :: 1 action',
|
||||||
|
`**Range:** :: ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
|
||||||
|
`**Components:** :: ${components}`,
|
||||||
|
`**Duration:** :: ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
|
||||||
|
'',
|
||||||
|
'A flame, equivalent in brightness to a torch, springs from an object that you touch. ',
|
||||||
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
|
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
|
||||||
'A *continual flame* can be covered or hidden but not smothered or quenched.',
|
'A *continual flame* can be covered or hidden but not smothered or quenched.',
|
||||||
'\n\n\n'
|
'\n\n\n'
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
},
|
||||||
|
|
||||||
|
item : function() {
|
||||||
|
return [
|
||||||
|
`#### ${_.sample(itemNames)}`,
|
||||||
|
`*${_.sample(['Wondrous item', 'Armor', 'Weapon'])}, ${_.sample(['Common', 'Uncommon', 'Rare', 'Very Rare', 'Legendary', 'Artifact'])} (requires attunement)*`,
|
||||||
|
`:`,
|
||||||
|
`This knob is pretty nice. When attached to a door, it allows a user to`,
|
||||||
|
`open that door with the strength of the nearest animal. For example, if`,
|
||||||
|
`there is a cow nearby, the user will have the "strength of a cow" while`,
|
||||||
|
`opening this door.`
|
||||||
|
].join('\n');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const genList = function(list, max){
|
const genList = function(list, max){
|
||||||
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
||||||
@@ -86,7 +87,7 @@ const getAlignment = function(){
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getStats = function(){
|
const getStats = function(){
|
||||||
return `>|${_.times(6, function(){
|
return `|${_.times(6, function(){
|
||||||
const num = _.random(1, 20);
|
const num = _.random(1, 20);
|
||||||
const mod = Math.ceil(num/2 - 5);
|
const mod = Math.ceil(num/2 - 5);
|
||||||
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
||||||
@@ -95,12 +96,26 @@ const getStats = function(){
|
|||||||
|
|
||||||
const genAbilities = function(){
|
const genAbilities = function(){
|
||||||
return _.sample([
|
return _.sample([
|
||||||
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
|
'***Pack Tactics.*** These guys work together like peanut butter and jelly.',
|
||||||
'> ***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
|
'***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
|
||||||
'> ***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
|
'***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
|
||||||
'> ***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
|
'***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
|
||||||
'> ***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
'***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
||||||
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
|
'***Big Jerk.*** Whenever this creature makes an attack, it starts telling you how much cooler it is than you.',
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
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.`
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,68 +148,37 @@ const genAction = function(){
|
|||||||
'Turnbuckle Roll'
|
'Turnbuckle Roll'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
|
return `***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
full : function(){
|
monster : function(classes, genLines){
|
||||||
return `${[
|
return dedent`
|
||||||
'___',
|
{{${classes}
|
||||||
'___',
|
## ${getMonsterName()}
|
||||||
`> ## ${getMonsterName()}`,
|
*${getType()}, ${getAlignment()}*
|
||||||
`>*${getType()}, ${getAlignment()}*`,
|
___
|
||||||
'> ___',
|
**Armor Class** :: ${_.random(10, 20)} (chain mail, shield)
|
||||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
**Hit Points** :: ${_.random(1, 150)}(1d4 + 5)
|
||||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
**Speed** :: ${_.random(0, 50)}ft.
|
||||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
___
|
||||||
'>___',
|
| STR | DEX | CON | INT | WIS | CHA |
|
||||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
${getStats()}
|
||||||
getStats(),
|
___
|
||||||
'>___',
|
**Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
|
||||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
**Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)}
|
||||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
**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(3, 6), function(){
|
:
|
||||||
return genAbilities();
|
${genLongAbilities()}
|
||||||
}).join('\n>\n'),
|
### Actions
|
||||||
'> ### Actions',
|
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n:\n')}
|
||||||
_.times(_.random(4, 6), function(){
|
}}
|
||||||
return genAction();
|
\n`;
|
||||||
}).join('\n>\n'),
|
|
||||||
].join('\n')}\n\n\n`;
|
|
||||||
},
|
|
||||||
|
|
||||||
half : function(){
|
|
||||||
return `${[
|
|
||||||
'___',
|
|
||||||
`> ## ${getMonsterName()}`,
|
|
||||||
`>*${getType()}, ${getAlignment()}*`,
|
|
||||||
'> ___',
|
|
||||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
|
||||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
|
||||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
|
||||||
'>___',
|
|
||||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
|
||||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
|
||||||
getStats(),
|
|
||||||
'>___',
|
|
||||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
|
||||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
|
||||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
|
||||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
|
||||||
'> ___',
|
|
||||||
_.times(_.random(2, 3), function(){
|
|
||||||
return genAbilities();
|
|
||||||
}).join('\n>\n'),
|
|
||||||
'> ### Actions',
|
|
||||||
_.times(_.random(1, 2), function(){
|
|
||||||
return genAction();
|
|
||||||
}).join('\n>\n'),
|
|
||||||
].join('\n')}\n\n\n`;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,79 +6,157 @@ 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;
|
||||||
|
const watercolorGen = require('./watercolor.gen.js');
|
||||||
|
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Editor',
|
groupName : 'Text Editor',
|
||||||
icon : 'fa-pencil',
|
icon : 'fas fa-pencil-alt',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Column Break',
|
name : 'Column Break',
|
||||||
icon : 'fa-columns',
|
icon : 'fas fa-columns',
|
||||||
gen : '```\n```\n\n'
|
gen : '\n\\column\n'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'New Page',
|
name : 'New Page',
|
||||||
icon : 'fa-file-text',
|
icon : 'fas fa-file-alt',
|
||||||
gen : '\\page\n\n'
|
gen : '\n\\page\n'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Vertical Spacing',
|
name : 'Vertical Spacing',
|
||||||
icon : 'fa-arrows-v',
|
icon : 'fas fa-arrows-alt-v',
|
||||||
gen : '<div style=\'margin-top:140px\'></div>\n\n'
|
gen : '\n::::\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Horizontal Spacing',
|
||||||
|
icon : 'fas fa-arrows-alt-h',
|
||||||
|
gen : ' {{width:100px}} '
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Wide Block',
|
name : 'Wide Block',
|
||||||
icon : 'fa-arrows-h',
|
icon : 'fas fa-window-maximize',
|
||||||
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n'
|
gen : dedent`\n
|
||||||
|
{{wide
|
||||||
|
Everything in here will be extra wide. Tables, text, everything!
|
||||||
|
Beware though, CSS columns can behave a bit weird sometimes. You may
|
||||||
|
have to manually place column breaks with \`\column\` to make the
|
||||||
|
surrounding text flow with this wide block the way you want.
|
||||||
|
}}
|
||||||
|
\n`
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'QR Code',
|
||||||
|
icon : 'fas fa-qrcode',
|
||||||
|
gen : (brew)=>{
|
||||||
|
return `![]` +
|
||||||
|
`(https://api.qrserver.com/v1/create-qr-code/?data=` +
|
||||||
|
`https://homebrewery.naturalcrit.com${brew.shareId ? `/share/${brew.shareId}` : ''}` +
|
||||||
|
`&size=100x100) {width:100px;mix-blend-mode:multiply}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Page Number',
|
||||||
|
icon : 'fas fa-bookmark',
|
||||||
|
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Auto-incrementing Page Number',
|
||||||
|
icon : 'fas fa-sort-numeric-down',
|
||||||
|
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Link to page',
|
||||||
|
icon : 'fas fa-link',
|
||||||
|
gen : '[Click here](#p3) to go to page 3\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Table of Contents',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : TableOfContentsGen
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
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',
|
||||||
|
icon : 'fas fa-remove-format',
|
||||||
|
gen : dedent`/* Removes Drop Caps */
|
||||||
|
.page h1+p:first-letter {
|
||||||
|
all: unset;
|
||||||
|
}\n\n`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Tweak Drop Cap',
|
||||||
|
icon : 'fas fa-sliders-h',
|
||||||
|
gen : dedent`/* Drop Cap settings */
|
||||||
|
.page h1 + p::first-letter {
|
||||||
|
font-family: SolberaImitationRemake;
|
||||||
|
font-size: 3.5cm;
|
||||||
|
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||||
|
line-height: 1em;
|
||||||
|
}\n\n`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
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',
|
name : 'Image',
|
||||||
icon : 'fa-image',
|
icon : 'fas fa-image',
|
||||||
gen : [
|
gen : dedent`
|
||||||
'<img ',
|
 {width:325px,mix-blend-mode:multiply}
|
||||||
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
|
|
||||||
' style=\'width:325px\' />',
|
{{artist,position:relative,top:-230px,left:10px,margin-bottom:-30px
|
||||||
'Credit: Kyounghwan Kim'
|
##### Cat Warrior
|
||||||
].join('\n')
|
[Kyoung Hwan Kim](https://www.artstation.com/tahra)
|
||||||
|
}}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Background Image',
|
name : 'Background Image',
|
||||||
icon : 'fa-tree',
|
icon : 'fas fa-tree',
|
||||||
gen : [
|
gen : dedent`
|
||||||
'<img ',
|
 {position:absolute,top:50px,right:30px,width:280px}
|
||||||
' src=\'http://i.imgur.com/hMna6G0.png\' ',
|
|
||||||
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
|
|
||||||
].join('\n')
|
|
||||||
},
|
|
||||||
|
|
||||||
|
{{artist,top:80px,right:30px
|
||||||
|
##### Homebrew Mug
|
||||||
|
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||||
|
}}`
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Page Number',
|
name : 'Watercolor Splatter',
|
||||||
icon : 'fa-bookmark',
|
icon : 'fas fa-fill-drip',
|
||||||
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
|
gen : watercolorGen,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name : 'Auto-incrementing Page Number',
|
name : 'Watermark',
|
||||||
icon : 'fa-sort-numeric-asc',
|
icon : 'fas fa-id-card',
|
||||||
gen : '<div class=\'pageNumber auto\'></div>\n'
|
gen : dedent`
|
||||||
|
{{watermark Homebrewery}}\n`
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
name : 'Link to page',
|
|
||||||
icon : 'fa-link',
|
|
||||||
gen : '[Click here](#p3) to go to page 3\n'
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name : 'Table of Contents',
|
|
||||||
icon : 'fa-book',
|
|
||||||
gen : TableOfContentsGen
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -87,64 +165,89 @@ module.exports = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'PHB',
|
groupName : 'PHB',
|
||||||
icon : 'fa-book',
|
icon : 'fas fa-book',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'Spell',
|
name : 'Spell',
|
||||||
icon : 'fa-magic',
|
icon : 'fas fa-magic',
|
||||||
gen : MagicGen.spell,
|
gen : MagicGen.spell,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Spell List',
|
name : 'Spell List',
|
||||||
icon : 'fa-list',
|
icon : 'fas fa-scroll',
|
||||||
gen : MagicGen.spellList,
|
gen : MagicGen.spellList,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Class Feature',
|
name : 'Class Feature',
|
||||||
icon : 'fa-trophy',
|
icon : 'fas fa-mask',
|
||||||
gen : ClassFeatureGen,
|
gen : ClassFeatureGen,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Note',
|
name : 'Note',
|
||||||
icon : 'fa-sticky-note',
|
icon : 'fas fa-sticky-note',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'> ##### Time to Drop Knowledge',
|
{{note
|
||||||
'> Use notes to point out some interesting information. ',
|
##### Time to Drop Knowledge
|
||||||
'> ',
|
Use notes to point out some interesting information.
|
||||||
'> **Tables and lists** both work within a note.'
|
|
||||||
].join('\n');
|
**Tables and lists** both work within a note.
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Descriptive Text Box',
|
name : 'Descriptive Text Box',
|
||||||
icon : 'fa-sticky-note-o',
|
icon : 'fas fa-comment-alt',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'<div class=\'descriptive\'>',
|
{{descriptive
|
||||||
'##### Time to Drop Knowledge',
|
##### Time to Drop Knowledge
|
||||||
'Use notes to point out some interesting information. ',
|
Use descriptive boxes to highlight text that should be read aloud.
|
||||||
'',
|
|
||||||
'**Tables and lists** both work within a note.',
|
**Tables and lists** both work within a descriptive box.
|
||||||
'</div>'
|
}}
|
||||||
].join('\n');
|
\n`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Monster Stat Block (unframed)',
|
||||||
|
icon : 'fas fa-paw',
|
||||||
|
gen : MonsterBlockGen.monster('monster', 2),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Monster Stat Block',
|
name : 'Monster Stat Block',
|
||||||
icon : 'fa-bug',
|
icon : 'fas fa-spider',
|
||||||
gen : MonsterBlockGen.half,
|
gen : MonsterBlockGen.monster('monster,frame', 2),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Wide Monster Stat Block',
|
name : 'Wide Monster Stat Block',
|
||||||
icon : 'fa-paw',
|
icon : 'fas fa-dragon',
|
||||||
gen : MonsterBlockGen.full,
|
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Cover Page',
|
name : 'Cover Page',
|
||||||
icon : 'fa-file-word-o',
|
icon : 'fas fa-file-word',
|
||||||
gen : CoverPageGen,
|
gen : CoverPageGen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Magic Item',
|
||||||
|
icon : 'fas fa-hat-wizard',
|
||||||
|
gen : MagicGen.item,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Artist Credit',
|
||||||
|
icon : 'fas fa-signature',
|
||||||
|
gen : function(){
|
||||||
|
return dedent`
|
||||||
|
{{artist,top:90px,right:30px
|
||||||
|
##### Starry Night
|
||||||
|
[Van Gogh](https://www.vangoghmuseum.nl/en)
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -154,79 +257,98 @@ module.exports = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Tables',
|
groupName : 'Tables',
|
||||||
icon : 'fa-table',
|
icon : 'fas fa-table',
|
||||||
|
view : 'text',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
|
||||||
name : 'Class Table',
|
|
||||||
icon : 'fa-table',
|
|
||||||
gen : ClassTableGen.full,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Half Class Table',
|
|
||||||
icon : 'fa-list-alt',
|
|
||||||
gen : ClassTableGen.half,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name : 'Table',
|
name : 'Table',
|
||||||
icon : 'fa-th-list',
|
icon : 'fas fa-th-list',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'##### Cookie Tastiness',
|
##### Character Advancement
|
||||||
'| Tastiness | Cookie Type |',
|
| Experience Points | Level | Proficiency Bonus |
|
||||||
'|:----:|:-------------|',
|
|:------------------|:-----:|:-----------------:|
|
||||||
'| -5 | Raisin |',
|
| 0 | 1 | +2 |
|
||||||
'| 8th | Chocolate Chip |',
|
| 300 | 2 | +2 |
|
||||||
'| 11th | 2 or lower |',
|
| 900 | 3 | +2 |
|
||||||
'| 14th | 3 or lower |',
|
| 2,700 | 4 | +2 |
|
||||||
'| 17th | 4 or lower |\n\n',
|
| 6,500 | 5 | +3 |
|
||||||
].join('\n');
|
| 14,000 | 6 | +3 |
|
||||||
},
|
\n`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Wide Table',
|
name : 'Wide Table',
|
||||||
icon : 'fa-list',
|
icon : 'fas fa-list',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'<div class=\'wide\'>',
|
{{wide
|
||||||
'##### Cookie Tastiness',
|
##### Weapons
|
||||||
'| Tastiness | Cookie Type |',
|
| Name | Cost | Damage | Weight | Properties |
|
||||||
'|:----:|:-------------|',
|
|:------------------------|:-----:|:----------------|--------:|:-----------|
|
||||||
'| -5 | Raisin |',
|
| *Simple Melee Weapons* | | | | |
|
||||||
'| 8th | Chocolate Chip |',
|
|   Club | 1 sp | 1d4 bludgeoning | 2 lb. | Light |
|
||||||
'| 11th | 2 or lower |',
|
|   Dagger | 2 gp | 1d4 piercing | 1 lb. | Finesse |
|
||||||
'| 14th | 3 or lower |',
|
|   Spear | 1 gp | 1d6 piercing | 3 lb. | Thrown |
|
||||||
'| 17th | 4 or lower |',
|
| *Simple Ranged Weapons* | | | | |
|
||||||
'</div>\n\n'
|
|   Dart | 5 cp | 1d4 piercig | 1/4 lb. | Finesse |
|
||||||
].join('\n');
|
|   Shortbow | 25 gp | 1d6 piercing | 2 lb. | Ammunition |
|
||||||
},
|
|   Sling | 1 sp | 1d4 bludgeoning | — | Ammunition |
|
||||||
|
}}
|
||||||
|
\n`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Split Table',
|
name : 'Split Table',
|
||||||
icon : 'fa-th-large',
|
icon : 'fas fa-th-large',
|
||||||
gen : function(){
|
gen : function(){
|
||||||
return [
|
return dedent`
|
||||||
'<div style=\'column-count:2\'>',
|
##### Typical Difficulty Classes
|
||||||
'| d10 | Damage Type |',
|
{{column-count:2
|
||||||
'|:---:|:------------|',
|
| Task Difficulty | DC |
|
||||||
'| 1 | Acid |',
|
|:----------------|:--:|
|
||||||
'| 2 | Cold |',
|
| Very easy | 5 |
|
||||||
'| 3 | Fire |',
|
| Easy | 10 |
|
||||||
'| 4 | Force |',
|
| Medium | 15 |
|
||||||
'| 5 | Lightning |',
|
|
||||||
'',
|
| Task Difficulty | DC |
|
||||||
'```',
|
|:------------------|:--:|
|
||||||
'```',
|
| Hard | 20 |
|
||||||
'',
|
| Very hard | 25 |
|
||||||
'| d10 | Damage Type |',
|
| Nearly impossible | 30 |
|
||||||
'|:---:|:------------|',
|
}}
|
||||||
'| 6 | Necrotic |',
|
\n`;
|
||||||
'| 7 | Poison |',
|
}
|
||||||
'| 8 | Psychic |',
|
},
|
||||||
'| 9 | Radiant |',
|
{
|
||||||
'| 10 | Thunder |',
|
name : 'Class Table',
|
||||||
'</div>\n\n',
|
icon : 'fas fa-table',
|
||||||
].join('\n');
|
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'),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -234,33 +356,46 @@ module.exports = [
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**************** PRINT *************/
|
/**************** PAGE *************/
|
||||||
|
|
||||||
{
|
{
|
||||||
groupName : 'Print',
|
groupName : 'Print',
|
||||||
icon : 'fa-print',
|
icon : 'fas fa-print',
|
||||||
|
view : 'style',
|
||||||
snippets : [
|
snippets : [
|
||||||
{
|
{
|
||||||
name : 'A4 PageSize',
|
name : 'A4 Page Size',
|
||||||
icon : 'fa-file-o',
|
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 : '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`
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const getTOC = (pages)=>{
|
const getTOC = (pages)=>{
|
||||||
const add1 = (title, page)=>{
|
const add1 = (title, page)=>{
|
||||||
@@ -9,7 +10,7 @@ const getTOC = (pages)=>{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const add2 = (title, page)=>{
|
const add2 = (title, page)=>{
|
||||||
if(!_.last(res)) add1('', page);
|
if(!_.last(res)) add1(null, page);
|
||||||
_.last(res).children.push({
|
_.last(res).children.push({
|
||||||
title : title,
|
title : title,
|
||||||
page : page + 1,
|
page : page + 1,
|
||||||
@@ -17,8 +18,8 @@ const getTOC = (pages)=>{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const add3 = (title, page)=>{
|
const add3 = (title, page)=>{
|
||||||
if(!_.last(res)) add1('', page);
|
if(!_.last(res)) add1(null, page);
|
||||||
if(!_.last(_.last(res).children)) add2('', page);
|
if(!_.last(_.last(res).children)) add2(null, page);
|
||||||
_.last(_.last(res).children).children.push({
|
_.last(_.last(res).children).children.push({
|
||||||
title : title,
|
title : title,
|
||||||
page : page + 1,
|
page : page + 1,
|
||||||
@@ -48,16 +49,24 @@ const getTOC = (pages)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
module.exports = function(brew){
|
module.exports = function(brew){
|
||||||
const pages = brew.split('\\page');
|
const pages = brew.text.split('\\page');
|
||||||
const TOC = getTOC(pages);
|
const TOC = getTOC(pages);
|
||||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
if(g1.title !== null) {
|
||||||
|
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)=>{
|
||||||
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
|
if(g2.title !== null) {
|
||||||
|
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)=>{
|
||||||
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
|
if(g2.title !== null) {
|
||||||
|
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||||
|
} else { // Don't over-indent if no level-2 parent entry
|
||||||
|
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -65,8 +74,11 @@ module.exports = function(brew){
|
|||||||
return r;
|
return r;
|
||||||
}, []).join('\n');
|
}, []).join('\n');
|
||||||
|
|
||||||
return `<div class='toc'>
|
return dedent`
|
||||||
##### Table Of Contents
|
{{toc,wide
|
||||||
${markdown}
|
# Table Of Contents
|
||||||
</div>\n`;
|
|
||||||
|
${markdown}
|
||||||
|
}}
|
||||||
|
\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`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
module.exports = function(classname){
|
||||||
|
|
||||||
|
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
|
||||||
|
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']);
|
||||||
|
|
||||||
|
classname = classname.toLowerCase();
|
||||||
|
|
||||||
|
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
||||||
|
|
||||||
|
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'];
|
||||||
|
|
||||||
|
|
||||||
|
return [
|
||||||
|
'## Class Features',
|
||||||
|
`As a ${classname}, you gain the following class features`,
|
||||||
|
'#### Hit Points',
|
||||||
|
'___',
|
||||||
|
`- **Hit Dice:** 1d${hitDie} per ${classname} level`,
|
||||||
|
`- **Hit Points at 1st Level:** ${hitDie} + your Constitution modifier`,
|
||||||
|
`- **Hit Points at Higher Levels:** 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st`,
|
||||||
|
'',
|
||||||
|
'#### Proficiencies',
|
||||||
|
'___',
|
||||||
|
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
|
||||||
|
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||||
|
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||||
|
'',
|
||||||
|
'___',
|
||||||
|
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
||||||
|
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
||||||
|
'',
|
||||||
|
'#### Equipment',
|
||||||
|
'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)* five javelins or *(b)* any simple melee weapon',
|
||||||
|
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
|
||||||
|
'\n\n\n'
|
||||||
|
].join('\n');
|
||||||
|
};
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const features = [
|
||||||
|
'Astrological Botany',
|
||||||
|
'Astrological Chemistry',
|
||||||
|
'Biochemical Sorcery',
|
||||||
|
'Civil Alchemy',
|
||||||
|
'Consecrated Biochemistry',
|
||||||
|
'Demonic Anthropology',
|
||||||
|
'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',
|
||||||
|
'Gunpowder Torturer',
|
||||||
|
'Orbital Gravedigger',
|
||||||
|
'Phased Linguist',
|
||||||
|
'Mathematical Pharmacist',
|
||||||
|
'Plasma Outlaw',
|
||||||
|
'Malefic Chemist',
|
||||||
|
'Police Cultist'
|
||||||
|
];
|
||||||
|
|
||||||
|
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||||
|
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
|
||||||
|
|
||||||
|
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 getFeature = (level)=>{
|
||||||
|
let res = [];
|
||||||
|
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
|
||||||
|
res = ['Ability Score Improvement'];
|
||||||
|
}
|
||||||
|
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
|
||||||
|
if(!res.length) return '─';
|
||||||
|
return res.join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
full : function(){
|
||||||
|
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 spells = 1;
|
||||||
|
let slots = 2;
|
||||||
|
return `<div class='classTable wide'>\n##### The ${classname}\n` +
|
||||||
|
`| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
|
||||||
|
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
|
||||||
|
_.map(levels, function(levelName, level){
|
||||||
|
const res = [
|
||||||
|
levelName,
|
||||||
|
`+${profBonus[level]}`,
|
||||||
|
getFeature(level),
|
||||||
|
cantrips,
|
||||||
|
spells,
|
||||||
|
drawSlots(slots)
|
||||||
|
].join(' | ');
|
||||||
|
|
||||||
|
cantrips += _.random(0, 1);
|
||||||
|
spells += _.random(0, 1);
|
||||||
|
slots += _.random(0, 2);
|
||||||
|
|
||||||
|
return `| ${res} |`;
|
||||||
|
}).join('\n')}\n</div>\n\n`;
|
||||||
|
},
|
||||||
|
|
||||||
|
half : function(){
|
||||||
|
const classname = _.sample(classnames);
|
||||||
|
|
||||||
|
let featureScore = 1;
|
||||||
|
return `<div class='classTable'>\n##### The ${classname}\n` +
|
||||||
|
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
|
||||||
|
`|:---:|:---:|:---|:---:|\n${
|
||||||
|
_.map(levels, function(levelName, level){
|
||||||
|
const res = [
|
||||||
|
levelName,
|
||||||
|
`+${profBonus[level]}`,
|
||||||
|
getFeature(level),
|
||||||
|
`+${featureScore}`
|
||||||
|
].join(' | ');
|
||||||
|
|
||||||
|
featureScore += _.random(0, 1);
|
||||||
|
|
||||||
|
return `| ${res} |`;
|
||||||
|
}).join('\n')}\n</div>\n\n`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const titles = [
|
||||||
|
'The Burning Gallows',
|
||||||
|
'The Ring of Nenlast',
|
||||||
|
'Below the Blind Tavern',
|
||||||
|
'Below the Hungering River',
|
||||||
|
'Before Bahamut\'s Land',
|
||||||
|
'The Cruel Grave from Within',
|
||||||
|
'The Strength of Trade Road',
|
||||||
|
'Through The Raven Queen\'s Worlds',
|
||||||
|
'Within the Settlement',
|
||||||
|
'The Crown from Within',
|
||||||
|
'The Merchant Within the Battlefield',
|
||||||
|
'Ioun\'s Fading Traveler',
|
||||||
|
'The Legion Ingredient',
|
||||||
|
'The Explorer Lure',
|
||||||
|
'Before the Charming Badlands',
|
||||||
|
'The Living Dead Above the Fearful Cage',
|
||||||
|
'Vecna\'s Hidden Sage',
|
||||||
|
'Bahamut\'s Demonspawn',
|
||||||
|
'Across Gruumsh\'s Elemental Chaos',
|
||||||
|
'The Blade of Orcus',
|
||||||
|
'Beyond Revenge',
|
||||||
|
'Brain of Insanity',
|
||||||
|
'Breed Battle!, A New Beginning',
|
||||||
|
'Evil Lake, A New Beginning',
|
||||||
|
'Invasion of the Gigantic Cat, Part II',
|
||||||
|
'Kraken War 2020',
|
||||||
|
'The Body Whisperers',
|
||||||
|
'The Diabolical Tales of the Ape-Women',
|
||||||
|
'The Doctor Immortal',
|
||||||
|
'The Doctor from Heaven',
|
||||||
|
'The Graveyard',
|
||||||
|
'Azure Core',
|
||||||
|
'Core Battle',
|
||||||
|
'Core of Heaven: The Guardian of Amazement',
|
||||||
|
'Deadly Amazement III',
|
||||||
|
'Dry Chaos IX',
|
||||||
|
'Gate Thunder',
|
||||||
|
'Guardian: Skies of the Dark Wizard',
|
||||||
|
'Lute of Eternity',
|
||||||
|
'Mercury\'s Planet: Brave Evolution',
|
||||||
|
'Ruby of Atlantis: The Quake of Peace',
|
||||||
|
'Sky of Zelda: The Thunder of Force',
|
||||||
|
'Vyse\'s Skies',
|
||||||
|
'White Greatness III',
|
||||||
|
'Yellow Divinity',
|
||||||
|
'Zidane\'s Ghost'
|
||||||
|
];
|
||||||
|
|
||||||
|
const subtitles = [
|
||||||
|
'In an ominous universe, a botanist opposes terrorism.',
|
||||||
|
'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.',
|
||||||
|
'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.',
|
||||||
|
'In an evil empire of horror, two rangers battle the forces of hell.',
|
||||||
|
'In a lost city, in an age of sorcery, a librarian quests for revenge.',
|
||||||
|
'In a universe of illusions and danger, three time travellers and an adventurer search for justice.',
|
||||||
|
'In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.',
|
||||||
|
'In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.',
|
||||||
|
'In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.',
|
||||||
|
'In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.',
|
||||||
|
'In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.',
|
||||||
|
'In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.',
|
||||||
|
'In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.',
|
||||||
|
'In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.',
|
||||||
|
'In a kingdom of deception, a reporter searches for fame.',
|
||||||
|
'In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.',
|
||||||
|
'In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.',
|
||||||
|
'In a dark city of confusion, three swordswomen and a singer battle lawlessness.',
|
||||||
|
'In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.',
|
||||||
|
'In a kingdom of panic, six adventurers oppose lawlessness.',
|
||||||
|
'In a land of dreams and hopelessness, three hackers and a cyborg search for justice.',
|
||||||
|
'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.',
|
||||||
|
'In a wicked universe, five seers fight lawlessness.',
|
||||||
|
'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.',
|
||||||
|
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.',
|
||||||
|
'In a cursed empire, five inventors oppose terrorism.',
|
||||||
|
'On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.',
|
||||||
|
'In a forgotten land, a reporter and a spy try to stop the apocalypse.',
|
||||||
|
'In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.',
|
||||||
|
'On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.',
|
||||||
|
'In a galaxy of dark magic, four fighters seek freedom.',
|
||||||
|
'In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.',
|
||||||
|
'In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.',
|
||||||
|
'In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.',
|
||||||
|
'In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.',
|
||||||
|
'On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.',
|
||||||
|
'In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.',
|
||||||
|
'In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.',
|
||||||
|
'In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.',
|
||||||
|
'In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.',
|
||||||
|
'In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.',
|
||||||
|
'In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.',
|
||||||
|
'In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.',
|
||||||
|
'In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.',
|
||||||
|
'In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime.'
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = ()=>{
|
||||||
|
return `<style>
|
||||||
|
.phb#p1{ text-align:center; }
|
||||||
|
.phb#p1:after{ display:none; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div style='margin-top:450px;'></div>
|
||||||
|
|
||||||
|
# ${_.sample(titles)}
|
||||||
|
|
||||||
|
<div style='margin-top:25px'></div>
|
||||||
|
<div class='wide'>
|
||||||
|
##### ${_.sample(subtitles)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
\\page`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||||
|
|
||||||
|
const ClassTableGen = require('./classtable.gen.js');
|
||||||
|
|
||||||
|
module.exports = function(){
|
||||||
|
|
||||||
|
const classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||||
|
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge']);
|
||||||
|
|
||||||
|
|
||||||
|
const image = _.sample(_.map([
|
||||||
|
'http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png',
|
||||||
|
'http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png',
|
||||||
|
'http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png',
|
||||||
|
'http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png',
|
||||||
|
'http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png',
|
||||||
|
'http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png',
|
||||||
|
'http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png',
|
||||||
|
'http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png',
|
||||||
|
], function(url){
|
||||||
|
return `<img src = '${url}' style='max-width:8cm;max-height:25cm' />`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
return `${[
|
||||||
|
image,
|
||||||
|
'',
|
||||||
|
'```',
|
||||||
|
'```',
|
||||||
|
'<div style=\'margin-top:240px\'></div>\n\n',
|
||||||
|
`## ${classname}`,
|
||||||
|
'Cool intro stuff will go here',
|
||||||
|
|
||||||
|
'\\page',
|
||||||
|
ClassTableGen(classname),
|
||||||
|
ClassFeatureGen(classname),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
].join('\n')}\n\n\n`;
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const spellNames = [
|
||||||
|
'Astral Rite of Acne',
|
||||||
|
'Create Acne',
|
||||||
|
'Cursed Ramen Erruption',
|
||||||
|
'Dark Chant of the Dentists',
|
||||||
|
'Erruption of Immaturity',
|
||||||
|
'Flaming Disc of Inconvenience',
|
||||||
|
'Heal Bad Hygene',
|
||||||
|
'Heavenly Transfiguration of the Cream Devil',
|
||||||
|
'Hellish Cage of Mucus',
|
||||||
|
'Irritate Peanut Butter Fairy',
|
||||||
|
'Luminous Erruption of Tea',
|
||||||
|
'Mystic Spell of the Poser',
|
||||||
|
'Sorcerous Enchantment of the Chimneysweep',
|
||||||
|
'Steak Sauce Ray',
|
||||||
|
'Talk to Groupie',
|
||||||
|
'Astonishing Chant of Chocolate',
|
||||||
|
'Astounding Pasta Puddle',
|
||||||
|
'Ball of Annoyance',
|
||||||
|
'Cage of Yarn',
|
||||||
|
'Control Noodles Elemental',
|
||||||
|
'Create Nervousness',
|
||||||
|
'Cure Baldness',
|
||||||
|
'Cursed Ritual of Bad Hair',
|
||||||
|
'Dispell Piles in Dentist',
|
||||||
|
'Eliminate Florists',
|
||||||
|
'Illusionary Transfiguration of the Babysitter',
|
||||||
|
'Necromantic Armor of Salad Dressing',
|
||||||
|
'Occult Transfiguration of Foot Fetish',
|
||||||
|
'Protection from Mucus Giant',
|
||||||
|
'Tinsel Blast',
|
||||||
|
'Alchemical Evocation of the Goths',
|
||||||
|
'Call Fangirl',
|
||||||
|
'Divine Spell of Crossdressing',
|
||||||
|
'Dominate Ramen Giant',
|
||||||
|
'Eliminate Vindictiveness in Gym Teacher',
|
||||||
|
'Extra-Planar Spell of Irritation',
|
||||||
|
'Induce Whining in Babysitter',
|
||||||
|
'Invoke Complaining',
|
||||||
|
'Magical Enchantment of Arrogance',
|
||||||
|
'Occult Globe of Salad Dressing',
|
||||||
|
'Overwhelming Enchantment of the Chocolate Fairy',
|
||||||
|
'Sorcerous Dandruff Globe',
|
||||||
|
'Spiritual Invocation of the Costumers',
|
||||||
|
'Ultimate Rite of the Confetti Angel',
|
||||||
|
'Ultimate Ritual of Mouthwash',
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
spellList : function(){
|
||||||
|
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 spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
||||||
|
return `- ${spell}`;
|
||||||
|
}).join('\n');
|
||||||
|
return `##### ${level} \n${spells} \n`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
return `<div class='spellList'>\n${content}\n</div>`;
|
||||||
|
},
|
||||||
|
|
||||||
|
spell : function(){
|
||||||
|
const level = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th'];
|
||||||
|
const spellSchools = ['abjuration', 'conjuration', 'divination', 'enchantment', 'evocation', 'illusion', 'necromancy', 'transmutation'];
|
||||||
|
|
||||||
|
|
||||||
|
let components = _.sampleSize(['V', 'S', 'M'], _.random(1, 3)).join(', ');
|
||||||
|
if(components.indexOf('M') !== -1){
|
||||||
|
components += ` (${_.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1, 3)).join(', ')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
`#### ${_.sample(spellNames)}`,
|
||||||
|
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
|
||||||
|
'___',
|
||||||
|
'- **Casting Time:** 1 action',
|
||||||
|
`- **Range:** ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
|
||||||
|
`- **Components:** ${components}`,
|
||||||
|
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
|
||||||
|
'',
|
||||||
|
'A flame, equivalent in brightness to a torch, springs from an object that you touch. ',
|
||||||
|
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
|
||||||
|
'A *continual flame* can be covered or hidden but not smothered or quenched.',
|
||||||
|
'\n\n\n'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const genList = function(list, max){
|
||||||
|
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMonsterName = function(){
|
||||||
|
return _.sample([
|
||||||
|
'All-devouring Baseball Imp',
|
||||||
|
'All-devouring Gumdrop Wraith',
|
||||||
|
'Chocolate Hydra',
|
||||||
|
'Devouring Peacock',
|
||||||
|
'Economy-sized Colossus of the Lemonade Stand',
|
||||||
|
'Ghost Pigeon',
|
||||||
|
'Gibbering Duck',
|
||||||
|
'Sparklemuffin Peacock Spider',
|
||||||
|
'Gum Elemental',
|
||||||
|
'Illiterate Construct of the Candy Store',
|
||||||
|
'Ineffable Chihuahua',
|
||||||
|
'Irritating Death Hamster',
|
||||||
|
'Irritating Gold Mouse',
|
||||||
|
'Juggernaut Snail',
|
||||||
|
'Juggernaut of the Sock Drawer',
|
||||||
|
'Koala of the Cosmos',
|
||||||
|
'Mad Koala of the West',
|
||||||
|
'Milk Djinni of the Lemonade Stand',
|
||||||
|
'Mind Ferret',
|
||||||
|
'Mystic Salt Spider',
|
||||||
|
'Necrotic Halitosis Angel',
|
||||||
|
'Pinstriped Famine Sheep',
|
||||||
|
'Ritalin Leech',
|
||||||
|
'Shocker Kangaroo',
|
||||||
|
'Stellar Tennis Juggernaut',
|
||||||
|
'Wailing Quail of the Sun',
|
||||||
|
'Angel Pigeon',
|
||||||
|
'Anime Sphinx',
|
||||||
|
'Bored Avalanche Sheep of the Wasteland',
|
||||||
|
'Devouring Nougat Sphinx of the Sock Drawer',
|
||||||
|
'Djinni of the Footlocker',
|
||||||
|
'Ectoplasmic Jazz Devil',
|
||||||
|
'Flatuent Angel',
|
||||||
|
'Gelatinous Duck of the Dream-Lands',
|
||||||
|
'Gelatinous Mouse',
|
||||||
|
'Golem of the Footlocker',
|
||||||
|
'Lich Wombat',
|
||||||
|
'Mechanical Sloth of the Past',
|
||||||
|
'Milkshake Succubus',
|
||||||
|
'Puffy Bone Peacock of the East',
|
||||||
|
'Rainbow Manatee',
|
||||||
|
'Rune Parrot',
|
||||||
|
'Sand Cow',
|
||||||
|
'Sinister Vanilla Dragon',
|
||||||
|
'Snail of the North',
|
||||||
|
'Spider of the Sewer',
|
||||||
|
'Stellar Sawdust Leech',
|
||||||
|
'Storm Anteater of Hell',
|
||||||
|
'Stupid Spirit of the Brewery',
|
||||||
|
'Time Kangaroo',
|
||||||
|
'Tomb Poodle',
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getType = function(){
|
||||||
|
return `${_.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'])} ${_.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAlignment = function(){
|
||||||
|
return _.sample([
|
||||||
|
'annoying evil',
|
||||||
|
'chaotic gossipy',
|
||||||
|
'chaotic sloppy',
|
||||||
|
'depressed neutral',
|
||||||
|
'lawful bogus',
|
||||||
|
'lawful coy',
|
||||||
|
'manic-depressive evil',
|
||||||
|
'narrow-minded neutral',
|
||||||
|
'neutral annoying',
|
||||||
|
'neutral ignorant',
|
||||||
|
'oedpipal neutral',
|
||||||
|
'silly neutral',
|
||||||
|
'unoriginal neutral',
|
||||||
|
'weird neutral',
|
||||||
|
'wordy evil',
|
||||||
|
'unaligned'
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStats = function(){
|
||||||
|
return `>|${_.times(6, function(){
|
||||||
|
const num = _.random(1, 20);
|
||||||
|
const mod = Math.ceil(num/2 - 5);
|
||||||
|
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
||||||
|
}).join('|')}|`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const genAbilities = function(){
|
||||||
|
return _.sample([
|
||||||
|
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
|
||||||
|
'> ***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
|
||||||
|
'> ***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
|
||||||
|
'> ***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
|
||||||
|
'> ***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
||||||
|
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const genAction = function(){
|
||||||
|
const name = _.sample([
|
||||||
|
'Abdominal Drop',
|
||||||
|
'Airplane Hammer',
|
||||||
|
'Atomic Death Throw',
|
||||||
|
'Bulldog Rake',
|
||||||
|
'Corkscrew Strike',
|
||||||
|
'Crossed Splash',
|
||||||
|
'Crossface Suplex',
|
||||||
|
'DDT Powerbomb',
|
||||||
|
'Dual Cobra Wristlock',
|
||||||
|
'Dual Throw',
|
||||||
|
'Elbow Hold',
|
||||||
|
'Gory Body Sweep',
|
||||||
|
'Heel Jawbreaker',
|
||||||
|
'Jumping Driver',
|
||||||
|
'Open Chin Choke',
|
||||||
|
'Scorpion Flurry',
|
||||||
|
'Somersault Stump Fists',
|
||||||
|
'Suffering Wringer',
|
||||||
|
'Super Hip Submission',
|
||||||
|
'Super Spin',
|
||||||
|
'Team Elbow',
|
||||||
|
'Team Foot',
|
||||||
|
'Tilt-a-whirl Chin Sleeper',
|
||||||
|
'Tilt-a-whirl Eye Takedown',
|
||||||
|
'Turnbuckle Roll'
|
||||||
|
]);
|
||||||
|
|
||||||
|
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
|
||||||
|
full : function(){
|
||||||
|
return `${[
|
||||||
|
'___',
|
||||||
|
'___',
|
||||||
|
`> ## ${getMonsterName()}`,
|
||||||
|
`>*${getType()}, ${getAlignment()}*`,
|
||||||
|
'> ___',
|
||||||
|
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||||
|
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||||
|
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||||
|
'>___',
|
||||||
|
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||||
|
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||||
|
getStats(),
|
||||||
|
'>___',
|
||||||
|
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||||
|
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||||
|
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||||
|
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||||
|
'> ___',
|
||||||
|
_.times(_.random(3, 6), function(){
|
||||||
|
return genAbilities();
|
||||||
|
}).join('\n>\n'),
|
||||||
|
'> ### Actions',
|
||||||
|
_.times(_.random(4, 6), function(){
|
||||||
|
return genAction();
|
||||||
|
}).join('\n>\n'),
|
||||||
|
].join('\n')}\n\n\n`;
|
||||||
|
},
|
||||||
|
|
||||||
|
half : function(){
|
||||||
|
return `${[
|
||||||
|
'___',
|
||||||
|
`> ## ${getMonsterName()}`,
|
||||||
|
`>*${getType()}, ${getAlignment()}*`,
|
||||||
|
'> ___',
|
||||||
|
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||||
|
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||||
|
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||||
|
'>___',
|
||||||
|
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||||
|
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||||
|
getStats(),
|
||||||
|
'>___',
|
||||||
|
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||||
|
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||||
|
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||||
|
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||||
|
'> ___',
|
||||||
|
_.times(_.random(2, 3), function(){
|
||||||
|
return genAbilities();
|
||||||
|
}).join('\n>\n'),
|
||||||
|
'> ### Actions',
|
||||||
|
_.times(_.random(1, 2), function(){
|
||||||
|
return genAction();
|
||||||
|
}).join('\n>\n'),
|
||||||
|
].join('\n')}\n\n\n`;
|
||||||
|
}
|
||||||
|
};
|
||||||
327
client/homebrew/editor/snippetbar/snippetsLegacy/snippets.js
Normal file
327
client/homebrew/editor/snippetbar/snippetsLegacy/snippets.js
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
const MagicGen = require('./magic.gen.js');
|
||||||
|
const ClassTableGen = require('./classtable.gen.js');
|
||||||
|
const MonsterBlockGen = require('./monsterblock.gen.js');
|
||||||
|
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||||
|
const CoverPageGen = require('./coverpage.gen.js');
|
||||||
|
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
module.exports = [
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'Text Editor',
|
||||||
|
icon : 'fas fa-pencil-alt',
|
||||||
|
view : 'text',
|
||||||
|
snippets : [
|
||||||
|
{
|
||||||
|
name : 'Column Break',
|
||||||
|
icon : 'fas fa-columns',
|
||||||
|
gen : '```\n```\n\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'New Page',
|
||||||
|
icon : 'fas fa-file-alt',
|
||||||
|
gen : '\\page\n\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Vertical Spacing',
|
||||||
|
icon : 'fas fa-arrows-alt-v',
|
||||||
|
gen : '<div style=\'margin-top:140px\'></div>\n\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Wide Block',
|
||||||
|
icon : 'fas fa-arrows-alt-h',
|
||||||
|
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Image',
|
||||||
|
icon : 'fas fa-image',
|
||||||
|
gen : [
|
||||||
|
'<img ',
|
||||||
|
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
|
||||||
|
' style=\'width:325px\' />',
|
||||||
|
'Credit: Kyounghwan Kim'
|
||||||
|
].join('\n')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Background Image',
|
||||||
|
icon : 'fas fa-tree',
|
||||||
|
gen : [
|
||||||
|
'<img ',
|
||||||
|
' src=\'http://i.imgur.com/hMna6G0.png\' ',
|
||||||
|
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
|
||||||
|
].join('\n')
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name : 'Page Number',
|
||||||
|
icon : 'fas fa-bookmark',
|
||||||
|
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name : 'Auto-incrementing Page Number',
|
||||||
|
icon : 'fas fa-sort-numeric-down',
|
||||||
|
gen : '<div class=\'pageNumber auto\'></div>\n'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name : 'Link to page',
|
||||||
|
icon : 'fas fa-link',
|
||||||
|
gen : '[Click here](#p3) to go to page 3\n'
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name : 'Table of Contents',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
gen : TableOfContentsGen
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
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',
|
||||||
|
icon : 'fas fa-remove-format',
|
||||||
|
gen : dedent`/* Removes Drop Caps */
|
||||||
|
.phb h1+p:first-letter {
|
||||||
|
all: unset;
|
||||||
|
}\n\n`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Tweak Drop Cap',
|
||||||
|
icon : 'fas fa-sliders-h',
|
||||||
|
gen : dedent`/* Drop Cap Settings */
|
||||||
|
.phb h1 + p::first-letter {
|
||||||
|
float: left;
|
||||||
|
font-family: Solberry;
|
||||||
|
font-size: 10em;
|
||||||
|
color: #222;
|
||||||
|
line-height: .8em;
|
||||||
|
}\n\n`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Add Comment',
|
||||||
|
icon : 'fas fa-code',
|
||||||
|
gen : '/* This is a comment that will not be rendered into your brew. */'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/************************* PHB ********************/
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'PHB',
|
||||||
|
icon : 'fas fa-book',
|
||||||
|
view : 'text',
|
||||||
|
snippets : [
|
||||||
|
{
|
||||||
|
name : 'Spell',
|
||||||
|
icon : 'fas fa-magic',
|
||||||
|
gen : MagicGen.spell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Spell List',
|
||||||
|
icon : 'fas fa-list',
|
||||||
|
gen : MagicGen.spellList,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Class Feature',
|
||||||
|
icon : 'fas fa-trophy',
|
||||||
|
gen : ClassFeatureGen,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Note',
|
||||||
|
icon : 'fas fa-sticky-note',
|
||||||
|
gen : function(){
|
||||||
|
return [
|
||||||
|
'> ##### Time to Drop Knowledge',
|
||||||
|
'> Use notes to point out some interesting information. ',
|
||||||
|
'> ',
|
||||||
|
'> **Tables and lists** both work within a note.'
|
||||||
|
].join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Descriptive Text Box',
|
||||||
|
icon : 'far fa-sticky-note',
|
||||||
|
gen : function(){
|
||||||
|
return [
|
||||||
|
'<div class=\'descriptive\'>',
|
||||||
|
'##### Time to Drop Knowledge',
|
||||||
|
'Use notes to point out some interesting information. ',
|
||||||
|
'',
|
||||||
|
'**Tables and lists** both work within a note.',
|
||||||
|
'</div>'
|
||||||
|
].join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Monster Stat Block',
|
||||||
|
icon : 'fas fa-bug',
|
||||||
|
gen : MonsterBlockGen.half,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Wide Monster Stat Block',
|
||||||
|
icon : 'fas fa-paw',
|
||||||
|
gen : MonsterBlockGen.full,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Cover Page',
|
||||||
|
icon : 'far fa-file-word',
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/********************* TABLES *********************/
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'Tables',
|
||||||
|
icon : 'fas fa-table',
|
||||||
|
view : 'text',
|
||||||
|
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',
|
||||||
|
icon : 'fas fa-th-list',
|
||||||
|
gen : function(){
|
||||||
|
return [
|
||||||
|
'##### Cookie Tastiness',
|
||||||
|
'| Tastiness | Cookie Type |',
|
||||||
|
'|:----:|:-------------|',
|
||||||
|
'| -5 | Raisin |',
|
||||||
|
'| 8th | Chocolate Chip |',
|
||||||
|
'| 11th | 2 or lower |',
|
||||||
|
'| 14th | 3 or lower |',
|
||||||
|
'| 17th | 4 or lower |\n\n',
|
||||||
|
].join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Wide Table',
|
||||||
|
icon : 'fas fa-list',
|
||||||
|
gen : function(){
|
||||||
|
return [
|
||||||
|
'<div class=\'wide\'>',
|
||||||
|
'##### Cookie Tastiness',
|
||||||
|
'| Tastiness | Cookie Type |',
|
||||||
|
'|:----:|:-------------|',
|
||||||
|
'| -5 | Raisin |',
|
||||||
|
'| 8th | Chocolate Chip |',
|
||||||
|
'| 11th | 2 or lower |',
|
||||||
|
'| 14th | 3 or lower |',
|
||||||
|
'| 17th | 4 or lower |',
|
||||||
|
'</div>\n\n'
|
||||||
|
].join('\n');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Split Table',
|
||||||
|
icon : 'fas fa-th-large',
|
||||||
|
gen : dedent`\n
|
||||||
|
<div style='column-count:2'>
|
||||||
|
| d10 | Damage Type |
|
||||||
|
|:---:|:------------|
|
||||||
|
| 1 | Acid |
|
||||||
|
| 2 | Cold |
|
||||||
|
| 3 | Fire |
|
||||||
|
| 4 | Force |
|
||||||
|
| 5 | Lightning |
|
||||||
|
|
||||||
|
| d10 | Damage Type |
|
||||||
|
|:---:|:------------|
|
||||||
|
| 6 | Necrotic |
|
||||||
|
| 7 | Poison |
|
||||||
|
| 8 | Psychic |
|
||||||
|
| 9 | Radiant |
|
||||||
|
| 10 | Thunder |
|
||||||
|
</div>
|
||||||
|
\n`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**************** PRINT *************/
|
||||||
|
|
||||||
|
{
|
||||||
|
groupName : 'Print',
|
||||||
|
icon : 'fas fa-print',
|
||||||
|
view : 'style',
|
||||||
|
snippets : [
|
||||||
|
{
|
||||||
|
name : 'A4 Page Size',
|
||||||
|
icon : 'far fa-file',
|
||||||
|
gen : ['/* A4 Page Size */',
|
||||||
|
'.phb {',
|
||||||
|
' width : 210mm;',
|
||||||
|
' height : 296.8mm;',
|
||||||
|
'}'
|
||||||
|
].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')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Ink Friendly',
|
||||||
|
icon : 'fas fa-tint',
|
||||||
|
gen : dedent`
|
||||||
|
/* Ink Friendly */
|
||||||
|
.phb, .phb blockquote, .phb hr+blockquote {
|
||||||
|
background : white;
|
||||||
|
box-shadow : 0px 0px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phb img {
|
||||||
|
visibility : hidden;
|
||||||
|
}`
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const getTOC = (pages)=>{
|
||||||
|
const add1 = (title, page)=>{
|
||||||
|
res.push({
|
||||||
|
title : title,
|
||||||
|
page : page + 1,
|
||||||
|
children : []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const add2 = (title, page)=>{
|
||||||
|
if(!_.last(res)) add1('', page);
|
||||||
|
_.last(res).children.push({
|
||||||
|
title : title,
|
||||||
|
page : page + 1,
|
||||||
|
children : []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const add3 = (title, page)=>{
|
||||||
|
if(!_.last(res)) add1('', page);
|
||||||
|
if(!_.last(_.last(res).children)) add2('', page);
|
||||||
|
_.last(_.last(res).children).children.push({
|
||||||
|
title : title,
|
||||||
|
page : page + 1,
|
||||||
|
children : []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = [];
|
||||||
|
_.each(pages, (page, pageNum)=>{
|
||||||
|
const lines = page.split('\n');
|
||||||
|
_.each(lines, (line)=>{
|
||||||
|
if(_.startsWith(line, '# ')){
|
||||||
|
const title = line.replace('# ', '');
|
||||||
|
add1(title, pageNum);
|
||||||
|
}
|
||||||
|
if(_.startsWith(line, '## ')){
|
||||||
|
const title = line.replace('## ', '');
|
||||||
|
add2(title, pageNum);
|
||||||
|
}
|
||||||
|
if(_.startsWith(line, '### ')){
|
||||||
|
const title = line.replace('### ', '');
|
||||||
|
add3(title, pageNum);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = function(brew){
|
||||||
|
const pages = brew.text.split('\\page');
|
||||||
|
const TOC = getTOC(pages);
|
||||||
|
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||||
|
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||||
|
if(g1.children.length){
|
||||||
|
_.each(g1.children, (g2, idx2)=>{
|
||||||
|
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
|
||||||
|
if(g2.children.length){
|
||||||
|
_.each(g2.children, (g3, idx3)=>{
|
||||||
|
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}, []).join('\n');
|
||||||
|
|
||||||
|
return `<div class='toc'>
|
||||||
|
##### Table Of Contents
|
||||||
|
${markdown}
|
||||||
|
</div>\n`;
|
||||||
|
};
|
||||||
BIN
client/homebrew/googleDrive.png
Normal file
BIN
client/homebrew/googleDrive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
BIN
client/homebrew/googleDriveMono.png
Normal file
BIN
client/homebrew/googleDriveMono.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -1,8 +1,6 @@
|
|||||||
require('./homebrew.less');
|
require('./homebrew.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 { StaticRouter:Router, Switch, Route } = require('react-router-dom');
|
const { StaticRouter:Router, Switch, Route } = require('react-router-dom');
|
||||||
const queryString = require('query-string');
|
const queryString = require('query-string');
|
||||||
|
|
||||||
@@ -15,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 : '',
|
||||||
@@ -22,6 +21,7 @@ const Homebrew = createClass({
|
|||||||
changelog : '',
|
changelog : '',
|
||||||
version : '0.0.0',
|
version : '0.0.0',
|
||||||
account : null,
|
account : null,
|
||||||
|
enable_v3 : false,
|
||||||
brew : {
|
brew : {
|
||||||
title : '',
|
title : '',
|
||||||
text : '',
|
text : '',
|
||||||
@@ -35,7 +35,7 @@ const Homebrew = createClass({
|
|||||||
componentWillMount : function() {
|
componentWillMount : function() {
|
||||||
global.account = this.props.account;
|
global.account = this.props.account;
|
||||||
global.version = this.props.version;
|
global.version = this.props.version;
|
||||||
|
global.enable_v3 = this.props.enable_v3;
|
||||||
},
|
},
|
||||||
render : function (){
|
render : function (){
|
||||||
return (
|
return (
|
||||||
@@ -44,12 +44,15 @@ const Homebrew = createClass({
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||||
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||||
|
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||||
|
<Route path='/new' exact component={(routeProps)=><NewPage />}/>
|
||||||
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
|
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
|
||||||
<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='/new' exact component={NewPage}/>
|
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
||||||
<Route path='/changelog' exact component={()=><SharePage brew={{ title: 'Changelog', text: this.props.changelog }} />}/>
|
<Route path='/faq' exact component={()=><SharePage brew={this.props.brew} />}/>
|
||||||
<Route path='/' component={()=><HomePage welcomeText={this.props.welcomeText}/>}/>
|
<Route path='/v3_preview' exact component={()=><HomePage brew={this.props.brew} />}/>
|
||||||
|
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@import 'naturalcrit/styles/core.less';
|
@import 'naturalcrit/styles/core.less';
|
||||||
.homebrew{
|
.homebrew{
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.page{
|
.sitePage{
|
||||||
display : flex;
|
display : flex;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
background-color : @steel;
|
background-color : @steel;
|
||||||
|
|||||||
@@ -2,17 +2,33 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
module.exports = function(props){
|
const Account = createClass({
|
||||||
if(global.account){
|
displayName : 'AccountNavItem',
|
||||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fa-user'>
|
getInitialState : function() {
|
||||||
{global.account.username}
|
return {
|
||||||
|
url : ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount : function(){
|
||||||
|
if(typeof window !== 'undefined'){
|
||||||
|
this.setState({
|
||||||
|
url : window.location.href
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
if(global.account){
|
||||||
|
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
|
||||||
|
{global.account.username}
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
||||||
|
login
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
let url = '';
|
});
|
||||||
if(typeof window !== 'undefined'){
|
|
||||||
url = window.location.href;
|
module.exports = Account;
|
||||||
}
|
|
||||||
return <Nav.item href={`http://naturalcrit.com/login?redirect=${url}`} color='teal' icon='fa-sign-in'>
|
|
||||||
login
|
|
||||||
</Nav.item>;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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 Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
@@ -8,6 +7,7 @@ const MAX_TITLE_LENGTH = 50;
|
|||||||
|
|
||||||
|
|
||||||
const EditTitle = createClass({
|
const EditTitle = createClass({
|
||||||
|
displayName : 'EditTitleNavItem',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
title : '',
|
title : '',
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ module.exports = function(props){
|
|||||||
return <Nav.item
|
return <Nav.item
|
||||||
newTab={true}
|
newTab={true}
|
||||||
color='red'
|
color='red'
|
||||||
icon='fa-bug'
|
icon='fas fa-bug'
|
||||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
||||||
report issue
|
report issue
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
require('./navbar.less');
|
require('./navbar.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 Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.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,
|
||||||
@@ -39,8 +40,10 @@ 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 />
|
||||||
{/*this.renderChromeWarning()*/}
|
{/*this.renderChromeWarning()*/}
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
|
@keyframes coloring {
|
||||||
|
//from {color: white;}
|
||||||
|
//to {color: red;}
|
||||||
|
0% {color: pink;}
|
||||||
|
50% {color: pink;}
|
||||||
|
75% {color: red;}
|
||||||
|
100% {color: pink;}
|
||||||
|
}
|
||||||
.homebrew nav{
|
.homebrew nav{
|
||||||
.homebrewLogo{
|
.homebrewLogo{
|
||||||
.animate(color);
|
.animate(color);
|
||||||
@@ -47,11 +55,16 @@
|
|||||||
text-transform : initial;
|
text-transform : initial;
|
||||||
}
|
}
|
||||||
.patreon.navItem{
|
.patreon.navItem{
|
||||||
|
border-left : 1px solid #666;
|
||||||
|
border-right : 1px solid #666;
|
||||||
|
&:hover i {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
i{
|
i{
|
||||||
.animate(color);
|
.animate(color);
|
||||||
&:hover{
|
animation-name: coloring;
|
||||||
color : @red;
|
animation-duration: 2s;
|
||||||
}
|
color: pink;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.recent.navItem{
|
.recent.navItem{
|
||||||
|
|||||||
11
client/homebrew/navbar/newbrew.navitem.jsx
Normal file
11
client/homebrew/navbar/newbrew.navitem.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
module.exports = function(props){
|
||||||
|
return <Nav.item
|
||||||
|
href='/new'
|
||||||
|
color='purple'
|
||||||
|
icon='fas fa-plus-square'>
|
||||||
|
new
|
||||||
|
</Nav.item>;
|
||||||
|
};
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
module.exports = function(props){
|
module.exports = function(props){
|
||||||
return <Nav.item
|
return <Nav.item
|
||||||
className='patreon'
|
className='patreon'
|
||||||
newTab={true}
|
newTab={true}
|
||||||
href='https://www.patreon.com/stolksdorf'
|
href='https://www.patreon.com/NaturalCrit'
|
||||||
color='green'
|
color='green'
|
||||||
icon='fa-heart'>
|
icon='fas fa-heart'>
|
||||||
help out
|
help out
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
};
|
};
|
||||||
@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
module.exports = function(props){
|
module.exports = function(props){
|
||||||
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='fa-file-pdf-o'>
|
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='far fa-file-pdf'>
|
||||||
get PDF
|
get PDF
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
};
|
};
|
||||||
@@ -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 : '',
|
||||||
@@ -35,24 +35,32 @@ const RecentItems = createClass({
|
|||||||
|
|
||||||
//== Add current brew to appropriate recent items list (depending on storageKey) ==//
|
//== Add current brew to appropriate recent items list (depending on storageKey) ==//
|
||||||
if(this.props.storageKey == 'edit'){
|
if(this.props.storageKey == 'edit'){
|
||||||
|
let editId = this.props.brew.editId;
|
||||||
|
if(this.props.brew.googleId){
|
||||||
|
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
||||||
|
}
|
||||||
edited = _.filter(edited, (brew)=>{
|
edited = _.filter(edited, (brew)=>{
|
||||||
return brew.id !== this.props.brew.editId;
|
return brew.id !== editId;
|
||||||
});
|
});
|
||||||
edited.unshift({
|
edited.unshift({
|
||||||
id : this.props.brew.editId,
|
id : editId,
|
||||||
title : this.props.brew.title,
|
title : this.props.brew.title,
|
||||||
url : `/edit/${this.props.brew.editId}`,
|
url : `/edit/${editId}`,
|
||||||
ts : Date.now()
|
ts : Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if(this.props.storageKey == 'view'){
|
if(this.props.storageKey == 'view'){
|
||||||
|
let shareId = this.props.brew.shareId;
|
||||||
|
if(this.props.brew.googleId){
|
||||||
|
shareId = `${this.props.brew.googleId}${this.props.brew.shareId}`;
|
||||||
|
}
|
||||||
viewed = _.filter(viewed, (brew)=>{
|
viewed = _.filter(viewed, (brew)=>{
|
||||||
return brew.id !== this.props.brew.shareId;
|
return brew.id !== shareId;
|
||||||
});
|
});
|
||||||
viewed.unshift({
|
viewed.unshift({
|
||||||
id : this.props.brew.shareId,
|
id : shareId,
|
||||||
title : this.props.brew.title,
|
title : this.props.brew.title,
|
||||||
url : `/share/${this.props.brew.shareId}`,
|
url : `/share/${shareId}`,
|
||||||
ts : Date.now()
|
ts : Date.now()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -70,6 +78,41 @@ const RecentItems = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidUpdate : function(prevProps) {
|
||||||
|
if(prevProps.brew && this.props.brew.editId !== prevProps.brew.editId) {
|
||||||
|
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||||
|
if(this.props.storageKey == 'edit') {
|
||||||
|
let prevEditId = prevProps.brew.editId;
|
||||||
|
if(prevProps.brew.googleId){
|
||||||
|
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
edited = _.filter(this.state.edit, (brew)=>{
|
||||||
|
return brew.id !== prevEditId;
|
||||||
|
});
|
||||||
|
let editId = this.props.brew.editId;
|
||||||
|
if(this.props.brew.googleId){
|
||||||
|
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
||||||
|
}
|
||||||
|
edited.unshift({
|
||||||
|
id : editId,
|
||||||
|
title : this.props.brew.title,
|
||||||
|
url : `/edit/${editId}`,
|
||||||
|
ts : Date.now()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//== Store the updated lists (up to 8 items each) ==//
|
||||||
|
edited = _.slice(edited, 0, 8);
|
||||||
|
|
||||||
|
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
edit : edited
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
handleDropdown : function(show){
|
handleDropdown : function(show){
|
||||||
this.setState({
|
this.setState({
|
||||||
showDropdown : show
|
showDropdown : show
|
||||||
@@ -81,7 +124,7 @@ const RecentItems = createClass({
|
|||||||
|
|
||||||
const makeItems = (brews)=>{
|
const makeItems = (brews)=>{
|
||||||
return _.map(brews, (brew)=>{
|
return _.map(brews, (brew)=>{
|
||||||
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} 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>;
|
||||||
@@ -101,7 +144,7 @@ const RecentItems = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <Nav.item icon='fa-clock-o' color='grey' className='recent'
|
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
||||||
onMouseEnter={()=>this.handleDropdown(true)}
|
onMouseEnter={()=>this.handleDropdown(true)}
|
||||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||||
{this.props.text}
|
{this.props.text}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
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');
|
||||||
|
|
||||||
//var striptags = require('striptags');
|
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
const MAX_URL_SIZE = 2083;
|
const MAX_URL_SIZE = 2083;
|
||||||
@@ -12,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 : {
|
||||||
@@ -31,10 +28,7 @@ const RedditShare = createClass({
|
|||||||
const url = [
|
const url = [
|
||||||
MAIN_URL,
|
MAIN_URL,
|
||||||
`title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`,
|
`title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`,
|
||||||
|
|
||||||
`text=${encodeURIComponent(this.props.brew.text)}`
|
`text=${encodeURIComponent(this.props.brew.text)}`
|
||||||
|
|
||||||
|
|
||||||
].join('&');
|
].join('&');
|
||||||
|
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
require('./editPage.less');
|
require('./editPage.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 request = require('superagent');
|
const request = require('superagent');
|
||||||
const { Meta } = require('vitreum/headtags');
|
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 NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
const ReportIssue = require('../../navbar/issue.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');
|
||||||
@@ -20,42 +21,59 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
const googleDriveActive = require('../../googleDrive.png');
|
||||||
|
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 : {
|
||||||
text : '',
|
text : '',
|
||||||
|
style : '',
|
||||||
shareId : null,
|
shareId : null,
|
||||||
editId : null,
|
editId : null,
|
||||||
createdAt : null,
|
createdAt : null,
|
||||||
updatedAt : null,
|
updatedAt : null,
|
||||||
|
gDrive : false,
|
||||||
|
trashed : false,
|
||||||
|
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
tags : '',
|
tags : '',
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : []
|
systems : [],
|
||||||
|
renderer : 'legacy'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
|
isSaving : false,
|
||||||
isSaving : false,
|
isPending : false,
|
||||||
isPending : false,
|
alertTrashedGoogleBrew : this.props.brew.trashed,
|
||||||
errors : null,
|
alertLoginToTransfer : false,
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
saveGoogle : this.props.brew.googleId ? true : false,
|
||||||
|
confirmGoogleTransfer : false,
|
||||||
|
errors : null,
|
||||||
|
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||||
|
url : ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
savedBrew : null,
|
savedBrew : null,
|
||||||
|
|
||||||
componentDidMount : function(){
|
componentDidMount : function(){
|
||||||
|
this.setState({
|
||||||
|
url : window.location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||||
|
|
||||||
this.trySave();
|
this.trySave();
|
||||||
window.onbeforeunload = ()=>{
|
window.onbeforeunload = ()=>{
|
||||||
if(this.state.isSaving || this.state.isPending){
|
if(this.state.isSaving || this.state.isPending){
|
||||||
@@ -74,13 +92,12 @@ const EditPage = createClass({
|
|||||||
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 S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.save();
|
if(e.keyCode == S_KEY) this.save();
|
||||||
if(e.keyCode == P_KEY) window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
|
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -91,17 +108,8 @@ const EditPage = createClass({
|
|||||||
this.refs.editor.update();
|
this.refs.editor.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetadataChange : function(metadata){
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : _.merge({}, prevState.brew, metadata),
|
|
||||||
isPending : true,
|
|
||||||
}), ()=>this.trySave());
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
//If there are errors, run the validator on everychange to give quick feedback
|
|
||||||
let htmlErrors = this.state.htmlErrors;
|
let htmlErrors = this.state.htmlErrors;
|
||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
@@ -112,9 +120,23 @@ const EditPage = createClass({
|
|||||||
}), ()=>this.trySave());
|
}), ()=>this.trySave());
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleStyleChange : function(style){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : _.merge({}, prevState.brew, { style: style }),
|
||||||
|
isPending : true
|
||||||
|
}), ()=>this.trySave());
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMetaChange : function(metadata){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : _.merge({}, prevState.brew, metadata),
|
||||||
|
isPending : true,
|
||||||
|
}), ()=>this.trySave());
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
hasChanges : function(){
|
hasChanges : function(){
|
||||||
const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew;
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
return !_.isEqual(this.state.brew, savedBrew);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
trySave : function(){
|
trySave : function(){
|
||||||
@@ -126,7 +148,45 @@ const EditPage = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
save : function(){
|
handleGoogleClick : function(){
|
||||||
|
if(!global.account?.googleId) {
|
||||||
|
this.setState({
|
||||||
|
alertLoginToTransfer : true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||||
|
}));
|
||||||
|
this.clearErrors();
|
||||||
|
},
|
||||||
|
|
||||||
|
closeAlerts : function(event){
|
||||||
|
event.stopPropagation(); //Only handle click once so alert doesn't reopen
|
||||||
|
this.setState({
|
||||||
|
alertTrashedGoogleBrew : false,
|
||||||
|
alertLoginToTransfer : false,
|
||||||
|
confirmGoogleTransfer : false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleGoogleStorage : function(){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
saveGoogle : !prevState.saveGoogle,
|
||||||
|
isSaving : false,
|
||||||
|
errors : null
|
||||||
|
}), ()=>this.save());
|
||||||
|
},
|
||||||
|
|
||||||
|
clearErrors : function(){
|
||||||
|
this.setState({
|
||||||
|
errors : null,
|
||||||
|
isSaving : false
|
||||||
|
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
save : async function(){
|
||||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
@@ -135,22 +195,130 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
request
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
.put(`/api/${this.props.brew.editId}`)
|
|
||||||
.send(this.state.brew)
|
const brew = this.state.brew;
|
||||||
.end((err, res)=>{
|
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
if(err){
|
|
||||||
this.setState({
|
if(this.state.saveGoogle) {
|
||||||
errors : err,
|
if(transfer) {
|
||||||
});
|
const res = await request
|
||||||
} else {
|
.post('/api/newGoogle/')
|
||||||
this.savedBrew = res.body;
|
.send(brew)
|
||||||
this.setState({
|
.catch((err)=>{
|
||||||
isPending : false,
|
console.log(err.status === 401
|
||||||
isSaving : false,
|
? 'Not signed in!'
|
||||||
});
|
: 'Error Transferring to Google!');
|
||||||
}
|
this.setState({ errors: err, saveGoogle: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(!res) { return; }
|
||||||
|
|
||||||
|
console.log('Deleting Local Copy');
|
||||||
|
await request.delete(`/api/${brew.editId}`)
|
||||||
|
.send()
|
||||||
|
.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/${brew.editId}`)
|
||||||
|
.send(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(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error creating Local Copy');
|
||||||
|
this.setState({ errors: err });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
await request.get(`/api/removeGoogle/${brew.googleId}${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/${brew.editId}`)
|
||||||
|
.send(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error Updating Local Brew');
|
||||||
|
this.setState({ errors: err });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.savedBrew = res.body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : _.merge({}, prevState.brew, {
|
||||||
|
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||||
|
editId : this.savedBrew.editId,
|
||||||
|
shareId : this.savedBrew.shareId
|
||||||
|
}),
|
||||||
|
isPending : false,
|
||||||
|
isSaving : false,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderGoogleDriveIcon : function(){
|
||||||
|
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||||
|
{this.state.saveGoogle
|
||||||
|
? <img src={googleDriveActive} alt='googleDriveActive'/>
|
||||||
|
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{this.state.confirmGoogleTransfer &&
|
||||||
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
|
{ this.state.saveGoogle
|
||||||
|
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
|
||||||
|
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
|
||||||
|
}
|
||||||
|
<br />
|
||||||
|
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
||||||
|
Yes
|
||||||
|
</div>
|
||||||
|
<div className='deny'>
|
||||||
|
No
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{this.state.alertLoginToTransfer &&
|
||||||
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
|
You must be signed in to a Google account to transfer
|
||||||
|
between the homebrewery and Google Drive!
|
||||||
|
<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>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
@@ -158,15 +326,57 @@ 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){}
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fa-warning'>
|
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!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
Looks like there was a problem saving. <br />
|
Looks like there was a problem saving. <br />
|
||||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
Report the issue <a target='_blank' rel='noopener noreferrer'
|
||||||
href={`https://github.com/stolksdorf/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||||
here
|
here
|
||||||
</a>.
|
</a>.
|
||||||
</div>
|
</div>
|
||||||
@@ -174,36 +384,81 @@ const EditPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.isSaving){
|
if(this.state.isSaving){
|
||||||
return <Nav.item className='save' icon='fa-spinner fa-spin'>saving...</Nav.item>;
|
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||||
}
|
}
|
||||||
if(this.state.isPending && this.hasChanges()){
|
if(this.state.isPending && this.hasChanges()){
|
||||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>;
|
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||||
}
|
}
|
||||||
if(!this.state.isPending && !this.state.isSaving){
|
if(!this.state.isPending && !this.state.isSaving){
|
||||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
processShareId : function() {
|
||||||
|
return this.state.brew.googleId ?
|
||||||
|
this.state.brew.googleId + 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 &&
|
||||||
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
|
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||||
|
<div className='confirm'>
|
||||||
|
OK
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
{this.renderGoogleDriveIcon()}
|
||||||
{this.renderSaveButton()}
|
{this.renderSaveButton()}
|
||||||
|
<NewBrew />
|
||||||
<ReportIssue />
|
<ReportIssue />
|
||||||
<Nav.item newTab={true} href={`/share/${this.props.brew.shareId}`} color='teal' icon='fa-share-alt'>
|
<Nav.dropdown>
|
||||||
Share
|
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||||
</Nav.item>
|
share
|
||||||
<PrintLink shareId={this.props.brew.shareId} />
|
</Nav.item>
|
||||||
<RecentNavItem brew={this.props.brew} storageKey='edit' />
|
<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()} />
|
||||||
|
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
</Navbar>;
|
</Navbar>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='editPage page'>
|
return <div className='editPage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
@@ -211,12 +466,13 @@ const EditPage = createClass({
|
|||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||||
<Editor
|
<Editor
|
||||||
ref='editor'
|
ref='editor'
|
||||||
value={this.state.brew.text}
|
brew={this.state.brew}
|
||||||
onChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
metadata={this.state.brew}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetadataChange={this.handleMetadataChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} />
|
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors} />
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
|
@keyframes glideDown {
|
||||||
|
0% {transform : translate(-50% + 3px, 0px);
|
||||||
|
opacity : 0;}
|
||||||
|
100% {transform : translate(-50% + 3px, 10px);
|
||||||
|
opacity : 1;}
|
||||||
|
}
|
||||||
.editPage{
|
.editPage{
|
||||||
.navItem.save{
|
.navItem.save{
|
||||||
width : 105px;
|
width : 106px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
|
position : relative;
|
||||||
&.saved{
|
&.saved{
|
||||||
cursor : initial;
|
cursor : initial;
|
||||||
color : #666;
|
color : #666;
|
||||||
@@ -10,17 +16,83 @@
|
|||||||
&.error{
|
&.error{
|
||||||
position : relative;
|
position : relative;
|
||||||
background-color : @red;
|
background-color : @red;
|
||||||
.errorContainer{
|
}
|
||||||
position : absolute;
|
}
|
||||||
top : 29px;
|
.googleDriveStorage {
|
||||||
left : -20px;
|
position : relative;
|
||||||
z-index : 1000;
|
}
|
||||||
width : 120px;
|
.googleDriveStorage img{
|
||||||
padding : 8px;
|
height : 20px;
|
||||||
background-color : #333;
|
padding : 0px;
|
||||||
a{
|
margin : -5px;
|
||||||
color : @teal;
|
}
|
||||||
}
|
.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const ErrorPage = createClass({
|
|||||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='errorPage page'>
|
return <div className='errorPage sitePage'>
|
||||||
<Navbar ver={this.props.ver}>
|
<Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='errorTitle'>
|
<Nav.item className='errorTitle'>
|
||||||
|
|||||||
@@ -8,7 +8,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 PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const IssueNavItem = require('../../navbar/issue.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 AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
@@ -21,23 +21,25 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
|
|
||||||
const HomePage = createClass({
|
const HomePage = createClass({
|
||||||
|
displayName : 'HomePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
welcomeText : '',
|
brew : {
|
||||||
ver : '0.0.0'
|
text : '',
|
||||||
|
},
|
||||||
|
ver : '0.0.0'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
text : this.props.welcomeText
|
brew : this.props.brew,
|
||||||
|
welcomeText : this.props.brew.text
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send({
|
.send({
|
||||||
text : this.state.text
|
text : this.state.brew.text
|
||||||
})
|
})
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err) return;
|
if(err) return;
|
||||||
@@ -49,47 +51,45 @@ const HomePage = createClass({
|
|||||||
this.refs.editor.update();
|
this.refs.editor.update();
|
||||||
},
|
},
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState({
|
this.setState((prevState)=>({
|
||||||
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>
|
||||||
<PatreonNavItem />
|
<NewBrewItem />
|
||||||
<IssueNavItem />
|
<IssueNavItem />
|
||||||
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'>
|
|
||||||
Changelog
|
|
||||||
</Nav.item>
|
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
{/*}
|
|
||||||
<Nav.item href='/new' color='green' icon='fa-external-link'>
|
|
||||||
New Brew
|
|
||||||
</Nav.item>
|
|
||||||
*/}
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>;
|
</Navbar>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='homePage page'>
|
return <div className='homePage sitePage'>
|
||||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||||
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
|
<Editor
|
||||||
<BrewRenderer text={this.state.text} />
|
ref='editor'
|
||||||
|
brew={this.state.brew}
|
||||||
|
onTextChange={this.handleTextChange}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
|
showEditButtons={false}
|
||||||
|
/>
|
||||||
|
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
|
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
||||||
Save current <i className='fa fa-save' />
|
Save current <i className='fas fa-save' />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href='/new' className='floatingNewButton'>
|
<a href='/new' className='floatingNewButton'>
|
||||||
Create your own <i className='fa fa-magic' />
|
Create your own <i className='fas fa-magic' />
|
||||||
</a>
|
</a>
|
||||||
</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;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Welcome traveler from an antique land. Please sit and tell us of what you have s
|
|||||||
### 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 +14,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,22 +36,28 @@ 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/).
|
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).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -96,5 +102,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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
171
client/homebrew/pages/homePage/welcome_msg_v3.md
Normal file
171
client/homebrew/pages/homePage/welcome_msg_v3.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
```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
|
||||||
|
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*).
|
||||||
|
|
||||||
|
Much of the syntax and styling has changed in V3. Code in one version may be broken in the other, and updating an older brew to V3 will require more than just a copy and paste. *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
|
||||||
|
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).
|
||||||
|
|
||||||
|
|
||||||
|
\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
|
||||||
|
V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||||
|
|
||||||
|
**Senses** :: Here is some text that is long and overflows into a second line, creating a "hanging indent".
|
||||||
|
|
||||||
|
### 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\*. Images can be included using Markdown-style images.
|
||||||
|
|
||||||
|
Using *Curly Injection* you can assign an id, classes, or specific inline CSS properties to the image.
|
||||||
|
|
||||||
|
 {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}}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./newPage.less');
|
require('./newPage.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 request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
@@ -17,33 +17,73 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const BREWKEY = 'homebrewery-new';
|
||||||
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
|
||||||
const KEY = 'homebrewery-new';
|
|
||||||
|
|
||||||
const NewPage = createClass({
|
const NewPage = createClass({
|
||||||
getInitialState : function() {
|
displayName : 'NewPage',
|
||||||
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
metadata : {
|
brew : {
|
||||||
|
text : '',
|
||||||
|
style : undefined,
|
||||||
|
shareId : null,
|
||||||
|
editId : null,
|
||||||
|
createdAt : null,
|
||||||
|
updatedAt : null,
|
||||||
|
gDrive : false,
|
||||||
|
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
tags : '',
|
tags : '',
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : []
|
systems : []
|
||||||
},
|
}
|
||||||
|
|
||||||
text : '',
|
|
||||||
isSaving : false,
|
|
||||||
errors : []
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
componentDidMount : function() {
|
|
||||||
const storage = localStorage.getItem(KEY);
|
getInitialState : function() {
|
||||||
if(storage){
|
const brew = this.props.brew;
|
||||||
this.setState({
|
|
||||||
text : storage
|
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 {
|
||||||
|
brew : {
|
||||||
|
text : brew.text || '',
|
||||||
|
style : brew.style || undefined,
|
||||||
|
gDrive : false,
|
||||||
|
title : brew.title || '',
|
||||||
|
description : brew.description || '',
|
||||||
|
tags : brew.tags || '',
|
||||||
|
published : false,
|
||||||
|
authors : [],
|
||||||
|
systems : brew.systems || [],
|
||||||
|
renderer : brew.renderer || 'legacy'
|
||||||
|
},
|
||||||
|
|
||||||
|
isSaving : false,
|
||||||
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
|
errors : null,
|
||||||
|
htmlErrors : Markdown.validate(brew.text)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount : function() {
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -66,29 +106,81 @@ const NewPage = createClass({
|
|||||||
this.refs.editor.update();
|
this.refs.editor.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetadataChange : function(metadata){
|
|
||||||
this.setState({
|
|
||||||
metadata : _.merge({}, this.state.metadata, metadata)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState({
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
text : text,
|
let htmlErrors = this.state.htmlErrors;
|
||||||
errors : Markdown.validate(text)
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
});
|
|
||||||
localStorage.setItem(KEY, text);
|
this.setState((prevState)=>({
|
||||||
|
brew : _.merge({}, prevState.brew, { text: text }),
|
||||||
|
htmlErrors : htmlErrors
|
||||||
|
}));
|
||||||
|
localStorage.setItem(BREWKEY, text);
|
||||||
},
|
},
|
||||||
|
|
||||||
save : function(){
|
handleStyleChange : function(style){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brew : _.merge({}, prevState.brew, { style: style }),
|
||||||
|
}));
|
||||||
|
localStorage.setItem(STYLEKEY, style);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleMetaChange : function(metadata){
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
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(){
|
||||||
this.setState({
|
this.setState({
|
||||||
isSaving : true
|
isSaving : true
|
||||||
});
|
});
|
||||||
|
|
||||||
request.post('/api')
|
console.log('saving new brew');
|
||||||
.send(_.merge({}, this.state.metadata, {
|
|
||||||
text : this.state.text
|
let brew = this.state.brew;
|
||||||
}))
|
// Split out CSS to Style if CSS codefence exists
|
||||||
|
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||||
|
const index = brew.text.indexOf('```\n\n');
|
||||||
|
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
||||||
|
brew.text = brew.text.slice(index + 5);
|
||||||
|
};
|
||||||
|
|
||||||
|
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
|
||||||
|
if(this.state.saveGoogle) {
|
||||||
|
const res = await request
|
||||||
|
.post('/api/newGoogle/')
|
||||||
|
.send(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log(err.status === 401
|
||||||
|
? 'Not signed in!'
|
||||||
|
: 'Error Creating New Google Brew!');
|
||||||
|
this.setState({ isSaving: false, errors: err });
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
brew = res.body;
|
||||||
|
localStorage.removeItem(BREWKEY);
|
||||||
|
localStorage.removeItem(STYLEKEY);
|
||||||
|
localStorage.removeItem(METAKEY);
|
||||||
|
window.location = `/edit/${brew.googleId}${brew.editId}`;
|
||||||
|
} else {
|
||||||
|
request.post('/api')
|
||||||
|
.send(brew)
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err){
|
if(err){
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -97,31 +189,94 @@ const NewPage = createClass({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.onbeforeunload = function(){};
|
window.onbeforeunload = function(){};
|
||||||
const brew = res.body;
|
brew = res.body;
|
||||||
localStorage.removeItem(KEY);
|
localStorage.removeItem(BREWKEY);
|
||||||
|
localStorage.removeItem(STYLEKEY);
|
||||||
|
localStorage.removeItem(METAKEY);
|
||||||
window.location = `/edit/${brew.editId}`;
|
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='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='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', this.state.text);
|
|
||||||
window.open('/print?dialog=true&local=print', '_blank');
|
window.open('/print?dialog=true&local=print', '_blank');
|
||||||
},
|
},
|
||||||
|
|
||||||
renderLocalPrintButton : function(){
|
renderLocalPrintButton : function(){
|
||||||
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}>
|
return <Nav.item color='purple' icon='far fa-file-pdf' onClick={this.print}>
|
||||||
get PDF
|
get PDF
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
},
|
},
|
||||||
@@ -130,7 +285,7 @@ const NewPage = createClass({
|
|||||||
return <Navbar>
|
return <Navbar>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{this.state.metadata.title}</Nav.item>
|
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
@@ -144,18 +299,19 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='newPage page'>
|
return <div className='newPage sitePage'>
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||||
<Editor
|
<Editor
|
||||||
ref='editor'
|
ref='editor'
|
||||||
value={this.state.text}
|
brew={this.state.brew}
|
||||||
onChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
metadata={this.state.metadata}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetadataChange={this.handleMetadataChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.text} errors={this.state.errors} />
|
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors}/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,48 +4,92 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
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 : {},
|
||||||
brew : {
|
brew : {
|
||||||
text : '',
|
text : '',
|
||||||
|
style : '',
|
||||||
|
renderer : 'legacy'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
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(){
|
||||||
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
|
if(this.state.brew.renderer == 'legacy') {
|
||||||
return <div
|
return _.map(this.state.brew.text.split('\\page'), (pageText, index)=>{
|
||||||
className='phb'
|
return <div
|
||||||
id={`p${index + 1}`}
|
className='phb page'
|
||||||
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
|
id={`p${index + 1}`}
|
||||||
key={index} />;
|
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }}
|
||||||
});
|
key={index} />;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return _.map(this.state.brew.text.split(/^\\page$/gm), (pageText, 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div>
|
return <div>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
{this.renderPages()}
|
<link href={`${this.state.brew.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||||
|
{/* Apply CSS from Style tab */}
|
||||||
|
{this.renderStyle()}
|
||||||
|
<div className='pages' ref='pages'>
|
||||||
|
{this.renderPages()}
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
require('./sharePage.less');
|
require('./sharePage.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 { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -16,15 +14,18 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = createClass({
|
||||||
|
displayName : 'SharePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
title : '',
|
title : '',
|
||||||
text : '',
|
text : '',
|
||||||
|
style : '',
|
||||||
shareId : null,
|
shareId : null,
|
||||||
createdAt : null,
|
createdAt : null,
|
||||||
updatedAt : null,
|
updatedAt : null,
|
||||||
views : 0
|
views : 0,
|
||||||
|
renderer : ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -32,21 +33,29 @@ const SharePage = createClass({
|
|||||||
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();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
processShareId : function() {
|
||||||
|
return this.props.brew.googleId ?
|
||||||
|
this.props.brew.googleId + this.props.brew.shareId :
|
||||||
|
this.props.brew.shareId;
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='sharePage page'>
|
return <div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
@@ -54,17 +63,30 @@ const SharePage = createClass({
|
|||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<PrintLink shareId={this.props.brew.shareId} />
|
{this.props.brew.shareId && <>
|
||||||
<Nav.item href={`/source/${this.props.brew.shareId}`} color='teal' icon='fa-code'>
|
<PrintLink shareId={this.processShareId()} />
|
||||||
source
|
<Nav.dropdown>
|
||||||
</Nav.item>
|
<Nav.item color='red' icon='fas fa-code'>
|
||||||
|
source
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' href={`/source/${this.processShareId()}`}>
|
||||||
|
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 />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<BrewRenderer text={this.props.brew.text} />
|
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +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 dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
|
displayName : 'BrewItem',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
@@ -27,52 +31,111 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.delete(`/api/${this.props.brew.editId}`)
|
if(this.props.brew.googleId) {
|
||||||
.send()
|
request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`)
|
||||||
.end(function(err, res){
|
.send()
|
||||||
location.reload();
|
.end(function(err, res){
|
||||||
});
|
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='fa fa-trash' />
|
<i className='fas fa-trash-alt' title='Delete' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderEditLink : function(){
|
renderEditLink : function(){
|
||||||
if(!this.props.brew.editId) return;
|
if(!this.props.brew.editId) return;
|
||||||
|
|
||||||
return <a href={`/edit/${this.props.brew.editId}`} target='_blank' rel='noopener noreferrer'>
|
let editLink = this.props.brew.editId;
|
||||||
<i className='fa fa-pencil' />
|
if(this.props.brew.googleId) {
|
||||||
|
editLink = this.props.brew.googleId + editLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<i className='fas fa-pencil-alt' title='Edit' />
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderShareLink : function(){
|
||||||
|
if(!this.props.brew.shareId) return;
|
||||||
|
|
||||||
|
let shareLink = this.props.brew.shareId;
|
||||||
|
if(this.props.brew.googleId) {
|
||||||
|
shareLink = this.props.brew.googleId + shareLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
|
<i className='fas fa-share-alt' title='Share' />
|
||||||
|
</a>;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDownloadLink : function(){
|
||||||
|
if(!this.props.brew.shareId) return;
|
||||||
|
|
||||||
|
let shareLink = this.props.brew.shareId;
|
||||||
|
if(this.props.brew.googleId) {
|
||||||
|
shareLink = this.props.brew.googleId + shareLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <a className='downloadLink' href={`/download/${shareLink}`}>
|
||||||
|
<i className='fas fa-download' title='Download' />
|
||||||
|
</a>;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderGoogleDriveIcon : function(){
|
||||||
|
if(!this.props.brew.gDrive) return;
|
||||||
|
|
||||||
|
return <span>
|
||||||
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
|
</span>;
|
||||||
|
},
|
||||||
|
|
||||||
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='fa fa-user' /> {brew.authors.join(', ')}
|
<i className='fas fa-user'/> {brew.authors.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<br />
|
||||||
<i className='fa 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 &&
|
||||||
<i className='fa fa-refresh' /> {moment(brew.updatedAt).fromNow()}
|
<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()}
|
||||||
</span>
|
</span>
|
||||||
|
{this.renderGoogleDriveIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='links'>
|
<div className='links'>
|
||||||
<a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>
|
{this.renderShareLink()}
|
||||||
<i className='fa fa-share-alt' />
|
|
||||||
</a>
|
|
||||||
{this.renderEditLink()}
|
{this.renderEditLink()}
|
||||||
|
{this.renderDownloadLink()}
|
||||||
{this.renderDeleteBrewLink()}
|
{this.renderDeleteBrewLink()}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -7,24 +7,31 @@
|
|||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
width : 48%;
|
width : 48%;
|
||||||
|
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{
|
.text {
|
||||||
margin-bottom : 5px;
|
min-height : 54px;
|
||||||
font-size : 2.2em;
|
h4{
|
||||||
|
margin-bottom : 5px;
|
||||||
|
font-size : 2.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.info{
|
.info{
|
||||||
|
position: initial;
|
||||||
|
bottom: 2px;
|
||||||
font-family : ScalySans;
|
font-family : ScalySans;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
&>span{
|
&>span{
|
||||||
margin-right : 15px;
|
margin-right : 12px;
|
||||||
|
line-height : 1.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
@@ -55,6 +62,14 @@
|
|||||||
&:hover{
|
&:hover{
|
||||||
opacity : 1;
|
opacity : 1;
|
||||||
}
|
}
|
||||||
|
i{
|
||||||
|
cursor : pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.googleDriveIcon {
|
||||||
|
height : 20px;
|
||||||
|
padding : 0px;
|
||||||
|
margin : -5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,16 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
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 BrewItem = require('./brewItem/brewItem.jsx');
|
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||||
|
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||||
|
|
||||||
// const brew = {
|
// const brew = {
|
||||||
// title : 'SUPER Long title woah now',
|
// title : 'SUPER Long title woah now',
|
||||||
@@ -20,25 +24,134 @@ const BrewItem = require('./brewItem/brewItem.jsx');
|
|||||||
|
|
||||||
|
|
||||||
const UserPage = createClass({
|
const UserPage = createClass({
|
||||||
|
displayName : 'UserPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : []
|
brews : [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
sortType : 'alpha',
|
||||||
|
sortDir : 'asc',
|
||||||
|
filterString : ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getUsernameWithS : function() {
|
||||||
|
if(this.props.username.endsWith('s'))
|
||||||
|
return `${this.props.username}'`;
|
||||||
|
return `${this.props.username}'s`;
|
||||||
|
},
|
||||||
|
|
||||||
renderBrews : function(brews){
|
renderBrews : function(brews){
|
||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
const sortedBrews = _.sortBy(brews, (brew)=>{ return brew.title; });
|
const sortedBrews = this.sortBrews(brews);
|
||||||
|
|
||||||
return _.map(sortedBrews, (brew, idx)=>{
|
return _.map(sortedBrews, (brew, idx)=>{
|
||||||
return <BrewItem brew={brew} key={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];
|
||||||
|
},
|
||||||
|
|
||||||
|
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={`sortOption ${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||||
|
>
|
||||||
|
{`${sortTitle}`}
|
||||||
|
</button>
|
||||||
|
</td>;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFilterTextChange : function(e){
|
||||||
|
this.setState({
|
||||||
|
filterString : e.target.value
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderFilterOption : function(){
|
||||||
|
return <td>
|
||||||
|
<label className='filterOption'>
|
||||||
|
<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(){
|
getSortedBrews : function(){
|
||||||
return _.groupBy(this.props.brews, (brew)=>{
|
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||||
|
const brewCollection = this.state.filterString ? _.filter(this.props.brews, (brew)=>{
|
||||||
|
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
||||||
|
(_.deburr(brew.description).toLowerCase().includes(testString));
|
||||||
|
}) : this.props.brews;
|
||||||
|
return _.groupBy(brewCollection, (brew)=>{
|
||||||
return (brew.published ? 'published' : 'private');
|
return (brew.published ? 'published' : 'private');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -46,24 +159,30 @@ const UserPage = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
const brews = this.getSortedBrews();
|
const brews = this.getSortedBrews();
|
||||||
|
|
||||||
return <div className='userPage page'>
|
return <div className='userPage sitePage'>
|
||||||
|
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
|
<ReportIssue />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content V3'>
|
||||||
<div className='phb'>
|
<div className='phb'>
|
||||||
<div>
|
{this.renderSortOptions()}
|
||||||
<h1>{this.props.username}'s brews</h1>
|
<div className='published'>
|
||||||
|
<h1>{this.getUsernameWithS()} published brews</h1>
|
||||||
{this.renderBrews(brews.published)}
|
{this.renderBrews(brews.published)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{this.props.username == global.account?.username &&
|
||||||
<h1>{this.props.username}'s unpublished brews</h1>
|
<div className='unpublished'>
|
||||||
{this.renderBrews(brews.private)}
|
<h1>{this.getUsernameWithS()} unpublished brews</h1>
|
||||||
</div>
|
{this.renderBrews(brews.private)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -30,4 +30,48 @@
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sort-container{
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
position : fixed;
|
||||||
|
top : 35px;
|
||||||
|
left : calc(50vw - 408px);
|
||||||
|
border : 2px solid #58180D;
|
||||||
|
width : 800px;
|
||||||
|
background-color : #EEE5CE;
|
||||||
|
padding : 2px;
|
||||||
|
text-align : center;
|
||||||
|
z-index : 15;
|
||||||
|
h6{
|
||||||
|
text-transform : uppercase;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 11px;
|
||||||
|
font-weight : bold;
|
||||||
|
color : #58180D;
|
||||||
|
}
|
||||||
|
table{
|
||||||
|
margin : 0px;
|
||||||
|
vertical-align : middle;
|
||||||
|
tbody tr{
|
||||||
|
background-color: transparent !important;
|
||||||
|
i{
|
||||||
|
padding-right : 5px
|
||||||
|
}
|
||||||
|
button{
|
||||||
|
background-color : transparent;
|
||||||
|
color : #58180D;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 11px;
|
||||||
|
text-transform : uppercase;
|
||||||
|
font-weight : normal;
|
||||||
|
&.active{
|
||||||
|
font-weight : bold;
|
||||||
|
border : 2px solid #58180D;
|
||||||
|
}
|
||||||
|
&.sortDir{
|
||||||
|
width : 75px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 327 B |
Binary file not shown.
|
Before Width: | Height: | Size: 530 B |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,19 +1,19 @@
|
|||||||
module.exports = async (name, props={})=>{
|
module.exports = async(name, title = '', props = {})=>{
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
|
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} rel='stylesheet'></link>
|
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||||
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
|
||||||
<title>The Homebrewery - NaturalCrit</title>
|
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||||
|
<script src=${`/${name}/bundle.js`}></script>
|
||||||
|
<script>start_app(${JSON.stringify(props)})</script>
|
||||||
</body>
|
</body>
|
||||||
<script src=${`/${name}/bundle.js`}></script>
|
|
||||||
<script>start_app(${JSON.stringify(props)})</script>
|
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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,
|
||||||
|
"enable_v3" : true
|
||||||
}
|
}
|
||||||
152
faq.md
Normal file
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.
|
||||||
35
install/README.FREEBSD.md
Normal file
35
install/README.FREEBSD.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# FreeBSD/FreeNAS Installation Instructions
|
||||||
|
|
||||||
|
## Before Installing
|
||||||
|
|
||||||
|
These instructions assume that you are installing to a completely new, fresh FreeBSD/FreeNAS jail. As such, some steps will not be necessary if you are installing to an existing FreeBSD/FreeNAS install.
|
||||||
|
|
||||||
|
## Installation instructions
|
||||||
|
|
||||||
|
1. Create a new jail, with the appropriate network settings to access the internet.
|
||||||
|
|
||||||
|
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/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`).
|
||||||
|
|
||||||
|
5. Run the script (`./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 FreeBSD/FreeNAS platforms:
|
||||||
|
|
||||||
|
* FreeNAS-11.3-U5; Jail 11.4-RELEASE-p2
|
||||||
|
|
||||||
|
## Final Notes
|
||||||
|
|
||||||
|
While this installation process works successfully at the time of writing (December 28, 2020), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function under FreeBSD. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation under FreeBSD may fail without warning at some point in the future.
|
||||||
|
|
||||||
|
Regards,
|
||||||
|
G
|
||||||
|
December 28, 2020
|
||||||
35
install/README.UBUNTU.md
Normal file
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
|
||||||
20
install/freebsd/install.sh
Normal file
20
install/freebsd/install.sh
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
pkg install -y git nano node npm mongodb44
|
||||||
|
|
||||||
|
sysrc mongod_enable=YES
|
||||||
|
service mongod start
|
||||||
|
|
||||||
|
cd /usr/local/
|
||||||
|
git clone https://github.com/naturalcrit/homebrewery.git
|
||||||
|
|
||||||
|
cd homebrewery
|
||||||
|
npm install
|
||||||
|
npm audit fix
|
||||||
|
npm run postinstall
|
||||||
|
|
||||||
|
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
|
||||||
|
chmod +x /usr/local/etc/rc.d/homebrewery
|
||||||
|
|
||||||
|
sysrc homebrewery_enable=YES
|
||||||
|
service homebrewery start
|
||||||
65
install/freebsd/rc.d/homebrewery
Normal file
65
install/freebsd/rc.d/homebrewery
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
# PROVIDE: homebrewery
|
||||||
|
# REQUIRE: NETWORKING
|
||||||
|
# KEYWORD: shutdown
|
||||||
|
|
||||||
|
# Author: S Robertson
|
||||||
|
# Version: 1.0.0
|
||||||
|
|
||||||
|
# Description:
|
||||||
|
# This script runs HomeBrewery as a service under the supplied user on boot
|
||||||
|
|
||||||
|
# How to use:
|
||||||
|
# Place this file in /usr/local/etc/rc.d/
|
||||||
|
# Add homebrewery_enable="YES" to /etc/rc.config
|
||||||
|
# (Optional) To run as non-root, add homebrewery_runAs="homebrewery" to /etc/rc.config
|
||||||
|
# (Optional) To pass HomeBrewery args, add homebrewery_args="" to /etc/rc.config
|
||||||
|
|
||||||
|
# Freebsd rc library
|
||||||
|
. /etc/rc.subr
|
||||||
|
|
||||||
|
# General Info
|
||||||
|
name="homebrewery" # Safe name of program
|
||||||
|
location="/usr/local/" # Install location
|
||||||
|
program_name="homebrewery" # Name of exec
|
||||||
|
title="HomeBrewery" # Title to display in top/htop
|
||||||
|
|
||||||
|
# RC.config vars
|
||||||
|
load_rc_config $name # Loading rc config vars
|
||||||
|
: ${homebrewery_enable="NO"} # Default: Do not enable HomeBrewery
|
||||||
|
: ${homebrewery_runAs="root"} # Default: Run HomeBrewery as root
|
||||||
|
: ${homebrewery_port=8000} # Default: Run HomeBrewery on port 8000
|
||||||
|
: ${homebrewery_NODE_ENV="local"} # Default: Run HomeBrewery in local mode
|
||||||
|
|
||||||
|
# Freebsd Setup
|
||||||
|
rcvar=homebrewery_enable # Enables the rc.conf YES/NO flag
|
||||||
|
pidfile="/var/run/${program_name}.pid" # File that allows the system to keep track of HomeBrewery status
|
||||||
|
|
||||||
|
# Env Setup
|
||||||
|
export HOME=$( getent passwd "homebrewery_runAs" | cut -d: -f6 ) # Gets the home directory of the runAs user
|
||||||
|
export NODE_ENV=${homebrewery_NODE_ENV}
|
||||||
|
export PORT=${homebrewery_port}
|
||||||
|
|
||||||
|
# Command Setup
|
||||||
|
exec_cmd="${location}/${program_name}/server.js" # Path to the HomeBrewery server.js, /usr/local/bin/ when installed globally
|
||||||
|
output_file="/var/log/${program_name}.log" # Path to HomeBrewery output file
|
||||||
|
|
||||||
|
# Command
|
||||||
|
command="/usr/sbin/daemon"
|
||||||
|
command_args="-r -t ${title} -u ${homebrewery_runAs} -o ${output_file} -P ${pidfile} /usr/local/bin/node ${exec_cmd} ${homebrewery_args}"
|
||||||
|
|
||||||
|
# Extra Commands
|
||||||
|
extra_commands="dev_mode"
|
||||||
|
|
||||||
|
dev_mode_cmd="homebrewery_dev_mode"
|
||||||
|
|
||||||
|
homebrewery_dev_mode() {
|
||||||
|
echo "Starting HomeBrewery in live rebuild Developer mode..."
|
||||||
|
cd ${location}/${program_name}/
|
||||||
|
/usr/local/bin/node ${location}/${program_name}/scripts/buildHomebrew.js --dev
|
||||||
|
}
|
||||||
|
|
||||||
|
# Loading Config
|
||||||
|
load_rc_config ${name}
|
||||||
|
run_rc_command "$1"
|
||||||
13
install/ubuntu/etc/systemd/system/homebrewery.service
Normal file
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
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
|
||||||
21442
package-lock.json
generated
21442
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
88
package.json
88
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.8.2",
|
"version": "3.0.6",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.16.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -18,8 +18,8 @@
|
|||||||
"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",
|
||||||
"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,45 +30,63 @@
|
|||||||
"eslintIgnore": [
|
"eslintIgnore": [
|
||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"pico-check": {
|
"jest": {
|
||||||
"require": "./tests/test.init.js"
|
"modulePaths": [
|
||||||
|
"mode_modules",
|
||||||
|
"shared",
|
||||||
|
"server"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
"env",
|
"@babel/preset-env",
|
||||||
"react"
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-transform-runtime"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.16.12",
|
||||||
"@babel/preset-env": "^7.9.6",
|
"@babel/plugin-transform-runtime": "^7.16.10",
|
||||||
"@babel/preset-react": "^7.9.4",
|
"@babel/preset-env": "^7.16.11",
|
||||||
"body-parser": "^1.19.0",
|
"@babel/preset-react": "^7.16.7",
|
||||||
"classnames": "^2.2.6",
|
"body-parser": "^1.19.1",
|
||||||
"codemirror": "^5.54.0",
|
"classnames": "^2.3.1",
|
||||||
"cookie-parser": "^1.4.5",
|
"codemirror": "^5.65.1",
|
||||||
"create-react-class": "^15.6.3",
|
"cookie-parser": "^1.4.6",
|
||||||
"express": "^4.17.1",
|
"create-react-class": "^15.7.0",
|
||||||
"fs-extra": "9.0.0",
|
"dedent-tabs": "^0.10.1",
|
||||||
|
"express": "^4.17.2",
|
||||||
|
"express-async-handler": "^1.2.0",
|
||||||
|
"express-static-gzip": "2.1.1",
|
||||||
|
"fs-extra": "10.0.0",
|
||||||
|
"googleapis": "92.0.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.11.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.21",
|
||||||
"marked": "^0.3.19",
|
"marked": "4.0.11",
|
||||||
"moment": "^2.26.0",
|
"marked-extended-tables": "^1.0.3",
|
||||||
"mongoose": "^5.9.15",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"nconf": "^0.10.0",
|
"moment": "^2.29.1",
|
||||||
"prop-types": "15.7.2",
|
"mongoose": "^6.1.8",
|
||||||
"query-string": "6.12.1",
|
"nanoid": "3.2.0",
|
||||||
"react": "^16.13.1",
|
"nconf": "^0.11.3",
|
||||||
"react-dom": "^16.13.1",
|
"prop-types": "15.8.0",
|
||||||
"react-router-dom": "5.2.0",
|
"query-string": "7.1.0",
|
||||||
"shortid": "^2.2.15",
|
"react": "^16.14.0",
|
||||||
"superagent": "^5.2.2",
|
"react-dom": "^16.14.0",
|
||||||
"vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b"
|
"react-frame-component": "5.2.2-alpha.0",
|
||||||
|
"react-router-dom": "5.3.0",
|
||||||
|
"sanitize-filename": "1.6.3",
|
||||||
|
"superagent": "^6.1.0",
|
||||||
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^7.0.0",
|
"eslint": "^8.7.0",
|
||||||
"eslint-plugin-react": "^7.20.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"pico-check": "^1.3.2"
|
"jest": "^27.4.5",
|
||||||
|
"supertest": "^6.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
# Notes
|
# Notes
|
||||||
User-agent: *
|
User-agent: *
|
||||||
|
Disallow: /edit/
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -1,26 +1,64 @@
|
|||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const zlib = require('zlib');
|
||||||
const Proj = require('./project.json');
|
const Proj = require('./project.json');
|
||||||
|
|
||||||
const { pack } = require('vitreum');
|
const { pack, watchFile, livereload } = require('vitreum');
|
||||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||||
|
|
||||||
const lessTransform = require('vitreum/transforms/less.js');
|
const lessTransform = require('vitreum/transforms/less.js');
|
||||||
const assetTransform = require('vitreum/transforms/asset.js');
|
const assetTransform = require('vitreum/transforms/asset.js');
|
||||||
//const Meta = require('vitreum/headtags');
|
const babel = require('@babel/core');
|
||||||
|
const less = require('less');
|
||||||
|
|
||||||
|
const babelify = async (code)=>(await babel.transformAsync(code, { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
||||||
|
|
||||||
const transforms = {
|
const transforms = {
|
||||||
|
'.js' : (code, filename, opts)=>babelify(code),
|
||||||
|
'.jsx' : (code, filename, opts)=>babelify(code),
|
||||||
'.less' : lessTransform,
|
'.less' : lessTransform,
|
||||||
'*' : assetTransform('./build')
|
'*' : assetTransform('./build')
|
||||||
};
|
};
|
||||||
|
|
||||||
const build = async ({ bundle, render, ssr })=>{
|
const build = async ({ bundle, render, ssr })=>{
|
||||||
await fs.outputFile('./build/homebrew/bundle.css', await lessTransform.generate({ paths: './shared' }));
|
const css = await lessTransform.generate({ paths: './shared' });
|
||||||
|
await fs.outputFile('./build/homebrew/bundle.css', css);
|
||||||
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.outputFile('./build/homebrew/render.js', render);
|
await fs.copy('./themes/fonts', './build/fonts');
|
||||||
|
let src = './themes/5ePhbLegacy.style.less';
|
||||||
|
//Parse brew theme files
|
||||||
|
less.render(fs.readFileSync(src).toString(), {
|
||||||
|
compress : !isDev
|
||||||
|
}, function(e, output) {
|
||||||
|
fs.outputFile('./build/themes/5ePhbLegacy.style.css', output.css);
|
||||||
|
});
|
||||||
|
src = './themes/5ePhb.style.less';
|
||||||
|
less.render(fs.readFileSync(src).toString(), {
|
||||||
|
compress : !isDev
|
||||||
|
}, function(e, output) {
|
||||||
|
fs.outputFile('./build/themes/5ePhb.style.css', output.css);
|
||||||
|
});
|
||||||
|
// await less.render(lessCode, {
|
||||||
|
// compress : !dev,
|
||||||
|
// sourceMap : (dev ? {
|
||||||
|
// sourceMapFileInline: true,
|
||||||
|
// outputSourceFiles: true
|
||||||
|
// } : false),
|
||||||
|
// })
|
||||||
|
|
||||||
|
//compress files in production
|
||||||
|
if(!isDev){
|
||||||
|
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
|
||||||
|
await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
|
||||||
|
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
|
||||||
|
} else {
|
||||||
|
await fs.remove('./build/homebrew/bundle.css.br');
|
||||||
|
await fs.remove('./build/homebrew/bundle.js.br');
|
||||||
|
await fs.remove('./build/homebrew/ssr.js.br');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.emptyDirSync('./build/homebrew');
|
fs.emptyDirSync('./build');
|
||||||
pack('./client/homebrew/homebrew.jsx', {
|
pack('./client/homebrew/homebrew.jsx', {
|
||||||
paths : ['./shared'],
|
paths : ['./shared'],
|
||||||
libs : Proj.libs,
|
libs : Proj.libs,
|
||||||
@@ -29,3 +67,12 @@ pack('./client/homebrew/homebrew.jsx', {
|
|||||||
})
|
})
|
||||||
.then(build)
|
.then(build)
|
||||||
.catch(console.error);
|
.catch(console.error);
|
||||||
|
|
||||||
|
|
||||||
|
//In development set up a watch server and livereload
|
||||||
|
if(isDev){
|
||||||
|
livereload('./build');
|
||||||
|
watchFile('./server.js', {
|
||||||
|
watch : ['./client'] // Watch additional folders if you want
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,20 @@
|
|||||||
"classnames",
|
"classnames",
|
||||||
"codemirror",
|
"codemirror",
|
||||||
"codemirror/mode/gfm/gfm.js",
|
"codemirror/mode/gfm/gfm.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"
|
||||||
|
|||||||
148
server.js
148
server.js
@@ -1,12 +1,5 @@
|
|||||||
const _ = require('lodash');
|
const DB = require('./server/db.js');
|
||||||
const jwt = require('jwt-simple');
|
const server = require('./server/app.js');
|
||||||
const express = require('express');
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
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')
|
const config = require('nconf')
|
||||||
.argv()
|
.argv()
|
||||||
@@ -14,134 +7,11 @@ const config = require('nconf')
|
|||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||||
.file('defaults', { file: 'config/default.json' });
|
.file('defaults', { file: 'config/default.json' });
|
||||||
|
|
||||||
//DB
|
DB.connect(config).then(()=>{
|
||||||
const mongoose = require('mongoose');
|
// Ensure that we have successfully connected to the database
|
||||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
|
// before launching server
|
||||||
{ retryWrites: false, useNewUrlParser: true });
|
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||||
mongoose.connection.on('error', ()=>{
|
server.app.listen(PORT, ()=>{
|
||||||
console.log('Error : Could not connect to a Mongo Database.');
|
console.log(`server on port: ${PORT}`);
|
||||||
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'));
|
|
||||||
} catch (e){}
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
app.use(require('./server/homebrew.api.js'));
|
|
||||||
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`);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Source page
|
|
||||||
app.get('/source/:id', (req, res)=>{
|
|
||||||
HomebrewModel.get({ shareId: req.params.id })
|
|
||||||
.then((brew)=>{
|
|
||||||
const text = brew.text.replaceAll('<', '<').replaceAll('>', '>');
|
|
||||||
return res.send(`<code><pre style="white-space: pre-wrap;">${text}</pre></code>`);
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
return res.status(404).send('Could not find Homebrew with that id');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//User Page
|
|
||||||
app.get('/user/:username', (req, res, next)=>{
|
|
||||||
const fullAccess = req.account && (req.account.username == req.params.username);
|
|
||||||
HomebrewModel.getByUser(req.params.username, fullAccess)
|
|
||||||
.then((brews)=>{
|
|
||||||
req.brews = brews;
|
|
||||||
return next();
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Edit Page
|
|
||||||
app.get('/edit/:id', (req, res, next)=>{
|
|
||||||
HomebrewModel.get({ editId: req.params.id })
|
|
||||||
.then((brew)=>{
|
|
||||||
req.brew = brew.sanatize();
|
|
||||||
return next();
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
return res.status(400).send(`Can't get that`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Share Page
|
|
||||||
app.get('/share/:id', (req, res, next)=>{
|
|
||||||
HomebrewModel.get({ shareId: req.params.id })
|
|
||||||
.then((brew)=>{
|
|
||||||
return brew.increaseView();
|
|
||||||
})
|
|
||||||
.then((brew)=>{
|
|
||||||
req.brew = brew.sanatize(true);
|
|
||||||
return next();
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
return res.status(400).send(`Can't get that`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
//Print Page
|
|
||||||
app.get('/print/:id', (req, res, next)=>{
|
|
||||||
HomebrewModel.get({ shareId: req.params.id })
|
|
||||||
.then((brew)=>{
|
|
||||||
req.brew = brew.sanatize(true);
|
|
||||||
return next();
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
return res.status(400).send(`Can't get that`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//Render the page
|
|
||||||
//const render = require('.build/render');
|
|
||||||
const templateFn = require('./client/template.js');
|
|
||||||
app.use((req, res)=>{
|
|
||||||
const props = {
|
|
||||||
version : require('./package.json').version,
|
|
||||||
url : req.originalUrl,
|
|
||||||
welcomeText : welcomeText,
|
|
||||||
changelog : changelogText,
|
|
||||||
brew : req.brew,
|
|
||||||
brews : req.brews,
|
|
||||||
account : req.account,
|
|
||||||
};
|
|
||||||
templateFn('homebrew', props)
|
|
||||||
.then((page)=>res.send(page))
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err);
|
|
||||||
return res.sendStatus(500);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const PORT = process.env.PORT || 8000;
|
|
||||||
app.listen(PORT);
|
|
||||||
console.log(`server on port:${PORT}`);
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const junkBrewQuery = HomebrewModel.find({
|
|||||||
|
|
||||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||||
const uncompressedBrewQuery = HomebrewModel.find({
|
const uncompressedBrewQuery = HomebrewModel.find({
|
||||||
'textBin' : null
|
'text' : { '$exists': true }
|
||||||
}).lean().limit(10000).select('_id');
|
}).lean().limit(10000).select('_id');
|
||||||
|
|
||||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||||
|
|||||||
299
server/app.js
Normal file
299
server/app.js
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
|
const _ = require('lodash');
|
||||||
|
const jwt = require('jwt-simple');
|
||||||
|
const express = require('express');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
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.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;
|
||||||
|
}
|
||||||
|
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(`${__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('./forcessl.mw.js'));
|
||||||
|
|
||||||
|
// FIXME: the config should be passed as an argument for the app
|
||||||
|
const config = require('nconf')
|
||||||
|
.argv()
|
||||||
|
.env({ lowerCase: true })
|
||||||
|
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||||
|
.file('defaults', { file: 'config/default.json' });
|
||||||
|
|
||||||
|
//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 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(`${__dirname}/robots.txt`);
|
||||||
|
});
|
||||||
|
|
||||||
|
//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();
|
||||||
|
});
|
||||||
|
|
||||||
|
//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 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')
|
||||||
|
};
|
||||||
|
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
|
||||||
|
};
|
||||||
37
server/db.js
Normal file
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
|
||||||
|
};
|
||||||
392
server/googleActions.js
Normal file
392
server/googleActions.js
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
const _ = require('lodash');
|
||||||
|
const { google } = require('googleapis');
|
||||||
|
const { nanoid } = require('nanoid');
|
||||||
|
const token = require('./token.js');
|
||||||
|
const config = require('nconf')
|
||||||
|
.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 GoogleActions = {
|
||||||
|
|
||||||
|
authCheck : (account, res)=>{
|
||||||
|
if(!account || !account.googleId){ // If not signed into Google
|
||||||
|
const err = new Error('Not Signed In');
|
||||||
|
err.status = 401;
|
||||||
|
throw (err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const oAuth2Client = new google.auth.OAuth2(
|
||||||
|
config.get('google_client_id'),
|
||||||
|
config.get('google_client_secret'),
|
||||||
|
'/auth/google/redirect'
|
||||||
|
);
|
||||||
|
|
||||||
|
oAuth2Client.setCredentials({
|
||||||
|
access_token : account.googleAccessToken, //Comment out to refresh token
|
||||||
|
refresh_token : account.googleRefreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
oAuth2Client.on('tokens', (tokens)=>{
|
||||||
|
if(tokens.refresh_token) {
|
||||||
|
account.googleRefreshToken = tokens.refresh_token;
|
||||||
|
}
|
||||||
|
account.googleAccessToken = tokens.access_token;
|
||||||
|
const JWTToken = token.generateAccessToken(account);
|
||||||
|
|
||||||
|
//Save updated token to cookie
|
||||||
|
//res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' });
|
||||||
|
res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax', domain: '.naturalcrit.com' });
|
||||||
|
});
|
||||||
|
|
||||||
|
return oAuth2Client;
|
||||||
|
},
|
||||||
|
|
||||||
|
getGoogleFolder : async (auth)=>{
|
||||||
|
const drive = google.drive({ version: 'v3', auth: auth });
|
||||||
|
|
||||||
|
fileMetadata = {
|
||||||
|
'name' : 'Homebrewery',
|
||||||
|
'mimeType' : 'application/vnd.google-apps.folder'
|
||||||
|
};
|
||||||
|
|
||||||
|
const obj = await drive.files.list({
|
||||||
|
q : 'mimeType = \'application/vnd.google-apps.folder\''
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error searching Google Drive Folders');
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
|
});
|
||||||
|
|
||||||
|
let folderId;
|
||||||
|
|
||||||
|
if(obj.data.files.length == 0){
|
||||||
|
const obj = await drive.files.create({
|
||||||
|
resource : fileMetadata
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error creating Google Drive folder');
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
|
});
|
||||||
|
|
||||||
|
folderId = obj.data.id;
|
||||||
|
} else {
|
||||||
|
folderId = obj.data.files[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return folderId;
|
||||||
|
},
|
||||||
|
|
||||||
|
listGoogleBrews : async (req, res)=>{
|
||||||
|
|
||||||
|
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({
|
||||||
|
pageSize : 1000,
|
||||||
|
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||||
|
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log(`Error Listing Google Brews`);
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
|
//TODO: Should break out here, but continues on for some reason.
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!obj.data.files.length) {
|
||||||
|
console.log('No files found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const brews = obj.data.files.map((file)=>{
|
||||||
|
return {
|
||||||
|
text : '',
|
||||||
|
shareId : file.properties.shareId,
|
||||||
|
editId : file.properties.editId,
|
||||||
|
createdAt : file.createdTime,
|
||||||
|
updatedAt : file.modifiedTime,
|
||||||
|
gDrive : true,
|
||||||
|
googleId : file.id,
|
||||||
|
pageCount : parseInt(file.properties.pageCount),
|
||||||
|
title : file.properties.title,
|
||||||
|
description : file.description,
|
||||||
|
views : parseInt(file.properties.views),
|
||||||
|
tags : '',
|
||||||
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
|
authors : [req.account.username], //TODO: properly save and load authors to google drive
|
||||||
|
systems : []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return brews;
|
||||||
|
},
|
||||||
|
|
||||||
|
existsGoogleBrew : async (auth, id)=>{
|
||||||
|
const drive = google.drive({ version: 'v3', auth: auth });
|
||||||
|
|
||||||
|
const result = await drive.files.get({ fileId: id })
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('error checking file exists...');
|
||||||
|
console.error(err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
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)=>{
|
||||||
|
console.log('Error saving to google');
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
|
//return res.status(500).send('Error while saving');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (brew);
|
||||||
|
},
|
||||||
|
|
||||||
|
newGoogleBrew : async (auth, brew)=>{
|
||||||
|
const drive = google.drive({ version: 'v3', auth: auth });
|
||||||
|
|
||||||
|
const media = {
|
||||||
|
mimeType : 'text/plain',
|
||||||
|
body : brew.text
|
||||||
|
};
|
||||||
|
|
||||||
|
const folderId = await GoogleActions.getGoogleFolder(auth);
|
||||||
|
|
||||||
|
const fileMetadata = {
|
||||||
|
'name' : `${brew.title}.txt`,
|
||||||
|
'description' : `${brew.description}`,
|
||||||
|
'parents' : [folderId],
|
||||||
|
'properties' : { //AppProperties is not accessible
|
||||||
|
'shareId' : nanoid(12),
|
||||||
|
'editId' : nanoid(12),
|
||||||
|
'title' : brew.title,
|
||||||
|
'views' : '0',
|
||||||
|
'pageCount' : brew.pageCount,
|
||||||
|
'renderer' : brew.renderer || 'legacy'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const obj = await drive.files.create({
|
||||||
|
resource : fileMetadata,
|
||||||
|
media : media
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error while creating new Google brew');
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!obj) return;
|
||||||
|
|
||||||
|
await drive.permissions.create({
|
||||||
|
resource : { type : 'anyone',
|
||||||
|
role : 'writer' },
|
||||||
|
fileId : obj.data.id,
|
||||||
|
fields : 'id',
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error updating permissions');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const newHomebrew = {
|
||||||
|
text : brew.text,
|
||||||
|
shareId : fileMetadata.properties.shareId,
|
||||||
|
editId : fileMetadata.properties.editId,
|
||||||
|
createdAt : new Date(),
|
||||||
|
updatedAt : new Date(),
|
||||||
|
gDrive : true,
|
||||||
|
googleId : obj.data.id,
|
||||||
|
pageCount : fileMetadata.properties.pageCount,
|
||||||
|
|
||||||
|
title : brew.title,
|
||||||
|
description : brew.description,
|
||||||
|
tags : '',
|
||||||
|
published : brew.published,
|
||||||
|
renderer : brew.renderer,
|
||||||
|
authors : [],
|
||||||
|
systems : []
|
||||||
|
};
|
||||||
|
|
||||||
|
return newHomebrew;
|
||||||
|
},
|
||||||
|
|
||||||
|
readFileMetadata : async (auth, id, accessId, accessType)=>{
|
||||||
|
|
||||||
|
const drive = google.drive({ version: 'v3', auth: auth });
|
||||||
|
|
||||||
|
const obj = await drive.files.get({
|
||||||
|
fileId : id,
|
||||||
|
fields : 'properties, createdTime, modifiedTime, description, trashed'
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error loading from Google');
|
||||||
|
throw (err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(obj) {
|
||||||
|
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
||||||
|
throw ('Edit ID does not match');
|
||||||
|
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
||||||
|
throw ('Share ID does not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
//Access file using service account. Using API key only causes "automated query" lockouts after a while.
|
||||||
|
|
||||||
|
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({
|
||||||
|
fileId : id,
|
||||||
|
fields : 'description, properties',
|
||||||
|
alt : 'media'
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error getting file contents from Google');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const brew = {
|
||||||
|
shareId : obj.data.properties.shareId,
|
||||||
|
editId : obj.data.properties.editId,
|
||||||
|
title : obj.data.properties.title,
|
||||||
|
text : file.data,
|
||||||
|
|
||||||
|
description : obj.data.description,
|
||||||
|
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
||||||
|
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||||
|
authors : [],
|
||||||
|
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||||
|
trashed : obj.data.trashed,
|
||||||
|
|
||||||
|
createdAt : obj.data.createdTime,
|
||||||
|
updatedAt : obj.data.modifiedTime,
|
||||||
|
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
|
||||||
|
version : parseInt(obj.data.properties.version) || 0,
|
||||||
|
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
||||||
|
|
||||||
|
gDrive : true,
|
||||||
|
googleId : id
|
||||||
|
};
|
||||||
|
|
||||||
|
return (brew);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteGoogleBrew : async (req, res, id)=>{
|
||||||
|
|
||||||
|
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
||||||
|
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
||||||
|
|
||||||
|
const googleId = id.slice(0, -12);
|
||||||
|
const accessId = id.slice(-12);
|
||||||
|
|
||||||
|
const obj = await drive.files.get({
|
||||||
|
fileId : googleId,
|
||||||
|
fields : 'properties'
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error loading from Google');
|
||||||
|
console.error(err);
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
if(obj && obj.data.properties.editId != accessId) {
|
||||||
|
throw ('Not authorized to delete this Google brew');
|
||||||
|
}
|
||||||
|
|
||||||
|
await drive.files.update({
|
||||||
|
fileId : googleId,
|
||||||
|
resource : { trashed: true }
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Can\'t delete Google file');
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send();
|
||||||
|
},
|
||||||
|
|
||||||
|
increaseView : async (id, accessId, accessType, brew)=>{
|
||||||
|
//service account because this is modifying another user's file properties
|
||||||
|
//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({
|
||||||
|
fileId : brew.googleId,
|
||||||
|
resource : {
|
||||||
|
modifiedTime : brew.updatedAt,
|
||||||
|
properties : {
|
||||||
|
views : brew.views + 1,
|
||||||
|
lastViewed : new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log('Error updating Google views');
|
||||||
|
console.error(err);
|
||||||
|
//return res.status(500).send('Error while saving');
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = GoogleActions;
|
||||||
@@ -2,6 +2,9 @@ const _ = require('lodash');
|
|||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
const zlib = require('zlib');
|
const zlib = require('zlib');
|
||||||
|
const GoogleActions = require('./googleActions.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) {
|
||||||
@@ -9,28 +12,54 @@ const zlib = require('zlib');
|
|||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const getGoodBrewTitle = (text)=>{
|
const mergeBrewText = (brew)=>{
|
||||||
const titlePos = text.indexOf('# ');
|
let text = brew.text;
|
||||||
if(titlePos !== -1) {
|
if(brew.style !== undefined) {
|
||||||
const ending = text.indexOf('\n', titlePos);
|
text = `\`\`\`css\n` +
|
||||||
return text.substring(titlePos + 2, ending);
|
`${brew.style || ''}\n` +
|
||||||
} else {
|
`\`\`\`\n\n` +
|
||||||
return _.find(text.split('\n'), (line)=>line);
|
`${text}`;
|
||||||
}
|
}
|
||||||
|
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer']);
|
||||||
|
text = `\`\`\`metadata\n` +
|
||||||
|
`${yaml.dump(metadata)}\n` +
|
||||||
|
`\`\`\`\n\n` +
|
||||||
|
`${text}`;
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
|
const getGoodBrewTitle = (text)=>{
|
||||||
|
const tokens = Markdown.marked.lexer(text);
|
||||||
|
return (tokens.find((token)=>token.type == 'heading' || token.type == 'paragraph')?.text || 'No Title')
|
||||||
|
.slice(0, MAX_TITLE_LENGTH);
|
||||||
|
};
|
||||||
|
|
||||||
|
const excludePropsFromUpdate = (brew)=>{
|
||||||
|
// Remove undesired properties
|
||||||
|
const propsToExclude = ['views', 'lastViewed'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
delete brew[prop];
|
||||||
|
};
|
||||||
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
const newBrew = (req, res)=>{
|
const newBrew = (req, res)=>{
|
||||||
const authors = (req.account) ? [req.account.username] : [];
|
const brew = req.body;
|
||||||
|
|
||||||
const newHomebrew = new HomebrewModel(_.merge({},
|
if(!brew.title) {
|
||||||
req.body,
|
brew.title = getGoodBrewTitle(brew.text);
|
||||||
{ authors: authors }
|
|
||||||
));
|
|
||||||
|
|
||||||
if(!newHomebrew.title) {
|
|
||||||
newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
brew.authors = (req.account) ? [req.account.username] : [];
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
|
delete brew.editId;
|
||||||
|
delete brew.shareId;
|
||||||
|
delete brew.googleId;
|
||||||
|
|
||||||
|
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
|
||||||
@@ -41,16 +70,22 @@ const newBrew = (req, res)=>{
|
|||||||
console.error(err, err.toString(), err.stack);
|
console.error(err, err.toString(), err.stack);
|
||||||
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
||||||
}
|
}
|
||||||
return res.json(obj);
|
|
||||||
|
obj = obj.toObject();
|
||||||
|
obj.gDrive = false;
|
||||||
|
return res.status(200).send(obj);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateBrew = (req, res)=>{
|
const updateBrew = (req, res)=>{
|
||||||
HomebrewModel.get({ editId: req.params.id })
|
HomebrewModel.get({ editId: req.params.id })
|
||||||
.then((brew)=>{
|
.then((brew)=>{
|
||||||
brew = _.merge(brew, req.body);
|
const updateBrew = excludePropsFromUpdate(req.body);
|
||||||
|
brew = _.merge(brew, updateBrew);
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
// Compress brew text to binary before saving
|
// Compress brew text to binary before saving
|
||||||
brew.textBin = zlib.deflateRawSync(req.body.text);
|
brew.textBin = zlib.deflateRawSync(brew.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
|
||||||
brew.text = undefined;
|
brew.text = undefined;
|
||||||
brew.updatedAt = new Date();
|
brew.updatedAt = new Date();
|
||||||
@@ -103,49 +138,57 @@ const deleteBrew = (req, res)=>{
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const newGoogleBrew = async (req, res, next)=>{
|
||||||
|
let oAuth2Client;
|
||||||
|
|
||||||
|
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||||
|
|
||||||
|
const brew = req.body;
|
||||||
|
|
||||||
|
if(!brew.title) {
|
||||||
|
brew.title = getGoodBrewTitle(brew.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
brew.authors = (req.account) ? [req.account.username] : [];
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
|
delete brew.editId;
|
||||||
|
delete brew.shareId;
|
||||||
|
delete brew.googleId;
|
||||||
|
|
||||||
|
req.body = brew;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||||
|
return res.status(200).send(newBrew);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(err.response.status).send(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGoogleBrew = async (req, res, next)=>{
|
||||||
|
let oAuth2Client;
|
||||||
|
|
||||||
|
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||||
|
|
||||||
|
const brew = excludePropsFromUpdate(req.body);
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
|
||||||
|
return res.status(200).send(updatedBrew);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(err.response?.status || 500).send(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
/*
|
|
||||||
module.exports = function(app) {
|
|
||||||
app;
|
|
||||||
|
|
||||||
app.get('/api/search', mw.adminOnly, function(req, res) {
|
|
||||||
var page = req.query.page || 0;
|
|
||||||
var count = req.query.count || 20;
|
|
||||||
|
|
||||||
var query = {};
|
|
||||||
if (req.query && req.query.id) {
|
|
||||||
query = {
|
|
||||||
"$or": [{
|
|
||||||
editId : req.query.id
|
|
||||||
}, {
|
|
||||||
shareId : req.query.id
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
HomebrewModel.find(query, {
|
|
||||||
text : 0 //omit the text
|
|
||||||
}, {
|
|
||||||
skip: page*count,
|
|
||||||
limit: count*1
|
|
||||||
}, function(err, objs) {
|
|
||||||
if (err) console.error(err);
|
|
||||||
return res.json({
|
|
||||||
page : page,
|
|
||||||
count : count,
|
|
||||||
total : homebrewTotal,
|
|
||||||
brews : objs
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
const shortid = require('shortid');
|
const { nanoid } = require('nanoid');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const zlib = require('zlib');
|
const zlib = require('zlib');
|
||||||
|
|
||||||
const HomebrewSchema = mongoose.Schema({
|
const HomebrewSchema = mongoose.Schema({
|
||||||
shareId : { type: String, default: shortid.generate, index: { unique: true } },
|
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||||
editId : { type: String, default: shortid.generate, 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: '' },
|
||||||
systems : [String],
|
systems : [String],
|
||||||
|
renderer : { type: String, default: '' },
|
||||||
authors : [String],
|
authors : [String],
|
||||||
published : { type: Boolean, default: false },
|
published : { type: Boolean, default: false },
|
||||||
|
|
||||||
@@ -23,32 +25,17 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
version : { type: Number, default: 1 }
|
version : { type: Number, default: 1 }
|
||||||
}, { versionKey: false });
|
}, { versionKey: false });
|
||||||
|
|
||||||
|
HomebrewSchema.statics.increaseView = async function(query) {
|
||||||
|
const brew = await Homebrew.findOne(query).exec();
|
||||||
HomebrewSchema.methods.sanatize = function(full=false){
|
brew.lastViewed = new Date();
|
||||||
const brew = this.toJSON();
|
brew.views = brew.views + 1;
|
||||||
delete brew._id;
|
await brew.save()
|
||||||
delete brew.__v;
|
.catch((err)=>{
|
||||||
if(full){
|
return err;
|
||||||
delete brew.editId;
|
});
|
||||||
}
|
|
||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
HomebrewSchema.methods.increaseView = function(){
|
|
||||||
return new Promise((resolve, reject)=>{
|
|
||||||
this.lastViewed = new Date();
|
|
||||||
this.views = this.views + 1;
|
|
||||||
this.save((err)=>{
|
|
||||||
if(err) return reject(err);
|
|
||||||
return resolve(this);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
HomebrewSchema.statics.get = function(query){
|
HomebrewSchema.statics.get = function(query){
|
||||||
return new Promise((resolve, reject)=>{
|
return new Promise((resolve, reject)=>{
|
||||||
Homebrew.find(query, (err, brews)=>{
|
Homebrew.find(query, (err, brews)=>{
|
||||||
@@ -57,6 +44,8 @@ HomebrewSchema.statics.get = function(query){
|
|||||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||||
brews[0].text = unzipped.toString();
|
brews[0].text = unzipped.toString();
|
||||||
}
|
}
|
||||||
|
if(!brews[0].renderer)
|
||||||
|
brews[0].renderer = 'legacy';
|
||||||
return resolve(brews[0]);
|
return resolve(brews[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -68,17 +57,13 @@ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
|
|||||||
if(allowAccess){
|
if(allowAccess){
|
||||||
delete query.published;
|
delete query.published;
|
||||||
}
|
}
|
||||||
Homebrew.find(query, (err, brews)=>{
|
Homebrew.find(query).lean().exec((err, brews)=>{ //lean() converts results to JSObjects
|
||||||
if(err) return reject('Can not find brew');
|
if(err) return reject('Can not find brew');
|
||||||
return resolve(_.map(brews, (brew)=>{
|
return resolve(brews);
|
||||||
return brew.sanatize(!allowAccess);
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
31
server/static-assets.mv.js
Normal file
31
server/static-assets.mv.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
const expressStaticGzip = require('express-static-gzip');
|
||||||
|
|
||||||
|
// Serve brotli-compressed static files if available
|
||||||
|
const customCacheControlHandler=(response, path)=>{
|
||||||
|
if(path.endsWith('.br')) {
|
||||||
|
// Drop .br suffix to help mime understand the actual type of the file
|
||||||
|
path = path.slice(0, -3);
|
||||||
|
}
|
||||||
|
if(path.endsWith('.js') || path.endsWith('.css')) {
|
||||||
|
// .js and .css files are allowed to be cached up to 12 hours, but then
|
||||||
|
// they must be revalidated to see if there are any updates
|
||||||
|
response.setHeader('Cache-Control', 'public, max-age: 43200, must-revalidate');
|
||||||
|
} else {
|
||||||
|
// Everything else is cached up to a months as we don't update our images
|
||||||
|
// or fonts frequently
|
||||||
|
response.setHeader('Cache-Control', 'public, max-age=2592000, must-revalidate');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const init=(pathToAssets)=>{
|
||||||
|
return expressStaticGzip(pathToAssets, {
|
||||||
|
enableBrotli : true,
|
||||||
|
orderPreference : ['br'],
|
||||||
|
index : false,
|
||||||
|
serveStatic : {
|
||||||
|
cacheControl : false, // we are going to use custom cache-control
|
||||||
|
setHeaders : customCacheControlHandler
|
||||||
|
} });
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = init;
|
||||||
33
server/token.js
Normal file
33
server/token.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const jwt = require('jwt-simple');
|
||||||
|
|
||||||
|
// Load configuration values
|
||||||
|
const config = require('nconf')
|
||||||
|
.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
|
||||||
|
const generateAccessToken = (account)=>{
|
||||||
|
const payload = account;
|
||||||
|
|
||||||
|
// When the token was issued
|
||||||
|
payload.issued = (new Date());
|
||||||
|
// Which service issued the Token
|
||||||
|
payload.issuer = config.get('authentication_token_issuer');
|
||||||
|
// Which service is the token intended for
|
||||||
|
payload.audience = config.get('authentication_token_audience');
|
||||||
|
// The signing key for signing the token
|
||||||
|
delete payload.password;
|
||||||
|
delete payload._id;
|
||||||
|
|
||||||
|
const secret = config.get('authentication_token_secret');
|
||||||
|
|
||||||
|
const token = jwt.encode(payload, secret);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateAccessToken : generateAccessToken
|
||||||
|
};
|
||||||
@@ -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 : {}
|
||||||
@@ -53,8 +54,8 @@ const RenderWarnings = createClass({
|
|||||||
if(_.isEmpty(this.state.warnings)) return null;
|
if(_.isEmpty(this.state.warnings)) return null;
|
||||||
|
|
||||||
return <div className='renderWarnings'>
|
return <div className='renderWarnings'>
|
||||||
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
|
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
|
||||||
<i className='fa fa-exclamation-triangle ohno' />
|
<i className='fas fa-exclamation-triangle ohno' />
|
||||||
<h3>Render Warnings</h3>
|
<h3>Render Warnings</h3>
|
||||||
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
<small>If this homebrew is rendering badly if might be because of the following:</small>
|
||||||
<ul>{_.values(this.state.warnings)}</ul>
|
<ul>{_.values(this.state.warnings)}</ul>
|
||||||
|
|||||||
48
shared/naturalcrit/codeEditor/close-tag.js
Normal file
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'){
|
||||||
@@ -11,78 +12,365 @@ if(typeof navigator !== 'undefined'){
|
|||||||
|
|
||||||
//Language Modes
|
//Language Modes
|
||||||
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/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 : false,
|
wrap : true,
|
||||||
onChange : function(){},
|
onChange : ()=>{},
|
||||||
onCursorActivity : function(){},
|
enableFolding : true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
docs : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
this.codeMirror = CodeMirror(this.refs.editor, {
|
this.buildEditor();
|
||||||
value : this.props.value,
|
const newDoc = CodeMirror.Doc(this.props.value, this.props.language);
|
||||||
lineNumbers : true,
|
this.codeMirror.swapDoc(newDoc);
|
||||||
lineWrapping : this.props.wrap,
|
},
|
||||||
mode : this.props.language,
|
|
||||||
extraKeys : {
|
componentDidUpdate : function(prevProps) {
|
||||||
'Ctrl-B' : this.makeBold,
|
if(prevProps.view !== this.props.view){ //view changed; swap documents
|
||||||
'Ctrl-I' : this.makeItalic
|
let newDoc;
|
||||||
|
|
||||||
|
if(!this.state.docs[this.props.view]) {
|
||||||
|
newDoc = CodeMirror.Doc(this.props.value, this.props.language);
|
||||||
|
} else {
|
||||||
|
newDoc = this.state.docs[this.props.view];
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
this.codeMirror.on('change', this.handleChange);
|
const oldDoc = { [prevProps.view]: this.codeMirror.swapDoc(newDoc) };
|
||||||
this.codeMirror.on('cursorActivity', this.handleCursorActivity);
|
|
||||||
this.updateSize();
|
|
||||||
},
|
|
||||||
|
|
||||||
makeBold : function() {
|
this.setState((prevState)=>({
|
||||||
const selection = this.codeMirror.getSelection();
|
docs : _.merge({}, prevState.docs, oldDoc)
|
||||||
this.codeMirror.replaceSelection(`**${selection}**`, 'around');
|
}));
|
||||||
},
|
|
||||||
|
|
||||||
makeItalic : function() {
|
this.props.rerenderParent();
|
||||||
const selection = this.codeMirror.getSelection();
|
} else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
|
||||||
this.codeMirror.replaceSelection(`*${selection}*`, 'around');
|
this.codeMirror.setValue(this.props.value);
|
||||||
},
|
}
|
||||||
|
|
||||||
componentWillReceiveProps : function(nextProps){
|
if(this.props.enableFolding) {
|
||||||
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
|
this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror));
|
||||||
this.codeMirror.setValue(nextProps.value);
|
} else {
|
||||||
|
this.codeMirror.setOption('foldOptions', false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldComponentUpdate : function(nextProps, nextState) {
|
buildEditor : function() {
|
||||||
return false;
|
this.codeMirror = CodeMirror(this.refs.editor, {
|
||||||
|
lineNumbers : true,
|
||||||
|
lineWrapping : this.props.wrap,
|
||||||
|
indentWithTabs : true,
|
||||||
|
tabSize : 2,
|
||||||
|
historyEventDelay : 250,
|
||||||
|
extraKeys : {
|
||||||
|
'Ctrl-B' : this.makeBold,
|
||||||
|
'Cmd-B' : this.makeBold,
|
||||||
|
'Ctrl-I' : this.makeItalic,
|
||||||
|
'Cmd-I' : this.makeItalic,
|
||||||
|
'Ctrl-U' : this.makeUnderline,
|
||||||
|
'Cmd-U' : this.makeUnderline,
|
||||||
|
'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.
|
||||||
|
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
||||||
|
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() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '**' && selection.slice(-2) === '**';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `**${selection}**`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
makeItalic : function() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 1) === '*' && selection.slice(-1) === '*';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(1, -1) : `*${selection}*`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
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() {
|
||||||
|
const selection = this.codeMirror.getSelection(), t = selection.slice(0, 2) === '{{' && selection.slice(-2) === '}}';
|
||||||
|
this.codeMirror.replaceSelection(t ? selection.slice(2, -2) : `{{ ${selection}}}`, 'around');
|
||||||
|
if(selection.length === 0){
|
||||||
|
const cursor = this.codeMirror.getCursor();
|
||||||
|
this.codeMirror.setCursor({ line: cursor.line, ch: cursor.ch - 2 });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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 -==//
|
||||||
setCursorPosition : function(line, char){
|
setCursorPosition : function(line, char){
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
this.codeMirror.focus();
|
this.codeMirror.focus();
|
||||||
this.codeMirror.doc.setCursor(line, char);
|
this.codeMirror.doc.setCursor(line, char);
|
||||||
}, 10);
|
}, 10);
|
||||||
},
|
},
|
||||||
|
getCursorPosition : function(){
|
||||||
|
return this.codeMirror.getCursor();
|
||||||
|
},
|
||||||
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();
|
||||||
|
},
|
||||||
|
|
||||||
handleChange : function(editor){
|
foldOptions : function(cm){
|
||||||
this.props.onChange(editor.getValue());
|
return {
|
||||||
},
|
scanUp : true,
|
||||||
handleCursorActivity : function(){
|
rangeFinder : CodeMirror.fold.homebrewery,
|
||||||
this.props.onCursorActivity(this.codeMirror.doc.getCursor());
|
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;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user