mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 14:13:02 +00:00
Compare commits
1287 Commits
toWellForm
...
UnifyNewHo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6057b35d19 | ||
|
|
521d42f32f | ||
|
|
e9f8302597 | ||
|
|
f429b1755d | ||
|
|
20e12ebcb5 | ||
|
|
ae51213c8c | ||
|
|
44023f390c | ||
|
|
48b95712e2 | ||
|
|
16c28e16ce | ||
|
|
379b518c6b | ||
|
|
962dcbdbf6 | ||
|
|
400fa250ee | ||
|
|
e82921f81a | ||
|
|
18367526bd | ||
|
|
bc258f5239 | ||
|
|
e64fc83ea6 | ||
|
|
ee6d2ac35d | ||
|
|
f22f7196ca | ||
|
|
ba23763294 | ||
|
|
7f832a55db | ||
|
|
1c6a39363c | ||
|
|
bcca5fa97d | ||
|
|
bfe6142b04 | ||
|
|
aef835dfe7 | ||
|
|
274fbcb29e | ||
|
|
900cf6aebb | ||
|
|
24db8f85ac | ||
|
|
82a8db129e | ||
|
|
6d4ad6af7d | ||
|
|
4b753970c9 | ||
|
|
07495b0dea | ||
|
|
718dba3e4a | ||
|
|
c6ed67db08 | ||
|
|
fb75bd46d0 | ||
|
|
c5071aa27e | ||
|
|
f0baa763ec | ||
|
|
3ec650557e | ||
|
|
242ff8712f | ||
|
|
31a8101df7 | ||
|
|
788324fe31 | ||
|
|
da8772daa7 | ||
|
|
87a36bb02d | ||
|
|
1459f6a320 | ||
|
|
a11fa72261 | ||
|
|
2663d86627 | ||
|
|
8d4ea7cfd8 | ||
|
|
b6818e963b | ||
|
|
dc1bc471aa | ||
|
|
5504c1b96b | ||
|
|
fd370c777d | ||
|
|
58277585e1 | ||
|
|
885c0105f3 | ||
|
|
52486495c8 | ||
|
|
328e071268 | ||
|
|
088ca9971c | ||
|
|
c99f59d42b | ||
|
|
cb3eb77c61 | ||
|
|
7163b1a287 | ||
|
|
08d228831d | ||
|
|
ad8bb34c93 | ||
|
|
02a7920b2c | ||
|
|
43c639246b | ||
|
|
c2e6150edf | ||
|
|
95a1d74644 | ||
|
|
1044aa74b0 | ||
|
|
8a0f350c47 | ||
|
|
6f2c397574 | ||
|
|
8706f91b58 | ||
|
|
1eb5b6d3a4 | ||
|
|
90f6e7ec37 | ||
|
|
90a81237ec | ||
|
|
883f59ff0d | ||
|
|
a75364c7f6 | ||
|
|
597ce7cb48 | ||
|
|
d94afa9c50 | ||
|
|
13de195a66 | ||
|
|
32f9a44acf | ||
|
|
bb32f9fe95 | ||
|
|
63f4f5712e | ||
|
|
ede7ad683a | ||
|
|
172c11646a | ||
|
|
bbeac49552 | ||
|
|
1aeded648e | ||
|
|
c1ebc68cd4 | ||
|
|
93b86632fc | ||
|
|
d01860d4de | ||
|
|
86ac11e512 | ||
|
|
9c336062c6 | ||
|
|
2cd47c46f6 | ||
|
|
8671404bdc | ||
|
|
601fc732b0 | ||
|
|
fb3ab47ab0 | ||
|
|
518a3434be | ||
|
|
d01f4fb77e | ||
|
|
6600d9344c | ||
|
|
0371635e11 | ||
|
|
53f6e48f8f | ||
|
|
da578c53a8 | ||
|
|
986bfdd00a | ||
|
|
15c04ef37e | ||
|
|
8cf55932a9 | ||
|
|
759dcb5833 | ||
|
|
83c3eacf83 | ||
|
|
8a788a6ebf | ||
|
|
7198c21229 | ||
|
|
6c3a5f193d | ||
|
|
f1ad1b9124 | ||
|
|
593a98db9a | ||
|
|
e25c3daad6 | ||
|
|
96b175e74d | ||
|
|
8924685c26 | ||
|
|
74c9d7b3f1 | ||
|
|
cd378cad0c | ||
|
|
ce304996f0 | ||
|
|
029c105ff1 | ||
|
|
1f81cc9af0 | ||
|
|
6ac6eae863 | ||
|
|
a47a1a25a4 | ||
|
|
0500ac305a | ||
|
|
e1a441b04a | ||
|
|
b98c297079 | ||
|
|
90dfc75ce9 | ||
|
|
dd46a059c5 | ||
|
|
2d881b8dc9 | ||
|
|
e023bfeef6 | ||
|
|
8b351925c1 | ||
|
|
5ddd631dfd | ||
|
|
5ff6327c72 | ||
|
|
c993a1a8c9 | ||
|
|
b9372f17d9 | ||
|
|
6b7c57f0e4 | ||
|
|
6c5063a30d | ||
|
|
e20da7c67f | ||
|
|
3596eabbf5 | ||
|
|
fb4ca21cb4 | ||
|
|
99769c90f8 | ||
|
|
301c50cca9 | ||
|
|
320f1e120f | ||
|
|
cca9ebefdb | ||
|
|
aebc49c2d4 | ||
|
|
1eb226ea13 | ||
|
|
8049b5be9d | ||
|
|
a8dab28fcf | ||
|
|
253dbb358b | ||
|
|
719edd82c5 | ||
|
|
16d7b11b8d | ||
|
|
e2ed7b8600 | ||
|
|
63d957fdc6 | ||
|
|
7751c0e37b | ||
|
|
990bf80b59 | ||
|
|
f16598f238 | ||
|
|
4d014bf379 | ||
|
|
4856c803ed | ||
|
|
d9cd270f3b | ||
|
|
878ea1449d | ||
|
|
579e9e0ec5 | ||
|
|
f6629f2f9e | ||
|
|
b87c78474d | ||
|
|
958d282a58 | ||
|
|
7e56ae2019 | ||
|
|
ebca50ed4b | ||
|
|
bfd14757c2 | ||
|
|
3626ed5a31 | ||
|
|
d385bacdd6 | ||
|
|
cbbb2c0a7d | ||
|
|
fbe637ff82 | ||
|
|
82bd16c623 | ||
|
|
d1f13af67b | ||
|
|
b6c03e88b8 | ||
|
|
b587d17397 | ||
|
|
0a02f910f8 | ||
|
|
ddfa06e76b | ||
|
|
0c2b1fec04 | ||
|
|
6de7a64acd | ||
|
|
b9fe4c3901 | ||
|
|
5ae01862e5 | ||
|
|
398df7a061 | ||
|
|
443b0f6a37 | ||
|
|
544175b994 | ||
|
|
955602e7ee | ||
|
|
90e577dd3f | ||
|
|
828208aadb | ||
|
|
973e071e93 | ||
|
|
f9e7aa355d | ||
|
|
24dfd41714 | ||
|
|
638e54535d | ||
|
|
cbc6956221 | ||
|
|
248d2038ec | ||
|
|
5b66175b8c | ||
|
|
552aa7d41a | ||
|
|
b0a108b543 | ||
|
|
505d2840c0 | ||
|
|
41ff50fefe | ||
|
|
2fbcc84a50 | ||
|
|
45e4d27c0a | ||
|
|
77bf3ffc6f | ||
|
|
bc045ec6c9 | ||
|
|
6390ea076a | ||
|
|
6affcb587d | ||
|
|
7787afabff | ||
|
|
fb4a8e5cf1 | ||
|
|
8432a6e367 | ||
|
|
90ee08de42 | ||
|
|
40839b18e4 | ||
|
|
677c02cfa5 | ||
|
|
a7a8803e9d | ||
|
|
5fbc111db7 | ||
|
|
5edea7d0f4 | ||
|
|
d3a9d813c9 | ||
|
|
fc475b2a7e | ||
|
|
76b76b3bb6 | ||
|
|
22ef3cbebc | ||
|
|
9da8a17053 | ||
|
|
7cadbfbd7b | ||
|
|
98b9e86787 | ||
|
|
489b4b2694 | ||
|
|
8d279260c2 | ||
|
|
7c08c430d0 | ||
|
|
45689d119e | ||
|
|
c5805af935 | ||
|
|
b2c4bb7082 | ||
|
|
68460447dc | ||
|
|
440c7beff6 | ||
|
|
c7610cf0f8 | ||
|
|
7f3a818558 | ||
|
|
bc82afa5b2 | ||
|
|
abef250631 | ||
|
|
1794e96d50 | ||
|
|
25f25da499 | ||
|
|
aa15bdaacb | ||
|
|
7ba7991631 | ||
|
|
0e1ac26999 | ||
|
|
f49fed8c35 | ||
|
|
a8236fbab4 | ||
|
|
daf4eceedd | ||
|
|
a02361ee65 | ||
|
|
81e20f032e | ||
|
|
1d92b98568 | ||
|
|
0f4157d084 | ||
|
|
4dcc3749d8 | ||
|
|
8f058d56f2 | ||
|
|
d192a064d6 | ||
|
|
cccb531e17 | ||
|
|
6414e73e7d | ||
|
|
41daf8d172 | ||
|
|
4c897fdeb5 | ||
|
|
89ce4de354 | ||
|
|
43095507ee | ||
|
|
eb7fbbe018 | ||
|
|
869958ec38 | ||
|
|
99b90e0998 | ||
|
|
57a48100d3 | ||
|
|
8538e4fadb | ||
|
|
9a002511a3 | ||
|
|
3fa3a52e05 | ||
|
|
4fe920dac3 | ||
|
|
71dff5fbf9 | ||
|
|
26419d2ccb | ||
|
|
f02fe2d8f3 | ||
|
|
318fb53eb2 | ||
|
|
6a32b7427b | ||
|
|
5886bd65e5 | ||
|
|
9c5f80cbdb | ||
|
|
79d8956c4f | ||
|
|
2e491b3556 | ||
|
|
d9a8afa272 | ||
|
|
209195202c | ||
|
|
64235c844a | ||
|
|
5d000a4599 | ||
|
|
380e593b42 | ||
|
|
169f089d08 | ||
|
|
b3977ed141 | ||
|
|
9800561de7 | ||
|
|
166af08e6a | ||
|
|
48f17f7c5e | ||
|
|
87c9f52222 | ||
|
|
c80b7ffd66 | ||
|
|
5f16ce3dbd | ||
|
|
b5ff26f857 | ||
|
|
578b01bbb1 | ||
|
|
67467e0099 | ||
|
|
da21bf20f9 | ||
|
|
df7fcf1e5f | ||
|
|
702ece6671 | ||
|
|
1008321957 | ||
|
|
b547486c48 | ||
|
|
6bb0b8001b | ||
|
|
e1e661976d | ||
|
|
7bdeeee9ef | ||
|
|
becf35d336 | ||
|
|
d7585767c9 | ||
|
|
f9bb6209b7 | ||
|
|
13702a2f62 | ||
|
|
a6a684c89e | ||
|
|
862fa7de89 | ||
|
|
b671cf7b02 | ||
|
|
d5dbe0b4ba | ||
|
|
c2cf695c17 | ||
|
|
6d0d6f08b5 | ||
|
|
77dcc9b433 | ||
|
|
50d2a0d3a2 | ||
|
|
17f60ee159 | ||
|
|
5f2f3a6f3d | ||
|
|
bbb812cb06 | ||
|
|
e842599b22 | ||
|
|
5648e55774 | ||
|
|
c051580545 | ||
|
|
6e72fe2600 | ||
|
|
03602ae1e0 | ||
|
|
8de738a146 | ||
|
|
6960beb739 | ||
|
|
6748639ec5 | ||
|
|
e5651807fd | ||
|
|
9adf6dee61 | ||
|
|
03527a1f95 | ||
|
|
651863b0f7 | ||
|
|
450ecd24b7 | ||
|
|
995cfa2aa4 | ||
|
|
5eecb5ea20 | ||
|
|
0885473b66 | ||
|
|
eabff4f6b2 | ||
|
|
a773df25d0 | ||
|
|
b07f75ac36 | ||
|
|
ed5fbadd73 | ||
|
|
c74c2c8efe | ||
|
|
1efe570dae | ||
|
|
2571460f42 | ||
|
|
dbb67113b9 | ||
|
|
33e3e018f3 | ||
|
|
07adf0342d | ||
|
|
b2b1cb4985 | ||
|
|
c4d6cc4579 | ||
|
|
01fbb4439e | ||
|
|
eb48d981d6 | ||
|
|
3624fcef0f | ||
|
|
ab62f0fcf9 | ||
|
|
9e78671e4f | ||
|
|
f64a7b38ae | ||
|
|
3fdedd8861 | ||
|
|
1d4ebbb689 | ||
|
|
c4f148a3a1 | ||
|
|
7abf45e8ba | ||
|
|
bbae62e0b7 | ||
|
|
a9d71078d3 | ||
|
|
5bde870586 | ||
|
|
7ea78870bf | ||
|
|
393caa86eb | ||
|
|
9b7a3c5c70 | ||
|
|
fe69bd50b5 | ||
|
|
a2c4f604b3 | ||
|
|
083e8c9b52 | ||
|
|
d2a025ca41 | ||
|
|
181d6b7e0a | ||
|
|
dd20fc8475 | ||
|
|
33ea397915 | ||
|
|
320fb02543 | ||
|
|
e127a6a557 | ||
|
|
e774dfd97d | ||
|
|
1dcea0fe6a | ||
|
|
0ca53f8db6 | ||
|
|
5395a759ed | ||
|
|
8f470fb000 | ||
|
|
90c375a5c8 | ||
|
|
e8cc4a0c58 | ||
|
|
cf68cc46ad | ||
|
|
653e20b4e4 | ||
|
|
e97d45e5b5 | ||
|
|
691cd048e2 | ||
|
|
5071105f8c | ||
|
|
9cd009e89b | ||
|
|
acaf293c7c | ||
|
|
79503dd17f | ||
|
|
485b6a0041 | ||
|
|
983781303b | ||
|
|
9c8e03f961 | ||
|
|
a298288888 | ||
|
|
c48703aed5 | ||
|
|
09000bd20f | ||
|
|
237caa84f7 | ||
|
|
d292d60ee9 | ||
|
|
395e406d65 | ||
|
|
806c3f63bb | ||
|
|
4a296809a0 | ||
|
|
f8361fa141 | ||
|
|
8542056d6e | ||
|
|
f23be91b6d | ||
|
|
f810bea4c8 | ||
|
|
42136b89fd | ||
|
|
eb604d9201 | ||
|
|
e341069196 | ||
|
|
3a54ac9d7d | ||
|
|
42d8c1b33f | ||
|
|
f700620373 | ||
|
|
0f059bce66 | ||
|
|
0eb68aaf72 | ||
|
|
b9f825c168 | ||
|
|
58c2504394 | ||
|
|
a9aadbfef9 | ||
|
|
dae5922fd0 | ||
|
|
5fb20991bb | ||
|
|
75fe7b2c67 | ||
|
|
ab400b82d6 | ||
|
|
6867cb5a4a | ||
|
|
742de8582c | ||
|
|
600ff5f367 | ||
|
|
e751facf32 | ||
|
|
959d5fb6c9 | ||
|
|
3456d503b2 | ||
|
|
9ef291a8ae | ||
|
|
ff174870e2 | ||
|
|
a015714d5e | ||
|
|
9bcab7b82b | ||
|
|
bc0cb0d0be | ||
|
|
ce4299a1f0 | ||
|
|
398e985e65 | ||
|
|
a5f597f598 | ||
|
|
beb7ecd0a9 | ||
|
|
ea625a0fbc | ||
|
|
932120883b | ||
|
|
b29406da8b | ||
|
|
4cc2d429c5 | ||
|
|
77563d12a6 | ||
|
|
b914bf3bf5 | ||
|
|
6f52b8473f | ||
|
|
44713eda4e | ||
|
|
e552282299 | ||
|
|
9ecd53267f | ||
|
|
5ee1cf6aa5 | ||
|
|
1295f635dc | ||
|
|
60142d9467 | ||
|
|
6dc4355972 | ||
|
|
555a26f0d6 | ||
|
|
abce7d8531 | ||
|
|
678d981121 | ||
|
|
32f8c18adc | ||
|
|
0aead96dcf | ||
|
|
c238094e4c | ||
|
|
657eeea4d5 | ||
|
|
1e34e85aab | ||
|
|
b747968e74 | ||
|
|
25629173c9 | ||
|
|
96642c07d3 | ||
|
|
2bd0f909f3 | ||
|
|
9b4047f3f9 | ||
|
|
91e2916199 | ||
|
|
3fcc677f96 | ||
|
|
3f77e32550 | ||
|
|
c4903c4993 | ||
|
|
630f9002aa | ||
|
|
aea7809fbd | ||
|
|
30e644d5e0 | ||
|
|
fe2f5a405c | ||
|
|
07a1890ed9 | ||
|
|
fc400c226c | ||
|
|
8e3ccec855 | ||
|
|
25c09bc241 | ||
|
|
0eaba3de01 | ||
|
|
ece1a7e9a7 | ||
|
|
2ef7a1521b | ||
|
|
b72357096a | ||
|
|
8f4c74d0ce | ||
|
|
2589e6d919 | ||
|
|
99fb8faf96 | ||
|
|
519da0a5c0 | ||
|
|
814a70b704 | ||
|
|
ff72f6cbd1 | ||
|
|
511f33c44d | ||
|
|
b455165fd3 | ||
|
|
7be03ab738 | ||
|
|
b287163ef7 | ||
|
|
1429674013 | ||
|
|
5576a76731 | ||
|
|
9c1a0fd798 | ||
|
|
70afa96bb0 | ||
|
|
808f4dd9a0 | ||
|
|
8e74ba07fe | ||
|
|
0c33df1cd6 | ||
|
|
73bb6acc14 | ||
|
|
654c44ebc9 | ||
|
|
a6703ef731 | ||
|
|
04defb97b0 | ||
|
|
da4f6c9307 | ||
|
|
c3b0311a4b | ||
|
|
196f290320 | ||
|
|
b1fec69d8f | ||
|
|
b7a7446f75 | ||
|
|
bd9d9d4ab6 | ||
|
|
c6cd6e9864 | ||
|
|
5e7e314baa | ||
|
|
f2b995660a | ||
|
|
95f44f4460 | ||
|
|
bd68b9c0cb | ||
|
|
b19d05fbf7 | ||
|
|
dc724492ef | ||
|
|
e3de7b9f01 | ||
|
|
be2f1786b5 | ||
|
|
99a3131724 | ||
|
|
0dbbc469e1 | ||
|
|
d5a80cc89a | ||
|
|
0be5c6c576 | ||
|
|
d7b478e830 | ||
|
|
3ce76f450c | ||
|
|
ad04c68596 | ||
|
|
0d8bf5f0aa | ||
|
|
075fdb194e | ||
|
|
6ab1b7705a | ||
|
|
9151b8c575 | ||
|
|
d5186a03e9 | ||
|
|
b461ac0a68 | ||
|
|
477c9d1555 | ||
|
|
ea365e18f4 | ||
|
|
512eedfc39 | ||
|
|
518bc7030d | ||
|
|
ae8dc61423 | ||
|
|
b89532caa1 | ||
|
|
9a57b407a5 | ||
|
|
776de3618a | ||
|
|
b4d575c383 | ||
|
|
dc4382d067 | ||
|
|
08e00aa38b | ||
|
|
08946ce5d4 | ||
|
|
75212511d2 | ||
|
|
79845f2d63 | ||
|
|
c982ff546c | ||
|
|
9f56d100aa | ||
|
|
d0c3765f8f | ||
|
|
1ded1cad5a | ||
|
|
ec6258a2a5 | ||
|
|
f8566392f6 | ||
|
|
7a1042fedd | ||
|
|
61efc2d152 | ||
|
|
f74c2049a7 | ||
|
|
7451dda632 | ||
|
|
ab9b151b8a | ||
|
|
26aa302714 | ||
|
|
e2f2b2962f | ||
|
|
a218b87215 | ||
|
|
ef6f022ea3 | ||
|
|
a594d45611 | ||
|
|
4c4a023f34 | ||
|
|
1e35e1096f | ||
|
|
bd145f17da | ||
|
|
99c342f19b | ||
|
|
0bca3393d4 | ||
|
|
41bd27b573 | ||
|
|
30430cb8cb | ||
|
|
3cf98617f5 | ||
|
|
fa4b2ae0e3 | ||
|
|
e2b38829f2 | ||
|
|
0a4ac7a35a | ||
|
|
cb060ae8b1 | ||
|
|
98edd2740f | ||
|
|
82e711a344 | ||
|
|
2166d55878 | ||
|
|
8e8f520eaa | ||
|
|
4eeaa7c650 | ||
|
|
f5fc106d01 | ||
|
|
de1773361a | ||
|
|
b9b45632b0 | ||
|
|
2ce7c6c2be | ||
|
|
4137d0dd82 | ||
|
|
29bd8b45c3 | ||
|
|
9d1601f424 | ||
|
|
7525e087ff | ||
|
|
be4991a419 | ||
|
|
ac89f428b2 | ||
|
|
7765cb31bf | ||
|
|
8729407da6 | ||
|
|
eac87b65d8 | ||
|
|
848c68689d | ||
|
|
65001c44e6 | ||
|
|
6ec37d3fa4 | ||
|
|
9f68d60703 | ||
|
|
fc8654bff5 | ||
|
|
ebbf7cf3a2 | ||
|
|
225fcef291 | ||
|
|
b460acad0d | ||
|
|
1c1808378b | ||
|
|
2d25e08040 | ||
|
|
8fc1919d7c | ||
|
|
790c17ad53 | ||
|
|
d315e4f008 | ||
|
|
2bfc41ce30 | ||
|
|
cdacaac049 | ||
|
|
da88fd0b3f | ||
|
|
0095e4582b | ||
|
|
5cf0945fa7 | ||
|
|
8e10e9dea9 | ||
|
|
07f6439093 | ||
|
|
8c979b8545 | ||
|
|
4f3929c658 | ||
|
|
c8cf9e3002 | ||
|
|
7bc323c92c | ||
|
|
4d141fa6a3 | ||
|
|
25d1db5584 | ||
|
|
565d58bb31 | ||
|
|
2f95cc5f45 | ||
|
|
a62588a4c9 | ||
|
|
4afef9d3b3 | ||
|
|
a887b87350 | ||
|
|
3672285e92 | ||
|
|
ea4dd5defd | ||
|
|
712ee111d4 | ||
|
|
0fc7571c35 | ||
|
|
84cdf6a14e | ||
|
|
1f77656a1c | ||
|
|
bbd95ffe2a | ||
|
|
e3a7e1f403 | ||
|
|
746c71f44b | ||
|
|
cb61891450 | ||
|
|
481219402c | ||
|
|
48285e6738 | ||
|
|
551763fecb | ||
|
|
d2507fe99f | ||
|
|
07ff9a114e | ||
|
|
962d98543e | ||
|
|
7f17887e0e | ||
|
|
8e37806791 | ||
|
|
f076e05f49 | ||
|
|
163e3927b5 | ||
|
|
0234de12bb | ||
|
|
21be329e77 | ||
|
|
314c5cef7e | ||
|
|
ec36365697 | ||
|
|
f47a32067e | ||
|
|
fef571b1d6 | ||
|
|
80b33e3fed | ||
|
|
8b67118303 | ||
|
|
d5969a6573 | ||
|
|
547682a59a | ||
|
|
b2903137eb | ||
|
|
7329c69cfd | ||
|
|
83a095923e | ||
|
|
a44056a64b | ||
|
|
a705e3b9d8 | ||
|
|
d55a6cfd88 | ||
|
|
86ff2ab96b | ||
|
|
a960299612 | ||
|
|
99efe7f06b | ||
|
|
b605346c7d | ||
|
|
ab6c1ae402 | ||
|
|
94bcc8e997 | ||
|
|
72c2857237 | ||
|
|
20f0d16a58 | ||
|
|
9a26626412 | ||
|
|
f4afc91df7 | ||
|
|
baafb6d2f9 | ||
|
|
8b9e084b17 | ||
|
|
44a01f27fe | ||
|
|
543d18f9d9 | ||
|
|
7371f57ded | ||
|
|
ee543b7090 | ||
|
|
b67eb59461 | ||
|
|
e787a68859 | ||
|
|
edc4f8ec63 | ||
|
|
aec958249a | ||
|
|
f083391efd | ||
|
|
2c63c01723 | ||
|
|
85af5bbd27 | ||
|
|
17f26b803c | ||
|
|
8093380e0c | ||
|
|
07f0cef67c | ||
|
|
4241aa535b | ||
|
|
4c85f3ec4b | ||
|
|
57f273a276 | ||
|
|
e159e57222 | ||
|
|
d4991164e9 | ||
|
|
baa1ed2b53 | ||
|
|
f1e291e313 | ||
|
|
814f3a6c20 | ||
|
|
43dc1bed7d | ||
|
|
313492a344 | ||
|
|
4cd5c13841 | ||
|
|
c7a19857dd | ||
|
|
b07317b0f7 | ||
|
|
c0eef7530e | ||
|
|
55618a10b9 | ||
|
|
5f48b30449 | ||
|
|
e523886345 | ||
|
|
4918dc5239 | ||
|
|
a0de6295c7 | ||
|
|
3db778a665 | ||
|
|
a7eef65694 | ||
|
|
8d1464a2c4 | ||
|
|
552cf30863 | ||
|
|
20baa9984f | ||
|
|
4daa8042a2 | ||
|
|
51e79c2c5f | ||
|
|
88e8140b60 | ||
|
|
252698b135 | ||
|
|
21f1704626 | ||
|
|
7f128b0dae | ||
|
|
869d69b986 | ||
|
|
c04cc94570 | ||
|
|
46093ba6ba | ||
|
|
19556d9f36 | ||
|
|
0d4d97c5c5 | ||
|
|
55f333a9e5 | ||
|
|
2361cdeadc | ||
|
|
aeae704173 | ||
|
|
c420410904 | ||
|
|
0daf8c5c83 | ||
|
|
4b9b1ec9ac | ||
|
|
924d014c69 | ||
|
|
8992cf8251 | ||
|
|
7c6aa0ffec | ||
|
|
ebe64c508f | ||
|
|
f3514cfea6 | ||
|
|
8ed25fb7cf | ||
|
|
762cd58d52 | ||
|
|
477f706eb9 | ||
|
|
edcf9979a7 | ||
|
|
01f075d3f5 | ||
|
|
de18a53efe | ||
|
|
ef2beec590 | ||
|
|
c10559ba5f | ||
|
|
69c633dabe | ||
|
|
8bdcdcd510 | ||
|
|
ce03f598b2 | ||
|
|
caca578709 | ||
|
|
addbf19682 | ||
|
|
479aae4b2f | ||
|
|
4b6652c470 | ||
|
|
e9d1209ce8 | ||
|
|
7c62e49767 | ||
|
|
9b0da36365 | ||
|
|
1391a9053d | ||
|
|
fee88d1d47 | ||
|
|
a47dc51bd1 | ||
|
|
cfb9e1afa2 | ||
|
|
540a0a7a36 | ||
|
|
b7e422ac06 | ||
|
|
06e3fd6248 | ||
|
|
f3315d654e | ||
|
|
df5eeb5c97 | ||
|
|
e2de225625 | ||
|
|
5b7d5bee24 | ||
|
|
18eb3ec643 | ||
|
|
5f9cc48fe1 | ||
|
|
56d1855518 | ||
|
|
758b508955 | ||
|
|
3221b40903 | ||
|
|
39a49a6d62 | ||
|
|
02f63e0b02 | ||
|
|
0ba943ceb0 | ||
|
|
578a8d7eba | ||
|
|
9a9d7a6b5e | ||
|
|
917b6b3145 | ||
|
|
b36376f9e8 | ||
|
|
58a22750c5 | ||
|
|
df1b601de7 | ||
|
|
1ed44282e3 | ||
|
|
f421ce1d93 | ||
|
|
ca0f18acd6 | ||
|
|
87d76ea8f6 | ||
|
|
9f5a29099c | ||
|
|
0360d6b6c5 | ||
|
|
8d2057431b | ||
|
|
ee0d737b9c | ||
|
|
cb27b26103 | ||
|
|
0564fb82f6 | ||
|
|
5596f2d9da | ||
|
|
a11b67f139 | ||
|
|
17717ea2a9 | ||
|
|
c15e7b2da3 | ||
|
|
fcca56f502 | ||
|
|
68f66b2bac | ||
|
|
0d71f291e7 | ||
|
|
fc065d250b | ||
|
|
01d93b98d5 | ||
|
|
f5aa37bd5e | ||
|
|
d6d445dad5 | ||
|
|
1af66cf571 | ||
|
|
2cb8b5d014 | ||
|
|
34a0b4eb05 | ||
|
|
854a2ab35e | ||
|
|
42accdb54f | ||
|
|
7e5bade4fa | ||
|
|
ed30a1cd7d | ||
|
|
94f478477d | ||
|
|
50bda9455f | ||
|
|
d8d672fada | ||
|
|
bf297939dc | ||
|
|
df563b9294 | ||
|
|
e584eec8c2 | ||
|
|
557178172b | ||
|
|
45e98debbd | ||
|
|
0bd5ac42b6 | ||
|
|
af729de096 | ||
|
|
40cd53fcb8 | ||
|
|
f326d11232 | ||
|
|
09ac8b8a32 | ||
|
|
85ea91fed8 | ||
|
|
a0c9b8849c | ||
|
|
ff91ebb06a | ||
|
|
21baab784e | ||
|
|
1f3a0f1f99 | ||
|
|
6b4f5bd0af | ||
|
|
0c2f0ac31e | ||
|
|
777f51c661 | ||
|
|
3cfdb7eeb0 | ||
|
|
1f9495099f | ||
|
|
52cf1ddea0 | ||
|
|
b79c5954ff | ||
|
|
9944398e4c | ||
|
|
489f00b785 | ||
|
|
1a515f8d9c | ||
|
|
f386ba3f45 | ||
|
|
80564dd8db | ||
|
|
db16248afb | ||
|
|
cf4c1f7009 | ||
|
|
3ffdb34312 | ||
|
|
dc8d0e9483 | ||
|
|
38bd3b0fc5 | ||
|
|
634450d4a9 | ||
|
|
559f55f781 | ||
|
|
64b7527ad0 | ||
|
|
d48d5260a4 | ||
|
|
41dc78375c | ||
|
|
bbc601cf47 | ||
|
|
e89920bd1e | ||
|
|
2e12980180 | ||
|
|
b77af1bcc8 | ||
|
|
45d188fea1 | ||
|
|
1ce26ca953 | ||
|
|
d1c0557341 | ||
|
|
4e857a1a99 | ||
|
|
547ac11756 | ||
|
|
0e2443f772 | ||
|
|
9d16f4556e | ||
|
|
6d0d0057f6 | ||
|
|
b8d9023c98 | ||
|
|
4578cf6584 | ||
|
|
111869d33b | ||
|
|
d0b4486e15 | ||
|
|
1aed753911 | ||
|
|
c080e5b191 | ||
|
|
11396389ab | ||
|
|
0bcf228881 | ||
|
|
de30722554 | ||
|
|
6cfdfad7d3 | ||
|
|
a9fa0bd32d | ||
|
|
cfbf4021dc | ||
|
|
e3780e844d | ||
|
|
659510e364 | ||
|
|
29da0396fd | ||
|
|
c3e08181e9 | ||
|
|
213a719337 | ||
|
|
a7a7e46e89 | ||
|
|
d061b902d5 | ||
|
|
0a86990bdf | ||
|
|
ada06c9618 | ||
|
|
03798e945d | ||
|
|
6d2cbaacc0 | ||
|
|
f2f894381e | ||
|
|
7c293f51cb | ||
|
|
67b31c476c | ||
|
|
10fae6dbac | ||
|
|
ebc7f055fa | ||
|
|
ce01b6c1ff | ||
|
|
553562611f | ||
|
|
423caefe1a | ||
|
|
ae1de819ea | ||
|
|
27c4cfd25c | ||
|
|
bf22104474 | ||
|
|
c3e0a687c0 | ||
|
|
00a2b130eb | ||
|
|
8eef810f3f | ||
|
|
a04df0fdfc | ||
|
|
a504703d41 | ||
|
|
497f8bde83 | ||
|
|
8711265506 | ||
|
|
b1ff68c3b1 | ||
|
|
3ce9bb1310 | ||
|
|
564f5d71b2 | ||
|
|
158122ed55 | ||
|
|
66bfc8f27b | ||
|
|
6c8b94453e | ||
|
|
460fb655d8 | ||
|
|
be1742d01d | ||
|
|
5d3742aea6 | ||
|
|
1966027289 | ||
|
|
35d50cc9d1 | ||
|
|
3f41306306 | ||
|
|
988bf1b0a9 | ||
|
|
2f1ade8463 | ||
|
|
518924d725 | ||
|
|
6269651c8d | ||
|
|
057abcda0d | ||
|
|
b6b23a787c | ||
|
|
899004cfaf | ||
|
|
7e826cd4f5 | ||
|
|
3b150891bc | ||
|
|
e87acc3f0f | ||
|
|
b1e99f1385 | ||
|
|
4e0b6d634d | ||
|
|
a72f0f2f34 | ||
|
|
23944f4fe0 | ||
|
|
c244199190 | ||
|
|
8848c06b15 | ||
|
|
37d56f7365 | ||
|
|
e2d6b5afc4 | ||
|
|
e4df577a32 | ||
|
|
f005cb784f | ||
|
|
d733b1f8f8 | ||
|
|
d8d403ffb8 | ||
|
|
574d68f678 | ||
|
|
1b3d7b33c6 | ||
|
|
7f4a304f04 | ||
|
|
d0a06b5cf7 | ||
|
|
6dfd44e2f1 | ||
|
|
f1eb6e1ce4 | ||
|
|
004729b2a4 | ||
|
|
c27d9978fe | ||
|
|
f608cb2d65 | ||
|
|
28a1610573 | ||
|
|
03e7699b8b | ||
|
|
11f4275e7b | ||
|
|
07fe1c6f19 | ||
|
|
3e78b03785 | ||
|
|
6a31d612e6 | ||
|
|
ecd8869097 | ||
|
|
73c2be147c | ||
|
|
caa290f580 | ||
|
|
d69288076a | ||
|
|
df00160bc4 | ||
|
|
be18843b09 | ||
|
|
f1ff032e1e | ||
|
|
36df121cf6 | ||
|
|
c22bb7fb92 | ||
|
|
b94bb38922 | ||
|
|
1576a946b0 | ||
|
|
4de0a11f1a | ||
|
|
66fd9e188b | ||
|
|
a0f44a088f | ||
|
|
fb20be833c | ||
|
|
fc43f95ea5 | ||
|
|
29d04fe57d | ||
|
|
bd32f5a1b8 | ||
|
|
98c353b9fe | ||
|
|
342ac76982 | ||
|
|
41b80422c5 | ||
|
|
c1f608d02f | ||
|
|
abc830eda2 | ||
|
|
60b6dbb388 | ||
|
|
7610466ee4 | ||
|
|
9f8831eed6 | ||
|
|
0ac981586f | ||
|
|
fc085111db | ||
|
|
5e03d97869 | ||
|
|
a11ae6655e | ||
|
|
2471de20a9 | ||
|
|
8e99d47869 | ||
|
|
eebc9c2bfa | ||
|
|
bd5c85147d | ||
|
|
7f7a8338ff | ||
|
|
2a9945f09f | ||
|
|
b7241f79cb | ||
|
|
76ccbfbf20 | ||
|
|
77c58eae2e | ||
|
|
4a2b8dc261 | ||
|
|
fa1a0e2351 | ||
|
|
f7b36a9b05 | ||
|
|
f4ce2437a7 | ||
|
|
aa34bb44c9 | ||
|
|
e3c90ace73 | ||
|
|
7c1545a07d | ||
|
|
953c612830 | ||
|
|
5dbb5499c6 | ||
|
|
d4f6c329b8 | ||
|
|
a574ec0777 | ||
|
|
3e5a72fa96 | ||
|
|
4df2a73800 | ||
|
|
aea9296908 | ||
|
|
08eeb57cb0 | ||
|
|
e5e9a9efe1 | ||
|
|
aafc6fad7d | ||
|
|
b91f18a8a0 | ||
|
|
20bfff5157 | ||
|
|
3c735e599f | ||
|
|
4958ade937 | ||
|
|
57dc5d4923 | ||
|
|
3c5ad74e38 | ||
|
|
e988e20f5b | ||
|
|
cac6dbd40c | ||
|
|
2461b4ab6a | ||
|
|
7c4f163042 | ||
|
|
f6c95fb8b7 | ||
|
|
2fee37239f | ||
|
|
2cb19848aa | ||
|
|
913cde44ff | ||
|
|
c7ff1fc07f | ||
|
|
da42e835c5 | ||
|
|
7a071496f3 | ||
|
|
b8d65f2f56 | ||
|
|
662f039daa | ||
|
|
20c46bd27f | ||
|
|
9c197ea25a | ||
|
|
d75db5d378 | ||
|
|
a2538bed20 | ||
|
|
69c45d63a4 | ||
|
|
80003f6c57 | ||
|
|
9d67724da9 | ||
|
|
3578a7e1e2 | ||
|
|
533586f516 | ||
|
|
591ccf564c | ||
|
|
ecc91af1d6 | ||
|
|
4ff043f759 | ||
|
|
84e18aae5a | ||
|
|
b53bda937a | ||
|
|
4db4bba73f | ||
|
|
2c2e6d6027 | ||
|
|
1aeea034d2 | ||
|
|
63bd483b3e | ||
|
|
19cb24d8db | ||
|
|
96ebe0f617 | ||
|
|
eb3178bf80 | ||
|
|
a72f47df46 | ||
|
|
a9823d39e2 | ||
|
|
6ec65eee23 | ||
|
|
9c2610ff40 | ||
|
|
2d47cd2a76 | ||
|
|
6eb938bb37 | ||
|
|
94a431eec8 | ||
|
|
4eb71b1220 | ||
|
|
74122d9057 | ||
|
|
914521cada | ||
|
|
70bda94033 | ||
|
|
915137af5e | ||
|
|
7516c0cbd3 | ||
|
|
fdfae9a771 | ||
|
|
8cc693461d | ||
|
|
e7f8cda6ae | ||
|
|
b9f7e820c7 | ||
|
|
26cc272b37 | ||
|
|
bffa6eb0c9 | ||
|
|
2779055e50 | ||
|
|
37d00f1255 | ||
|
|
d9b599e814 | ||
|
|
40d453bc7c | ||
|
|
6ff0cfe383 | ||
|
|
a6b7ed4dd2 | ||
|
|
bf0614026d | ||
|
|
06005009e4 | ||
|
|
cf16566da8 | ||
|
|
34f104b406 | ||
|
|
766ab8f10a | ||
|
|
aa4276a50e | ||
|
|
fbedafb204 | ||
|
|
85cd7c7336 | ||
|
|
c137d40037 | ||
|
|
5a9e7850c2 | ||
|
|
6e7342d6f0 | ||
|
|
1598adfa67 | ||
|
|
b49936c24b | ||
|
|
816f4f75f6 | ||
|
|
a091a18604 | ||
|
|
edadb3cb77 | ||
|
|
3749a5c2b1 | ||
|
|
e9b5e4ab0c | ||
|
|
28109d28dc | ||
|
|
7f56797779 | ||
|
|
a95eef0545 | ||
|
|
bbf6c3589a | ||
|
|
4a4a14b2ab | ||
|
|
6b0c3b65b4 | ||
|
|
59006d354f | ||
|
|
fe2d02a24c | ||
|
|
7c357a2aa1 | ||
|
|
0e6380a8bd | ||
|
|
26c9406211 | ||
|
|
5eb8432544 | ||
|
|
fb13a1c98d | ||
|
|
b20eb28a37 | ||
|
|
d84f071c62 | ||
|
|
bc7297de2e | ||
|
|
a2c4f73e7d | ||
|
|
9804c3933f | ||
|
|
e2b0da7830 | ||
|
|
5a5119a367 | ||
|
|
c310a8c1c2 | ||
|
|
11bfdd89b8 | ||
|
|
6898425435 | ||
|
|
be2557611e | ||
|
|
1a9a726263 | ||
|
|
dbf82f69f1 | ||
|
|
107e54688b | ||
|
|
b99282a5a7 | ||
|
|
1c0eb720ad | ||
|
|
93482f9022 | ||
|
|
8159c408c8 | ||
|
|
0632d78f71 | ||
|
|
c0155052ea | ||
|
|
628b2542a0 | ||
|
|
85f1da942f | ||
|
|
3909d5aef9 | ||
|
|
f0e047e7cc | ||
|
|
a1237305d7 | ||
|
|
d588a92147 | ||
|
|
2b7a1e1cb2 | ||
|
|
c8efca3120 | ||
|
|
a53eacf055 | ||
|
|
1b10a4001a | ||
|
|
75e71dd6f5 | ||
|
|
3f87b9f7d3 | ||
|
|
32561cf368 | ||
|
|
bf94cdcb6f | ||
|
|
fcfd3171bd | ||
|
|
9a6cf8c5d2 | ||
|
|
91d928fd8a | ||
|
|
bca653bc4d | ||
|
|
ed099aa061 | ||
|
|
2bedc6d7d4 | ||
|
|
674fb6ff57 | ||
|
|
79c8309291 | ||
|
|
9745daf6e2 | ||
|
|
5f54777663 | ||
|
|
90632b78ce | ||
|
|
f71850d8b1 | ||
|
|
99d3d28754 | ||
|
|
08b0f47ea2 | ||
|
|
f9b42a30f7 | ||
|
|
7c69d2a74d | ||
|
|
89bd082967 | ||
|
|
f4c26053c0 | ||
|
|
47d7c69d1b | ||
|
|
909affcf99 | ||
|
|
86856605b9 | ||
|
|
dae297e0f5 | ||
|
|
6e5f071f22 | ||
|
|
12c155b46f | ||
|
|
f51c51f041 | ||
|
|
870a4c3363 | ||
|
|
aa951ff96c | ||
|
|
bae9fe939d | ||
|
|
43222b7651 | ||
|
|
70f86c6ebd | ||
|
|
b7cb6dc444 | ||
|
|
8492c63f62 | ||
|
|
73c68fd11c | ||
|
|
8c986bb97d | ||
|
|
74b4cb2afd | ||
|
|
fa96836b63 | ||
|
|
e763ae1631 | ||
|
|
008b31e530 | ||
|
|
b6e445c445 | ||
|
|
c5935ec262 | ||
|
|
5f67494f77 | ||
|
|
deb9c6651f | ||
|
|
440ad516df | ||
|
|
929469d0c0 | ||
|
|
e98b614f05 | ||
|
|
d541a70da5 | ||
|
|
cc9586aa64 | ||
|
|
f7561b7824 | ||
|
|
83abdc2ee6 | ||
|
|
e0400c0425 | ||
|
|
687b7e04d9 | ||
|
|
4bad047f93 | ||
|
|
28a7f24989 | ||
|
|
28855d02a6 | ||
|
|
650ec04417 | ||
|
|
9ef11bca99 | ||
|
|
88b34a7ba3 | ||
|
|
9d86384032 | ||
|
|
a6bc87bcea | ||
|
|
63add047b6 | ||
|
|
a0e88bb24f | ||
|
|
5b14e0e9b5 | ||
|
|
274e734135 | ||
|
|
3818424251 | ||
|
|
2222550669 | ||
|
|
93b9f1d1da | ||
|
|
f43a155e6e | ||
|
|
f4e9516233 | ||
|
|
7f7f3557b3 | ||
|
|
b9b3d284cf | ||
|
|
4f240bf110 | ||
|
|
7cd82ffc4e | ||
|
|
4448410c3e | ||
|
|
49db31426c | ||
|
|
ce31d30ed7 | ||
|
|
68831c759f | ||
|
|
5ab867f21e | ||
|
|
4126188df1 | ||
|
|
26050e2134 | ||
|
|
5c0d6e6012 | ||
|
|
de7b13bc15 | ||
|
|
b6bd7ccf67 | ||
|
|
822d0c7738 | ||
|
|
183dd63021 | ||
|
|
0afc2ab2e6 | ||
|
|
119755e23a | ||
|
|
41fdf48ad3 | ||
|
|
ebdbb39f24 | ||
|
|
976740dc8b | ||
|
|
cac87b14c7 | ||
|
|
220f4fad24 | ||
|
|
05c1d31550 | ||
|
|
c8bacabf24 | ||
|
|
bfab34f8c6 | ||
|
|
df5ed5190a | ||
|
|
30dac3a73c | ||
|
|
ba4c9745a2 | ||
|
|
a1c275479f | ||
|
|
708cbdc9e5 | ||
|
|
b0585e28ad | ||
|
|
575aa447e0 | ||
|
|
e57b88a019 | ||
|
|
380c1444ca | ||
|
|
a59135430c | ||
|
|
bdf2c97942 | ||
|
|
a3c01305df | ||
|
|
ad1dfc8e2b | ||
|
|
2c573bfef5 | ||
|
|
fd5ff2c61a | ||
|
|
1d03b200a5 | ||
|
|
177c90c8e9 | ||
|
|
933451b1ec | ||
|
|
b068749380 | ||
|
|
f82f893014 | ||
|
|
effeffd906 | ||
|
|
c269d32247 | ||
|
|
17b081b18b | ||
|
|
a917937f12 | ||
|
|
7fc0cadb81 | ||
|
|
7094d43ee5 | ||
|
|
51296a9100 | ||
|
|
0803362a50 | ||
|
|
7e80787679 | ||
|
|
fc99328459 | ||
|
|
c594fc58b3 | ||
|
|
24cf78bc03 | ||
|
|
17aa564c57 | ||
|
|
7d37602d43 | ||
|
|
44f2e38387 | ||
|
|
7830c7e2eb | ||
|
|
deef5998c5 | ||
|
|
01ae858a14 | ||
|
|
634b099ade | ||
|
|
240342007b | ||
|
|
bc7731b819 | ||
|
|
00fd1e415c | ||
|
|
f81d16309c | ||
|
|
fe09b89fcb | ||
|
|
6314672109 | ||
|
|
d995169b3c | ||
|
|
2c6e00702c | ||
|
|
6d9e564d1c | ||
|
|
d751addf9d | ||
|
|
1af7adc09b | ||
|
|
7b287fb0f4 | ||
|
|
489c65af3e | ||
|
|
4e11a0c737 | ||
|
|
96b955f2fe | ||
|
|
f5014f29c3 | ||
|
|
b237f29636 | ||
|
|
c0c674d862 | ||
|
|
7c4721932d | ||
|
|
b093be52a2 | ||
|
|
65f1c19721 | ||
|
|
2502c0e87c | ||
|
|
2a91d3ddbd | ||
|
|
e7dc757293 | ||
|
|
1fe2a26e83 | ||
|
|
9b7471d6d2 | ||
|
|
b4ce621630 | ||
|
|
bf88f63482 | ||
|
|
20d48d7dc2 | ||
|
|
4b1d6ebd7c | ||
|
|
e9db7d1bb9 | ||
|
|
1c2ae8392c | ||
|
|
e8b9b3d583 | ||
|
|
71b84e1aba | ||
|
|
99f5aad942 | ||
|
|
8feae7efb6 | ||
|
|
874cbe1da4 | ||
|
|
14c7a11528 | ||
|
|
b4a7dc0cbd | ||
|
|
8c5f2ff61c | ||
|
|
770025da04 | ||
|
|
eb719e34a8 | ||
|
|
b54448f830 | ||
|
|
b88480c9ba | ||
|
|
a8897b2813 | ||
|
|
cb139ae775 | ||
|
|
89a788ff9f |
@@ -10,7 +10,7 @@ orbs:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:20.17.0
|
- image: cimg/node:20.18.0
|
||||||
- image: mongo:4.4
|
- image: mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
@@ -64,9 +64,6 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Mustache Spans
|
name: Test - Mustache Spans
|
||||||
command: npm run test:mustache-syntax
|
command: npm run test:mustache-syntax
|
||||||
- run:
|
|
||||||
name: Test - Definition Lists
|
|
||||||
command: npm run test:definition-lists
|
|
||||||
- run:
|
- run:
|
||||||
name: Test - Hard Breaks
|
name: Test - Hard Breaks
|
||||||
command: npm run test:hard-breaks
|
command: npm run test:hard-breaks
|
||||||
|
|||||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -5,6 +5,15 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 99
|
open-pull-requests-limit: 99
|
||||||
|
groups:
|
||||||
|
dev-dependencies:
|
||||||
|
dependency-type: "development"
|
||||||
|
patterns: ["*"]
|
||||||
|
update-types: ["patch", "minor"]
|
||||||
|
prod-dependencies:
|
||||||
|
dependency-type: "production"
|
||||||
|
patterns: ["*"]
|
||||||
|
update-types: ["patch", "minor"]
|
||||||
ignore:
|
ignore:
|
||||||
- dependency-name: eslint
|
- dependency-name: eslint
|
||||||
versions:
|
versions:
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"stylelint-config-recess-order",
|
"stylelint-config-recess-order",
|
||||||
"stylelint-config-recommended"],
|
"stylelint-config-recommended"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@stylistic/stylelint-plugin",
|
"@stylistic/stylelint-plugin",
|
||||||
"./stylelint_plugins/declaration-colon-align.js",
|
"./stylelint_plugins/declaration-colon-align.js",
|
||||||
"./stylelint_plugins/declaration-colon-min-space-before",
|
"./stylelint_plugins/declaration-colon-min-space-before",
|
||||||
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
||||||
],
|
],
|
||||||
"customSyntax": "postcss-less",
|
"customSyntax": "postcss-less",
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-descending-specificity" : null,
|
"no-descending-specificity" : null,
|
||||||
"at-rule-no-unknown" : null,
|
"at-rule-no-unknown" : null,
|
||||||
"function-no-unknown" : null,
|
"function-no-unknown" : null,
|
||||||
"font-family-no-missing-generic-family-keyword" : null,
|
"font-family-no-missing-generic-family-keyword" : null,
|
||||||
"font-weight-notation" : "named-where-possible",
|
"font-weight-notation" : "named-where-possible",
|
||||||
"font-family-name-quotes" : "always-unless-keyword",
|
"font-family-name-quotes" : "always-unless-keyword",
|
||||||
"@stylistic/indentation" : "tab",
|
"@stylistic/indentation" : "tab",
|
||||||
"no-duplicate-selectors" : true,
|
"no-duplicate-selectors" : true,
|
||||||
"@stylistic/color-hex-case" : "upper",
|
"@stylistic/color-hex-case" : "upper",
|
||||||
"color-hex-length" : "long",
|
"color-hex-length" : "long",
|
||||||
"@stylistic/selector-combinator-space-after" : "always",
|
"@stylistic/selector-combinator-space-after" : "always",
|
||||||
"@stylistic/selector-combinator-space-before" : "always",
|
"@stylistic/selector-combinator-space-before" : "always",
|
||||||
"@stylistic/selector-attribute-operator-space-before" : "never",
|
"@stylistic/selector-attribute-operator-space-before" : "never",
|
||||||
"@stylistic/selector-attribute-operator-space-after" : "never",
|
"@stylistic/selector-attribute-operator-space-after" : "never",
|
||||||
"@stylistic/selector-attribute-brackets-space-inside" : "never",
|
"@stylistic/selector-attribute-brackets-space-inside" : "never",
|
||||||
"selector-attribute-quotes" : "always",
|
"selector-attribute-quotes" : "always",
|
||||||
"selector-pseudo-element-colon-notation" : "double",
|
"selector-pseudo-element-colon-notation" : "double",
|
||||||
"@stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
"@stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
||||||
"@stylistic/block-opening-brace-space-before" : "always",
|
"@stylistic/block-opening-brace-space-before" : "always",
|
||||||
"naturalcrit/declaration-colon-min-space-before" : 1,
|
"naturalcrit/declaration-colon-min-space-before" : 1,
|
||||||
"@stylistic/declaration-block-trailing-semicolon" : "always",
|
"@stylistic/declaration-block-trailing-semicolon" : "always",
|
||||||
"@stylistic/declaration-colon-space-after" : "always",
|
"@stylistic/declaration-colon-space-after" : "always",
|
||||||
"@stylistic/number-leading-zero" : "always",
|
"@stylistic/number-leading-zero" : "always",
|
||||||
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
||||||
"function-url-scheme-disallowed-list" : ["data","http"],
|
"function-url-scheme-disallowed-list" : ["data","http"],
|
||||||
"comment-whitespace-inside" : "always",
|
"comment-whitespace-inside" : "always",
|
||||||
"@stylistic/string-quotes" : "single",
|
"@stylistic/string-quotes" : "single",
|
||||||
"@stylistic/media-feature-range-operator-space-before" : "always",
|
"@stylistic/media-feature-range-operator-space-before" : "always",
|
||||||
"@stylistic/media-feature-range-operator-space-after" : "always",
|
"@stylistic/media-feature-range-operator-space-after" : "always",
|
||||||
"@stylistic/media-feature-parentheses-space-inside" : "never",
|
"@stylistic/media-feature-parentheses-space-inside" : "never",
|
||||||
"@stylistic/media-feature-colon-space-before" : "always",
|
"@stylistic/media-feature-colon-space-before" : "always",
|
||||||
"@stylistic/media-feature-colon-space-after" : "always",
|
"@stylistic/media-feature-colon-space-after" : "always",
|
||||||
"naturalcrit/declaration-colon-align" : true,
|
"naturalcrit/declaration-colon-align" : true,
|
||||||
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
"naturalcrit/declaration-block-multi-line-min-declarations" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
@@ -9,7 +9,10 @@ WORKDIR /usr/src/app
|
|||||||
# Copy package.json into the image, then run yarn install
|
# Copy package.json into the image, then run yarn install
|
||||||
# This improves caching so we don't have to download the dependencies every time the code changes
|
# This improves caching so we don't have to download the dependencies every time the code changes
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
|
COPY config/docker.json usr/src/app/config
|
||||||
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
||||||
|
RUN node --version
|
||||||
|
RUN npm --version
|
||||||
RUN npm install --ignore-scripts
|
RUN npm install --ignore-scripts
|
||||||
|
|
||||||
# Bundle app source and build application
|
# Bundle app source and build application
|
||||||
|
|||||||
136
README.DOCKER.md
136
README.DOCKER.md
@@ -1,12 +1,132 @@
|
|||||||
# Running Homebrewery via Docker
|
# Offline Install Instructions: Docker
|
||||||
|
|
||||||
The repo includes a Dockerfile and a docker-compose.yml file.
|
These instructions are for setting up a persistent instance of the Homebrewery application locally using Docker.
|
||||||
|
|
||||||
To run the application via docker-compose.yml:
|
If you intend to develop with Homebrewery, following the Homebrewery application section of this guide is not recommended. Using docker to deploy MongoDB locally for development is not a bad idea at all, however.
|
||||||
`docker-compose up -d`
|
|
||||||
|
|
||||||
To stop the application:
|
# Install Docker
|
||||||
`docker-compose down`
|
|
||||||
|
## Docker Desktop (MacOS/Windows)
|
||||||
|
|
||||||
|
Windows and Mac installs use Docker Desktop. Current install instructions are below.
|
||||||
|
|
||||||
|
* [Mac](https://docs.docker.com/desktop/mac/install/)
|
||||||
|
* [Windows](https://docs.docker.com/desktop/windows/install/)
|
||||||
|
|
||||||
|
You can set up the docker engine to start on boot via the Docker desktop UI.
|
||||||
|
|
||||||
|
## Docker Engine
|
||||||
|
|
||||||
|
Linux installs use Docker Engine. Docker provides installers and instructions for several of the most common distrubutions. If you do not see yours listed, it is very likely supported indirectly by your distribution.
|
||||||
|
|
||||||
|
* [Arch](https://docs.docker.com/desktop/setup/install/linux/archlinux/)
|
||||||
|
* [CentOS](https://docs.docker.com/engine/install/centos/)
|
||||||
|
* [Debian](https://docs.docker.com/engine/install/debian/)
|
||||||
|
* [Fedora](https://docs.docker.com/engine/install/fedora/)
|
||||||
|
* [RHEL](https://docs.docker.com/engine/install/rhel/)
|
||||||
|
* [Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
|
||||||
|
|
||||||
|
### Post installation steps
|
||||||
|
[Manage Docker as a non-root user (highly recommended)](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
|
||||||
|
[Enable Docker to start on boot (highly recommended)](https://docs.docker.com/engine/install/linux-postinstall/#configure-docker-to-start-on-boot)
|
||||||
|
|
||||||
|
# Build Homebrewery Image
|
||||||
|
|
||||||
|
Next we build the homebrewery docker image. Start by cloning the repository.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/naturalcrit/homebrewery.git
|
||||||
|
cd homebrewery
|
||||||
|
```
|
||||||
|
|
||||||
|
Make an changes you need to `config/docker.json` then build the image. If it does not exist,the below as a template.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"host" : "localhost:8000",
|
||||||
|
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||||
|
"secret" : "secret",
|
||||||
|
"web_port" : 8000,
|
||||||
|
"enable_v3" : true,
|
||||||
|
"mongodb_uri": "mongodb://172.17.0.2/homebrewery",
|
||||||
|
"enable_themes" : true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker-compose build homebrewery
|
||||||
|
```
|
||||||
|
|
||||||
|
# Add Mongo container
|
||||||
|
|
||||||
|
Once docker is installed and running, it is time to set up the containers. First up, Mongo.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 mongo:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Older CPUs may run into an issue with AVX support.
|
||||||
|
```
|
||||||
|
WARNING: MongoDB 5.0+ requires a CPU with AVX support, and your current system does not appear to have that!
|
||||||
|
see https://jira.mongodb.org/browse/SERVER-54407
|
||||||
|
see also https://www.mongodb.com/community/forums/t/mongodb-5-0-cpu-intel-g4650-compatibility/116610/2
|
||||||
|
see also https://github.com/docker-library/mongo/issues/485#issuecomment-891991814
|
||||||
|
```
|
||||||
|
If you see a message similar to this, try using the bitnami mongo instead.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 bitnami/mongo:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
If your distribution is running on an arm device such as a Raspberry Pi, you will need to run the arm-built MongoDB v4.4.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/data/db -p 27017:27017 arm64v8/mongo:4.4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the Homebrewery Image
|
||||||
|
```shell
|
||||||
|
# Make sure you run this in the homebrewery directory
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
|
||||||
|
```shell
|
||||||
|
# Make sure you run this in the homebrewery directory
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Updating the Image
|
||||||
|
|
||||||
|
When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image.
|
||||||
|
|
||||||
|
First, return to your homebrewery clone (from Build Homebrewery Image above) or recreate the clone if you deleted your copy of the code.
|
||||||
|
|
||||||
|
First, delete the existing image.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker rm -f homebrewery-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, update the clone's code to the latest version.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd homebrewery
|
||||||
|
git checkout master
|
||||||
|
git pull upstream master
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, rebuild and restart the homebrewery image.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
docker-compose build homebrewery
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
|
||||||
|
```shell
|
||||||
|
# Make sure you run this in the homebrewery directory
|
||||||
|
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
|
||||||
|
```
|
||||||
|
|
||||||
To stop the application and remove all data:
|
|
||||||
`docker-compose down -v`
|
|
||||||
|
|||||||
@@ -75,8 +75,9 @@ 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)
|
When the Homebrewery server is started for the first time, it will modify the database to create the indexes required for better Homebrewery performance. This may take a few moments to complete for each index, dependent on how much content is in your local database - a brand new, empty database should be done in seconds.
|
||||||
in your browser and use The Homebrewery offline.
|
|
||||||
|
On completion, you should be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use The Homebrewery offline.
|
||||||
|
|
||||||
If you had any issue at all, here are some links that may be useful:
|
If you had any issue at all, here are some links that may be useful:
|
||||||
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
|
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
|
||||||
@@ -144,3 +145,5 @@ 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-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
|
[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
|
[gitter-url]: https://gitter.im/naturalcrit/Lobby
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
226
changelog.md
226
changelog.md
@@ -77,14 +77,234 @@ pre {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.varSyntaxTable th:first-of-type {
|
.varSyntaxTable th:first-of-type {
|
||||||
width:6cm;
|
width:6cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .exampleTable td,th {
|
||||||
|
border:1px dashed #00000030;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
### Wednesday 7/09/2025 - v3.19.3
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
* [x] Restoring original saving behavior; will continue investigating why save was failing for some users in background
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Wednesday 7/09/2025 - v3.19.2
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
* [x] Hotfix for saving issues - Please refresh your browser and report if problems continue
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 7/09/2025 - v3.19.1
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
* [x] Send diffs instead of full file on save - should help with timeout/disconnect errors
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Thursday 05/22/2025 - v3.19.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### abquintic
|
||||||
|
* [x] Fix crash due to colons after `\page`
|
||||||
|
|
||||||
|
Fixes issue [#4105](https://github.com/naturalcrit/homebrewery/issues/4105)
|
||||||
|
|
||||||
|
* [x] Fix images with spaces in alt text not rendering
|
||||||
|
|
||||||
|
Fixes issue [#3659](https://github.com/naturalcrit/homebrewery/issues/3659)
|
||||||
|
|
||||||
|
* [x] Custom snippets! Open the new {{openSans **:fas_table_list: SNIPPETS**}} tab (next to the {{openSans **:fas_paintbrush: STYLE**}} tab). Custom snippets will appear in a new snippet dropdown, and will be included when imported as a custom theme.
|
||||||
|
|
||||||
|
* [x] Move several generic styles/snippets from PHB to the Blank theme; generic snippets like image masks no longer require the PHB theme.
|
||||||
|
|
||||||
|
* [x] Extract several Markdown+ syntax extensions into their own NPM packages, for use by the wider community.
|
||||||
|
|
||||||
|
* [x] Allow `\pagebreak` and `\columnbreak` as alternatives to `\page` and `\column`
|
||||||
|
|
||||||
|
Partially fixes issue [#4035](https://github.com/naturalcrit/homebrewery/issues/4035)
|
||||||
|
|
||||||
|
* [x] Fix misbehaving column breaks on old Chrome
|
||||||
|
|
||||||
|
Fixes issue [#4192](https://github.com/naturalcrit/homebrewery/issues/4192)
|
||||||
|
|
||||||
|
* [x] Self-host font-awesome icons; fix missing icons on local installs
|
||||||
|
|
||||||
|
Fixes issue [#1965](https://github.com/naturalcrit/homebrewery/issues/1965)
|
||||||
|
Fixes issue [#1548](https://github.com/naturalcrit/homebrewery/issues/1548)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
* [x] Fix CORS issue on local installs
|
||||||
|
|
||||||
|
* [x] Fix print size issues when using the Facing and Flow view options.
|
||||||
|
|
||||||
|
Fixes issue [#4146](https://github.com/naturalcrit/homebrewery/issues/4146)
|
||||||
|
|
||||||
|
* [x] New built-in `$[HB_pageNumber]` variable. Works with math operations or can be reassigned like any other variable for more customization over the old `{{pageNumber,auto}}` snippet.\
|
||||||
|
New snippet found at {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBERING :fas_arrow_right: :fas_arrow_down_1_9: VARIABLE AUTO PAGE NUMBER**}}
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
* [x] Fix search bar covering up snippet bar (3 times)
|
||||||
|
|
||||||
|
Fixes issue [#4098](https://github.com/naturalcrit/homebrewery/issues/4098)
|
||||||
|
|
||||||
|
* [x] Save view toolbar settings across sessions
|
||||||
|
|
||||||
|
Fixes issue [#3835](https://github.com/naturalcrit/homebrewery/issues/3835)
|
||||||
|
|
||||||
|
* [x] Fix styling issues on the view toolbar
|
||||||
|
|
||||||
|
* [x] Update the Darkbrewery editor theme
|
||||||
|
|
||||||
|
Fixes issue [#3312](https://github.com/naturalcrit/homebrewery/issues/3312)
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Monday 03/10/2025 - v3.18.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### abquintic
|
||||||
|
* [x] Add ability to paste in any Share ID/URL into a brew's {{openSans :fas_circle_info: **Properties** :fas_arrow_right: **THEMES**}} selection, as long as that brew has been tagged as `meta:theme`. You can now share your custom brew themes without needing to make a personal copy.
|
||||||
|
* [x] Begin migration of custom Markdown extensions into their own NPM packages, for easier adoption by other users or projects
|
||||||
|
* [x] Fix external HTML appearing in open codeblocks
|
||||||
|
|
||||||
|
Fixes issue [#3206](https://github.com/naturalcrit/homebrewery/issues/3206)
|
||||||
|
|
||||||
|
* [x] Fix tables not rendering when directly after text
|
||||||
|
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
* [x] Cleanup of "cover pages" in the {{openSans :fas_rectangle_list: **NAVIGATION**}} list
|
||||||
|
* [x] Fix autosave triggering when no changes are present
|
||||||
|
|
||||||
|
Fixes issue [#4051](https://github.com/naturalcrit/homebrewery/issues/4051)
|
||||||
|
|
||||||
|
* [x] Remove empty table rows resulting from rowspan
|
||||||
|
|
||||||
|
Fixes issue [#1729](https://github.com/naturalcrit/homebrewery/issues/1729)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
* [x] Style fixes for covers art and logos on A4 size pages
|
||||||
|
* [x] Fix crash when trying to open brews that don't exist
|
||||||
|
* [x] Tweaks and style update styling on {{openSans **VAULT** :fas_dungeon:}} page.
|
||||||
|
|
||||||
|
Fixes issue [#4079](https://github.com/naturalcrit/homebrewery/issues/4079)
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
* [x] `꞉꞉꞉꞉` now produces `<br>` instead of a `<div>`
|
||||||
|
* [x] Fix typos in tables freezing the editor
|
||||||
|
|
||||||
|
Fixes issue [#4059](https://github.com/naturalcrit/homebrewery/issues/4059)
|
||||||
|
|
||||||
|
|
||||||
|
##### MollyMaclachlan (New Contributor!)
|
||||||
|
* [x] Fixed typos in the Monster Stat Block snippet
|
||||||
|
|
||||||
|
Fixes issue [#4073](https://github.com/naturalcrit/homebrewery/issues/4073)
|
||||||
|
|
||||||
|
|
||||||
|
##### All
|
||||||
|
* [x] Update dependencies and scripts
|
||||||
|
* [x] Refactor components and backend tools
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Thursday 01/30/2025 - v3.17.0
|
||||||
|
|
||||||
|
{{taskList
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Update FAQ
|
||||||
|
|
||||||
|
* [x] Fix styling for Vault buttons and checkboxes
|
||||||
|
|
||||||
|
* [x] Improve navigation bar styling
|
||||||
|
|
||||||
|
* [x] Add feature to change username at https://www.naturalcrit.com/account
|
||||||
|
|
||||||
|
* [x] Fix Reddit link crash when title has non-latin chars
|
||||||
|
|
||||||
|
##### abquintic
|
||||||
|
|
||||||
|
* [x] Fix page shadows toolbar option
|
||||||
|
|
||||||
|
Fixes issue [#3919](https://github.com/naturalcrit/homebrewery/issues/3919)
|
||||||
|
|
||||||
|
* [x] Add `:>>>` syntax for horizontal :>>>>> spaces
|
||||||
|
|
||||||
|
* [x] Update Docker install instructions
|
||||||
|
|
||||||
|
Fixes issue [#1930](https://github.com/naturalcrit/homebrewery/issues/1930)
|
||||||
|
|
||||||
|
* [x] Allow styling pages via `\page{myStyles}` (with calculuschild)
|
||||||
|
|
||||||
|
Fixes issue [#3901](https://github.com/naturalcrit/homebrewery/issues/3901)
|
||||||
|
|
||||||
|
* [x] Update Ubuntu install instructions
|
||||||
|
|
||||||
|
Fixes issue [#1952](https://github.com/naturalcrit/homebrewery/issues/1952)
|
||||||
|
|
||||||
|
* [x] Add `:-:` `:-` `-:` syntax for paragraph alignment, similar to table column alignment; for example:
|
||||||
|
|
||||||
|
-: -: Right-aligned
|
||||||
|
|
||||||
|
:-: :-: Centered
|
||||||
|
|
||||||
|
* [x] Add `:-- 50% --:` syntax to allow setting table column widths by percentage; for example:
|
||||||
|
```
|
||||||
|
| Narrow | Wide |
|
||||||
|
|:- 10% -:|:-90%--:|
|
||||||
|
| Cell | Cell |
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
| Narrow | Wide |
|
||||||
|
|:- 10% -:|:-90%--:|
|
||||||
|
|Cell | Cell |
|
||||||
|
{exampleTable}
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix crash when opening brew Properties tab
|
||||||
|
|
||||||
|
Fixes issue [#3927](https://github.com/naturalcrit/homebrewery/issues/3927)
|
||||||
|
|
||||||
|
* [x] Update error pages with steps to refresh credentials
|
||||||
|
|
||||||
|
Fixes issue [#3955](https://github.com/naturalcrit/homebrewery/issues/3955)
|
||||||
|
|
||||||
|
* [x] Add {{openSans :fas_rectangle_list: **NAVIGATION**}} menu to the viewer toolbar
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Reduce display lag on large brews
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Smarter detection of current page number
|
||||||
|
|
||||||
|
Fixes issue [#3824](https://github.com/naturalcrit/homebrewery/issues/3824)
|
||||||
|
|
||||||
|
##### All
|
||||||
|
* [x] Update dependencies and scripts
|
||||||
|
* [x] Refactor components and fix various errors
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 11/27/2024 - v3.16.1
|
### Wednesday 11/27/2024 - v3.16.1
|
||||||
|
|
||||||
{{taskList
|
{{taskList
|
||||||
@@ -2053,4 +2273,4 @@ 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
|
||||||
@@ -1,47 +1,52 @@
|
|||||||
require('./admin.less');
|
import './admin.less';
|
||||||
const React = require('react');
|
import React, { useEffect, useState } from 'react';
|
||||||
const createClass = require('create-react-class');
|
|
||||||
|
|
||||||
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
||||||
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
||||||
|
import AuthorUtils from './authorUtils/authorUtils.jsx';
|
||||||
|
import LockTools from './lockTools/lockTools.jsx';
|
||||||
|
|
||||||
const tabGroups = ['brew', 'notifications'];
|
const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
|
||||||
|
|
||||||
const Admin = createClass({
|
const ADMIN_TAB = 'HB_adminPage_currentTab';
|
||||||
getDefaultProps : function() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function(){
|
const Admin = ()=>{
|
||||||
return ({
|
const [currentTab, setCurrentTab] = useState('');
|
||||||
currentTab : 'brew'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleClick : function(newTab){
|
useEffect(()=>{
|
||||||
if(this.state.currentTab === newTab) return;
|
setCurrentTab(localStorage.getItem(ADMIN_TAB) || 'brew');
|
||||||
this.setState({
|
}, []);
|
||||||
currentTab : newTab
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
useEffect(()=>{
|
||||||
return <div className='admin'>
|
localStorage.setItem(ADMIN_TAB, currentTab);
|
||||||
|
}, [currentTab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='admin'>
|
||||||
<header>
|
<header>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<i className='fas fa-rocket' />
|
<i className='fas fa-rocket' />
|
||||||
homebrewery admin
|
The Homebrewery Admin Page
|
||||||
|
<a href='/'>back to homepage</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className='container'>
|
<main className='container'>
|
||||||
<nav className='tabs'>
|
<nav className='tabs'>
|
||||||
{tabGroups.map((tab, idx)=>{ return <button className={tab===this.state.currentTab ? 'active' : ''} key={idx} onClick={()=>{ return this.handleClick(tab); }}>{tab.toUpperCase()}</button>; })}
|
{tabGroups.map((tab, idx)=>(
|
||||||
|
<button
|
||||||
|
className={tab === currentTab ? 'active' : ''}
|
||||||
|
key={idx}
|
||||||
|
onClick={()=>setCurrentTab(tab)}>
|
||||||
|
{tab.toUpperCase()}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
{this.state.currentTab==='brew' && <BrewUtils />}
|
{currentTab === 'brew' && <BrewUtils />}
|
||||||
{this.state.currentTab==='notifications' && <NotificationUtils />}
|
{currentTab === 'notifications' && <NotificationUtils />}
|
||||||
|
{currentTab === 'authors' && <AuthorUtils />}
|
||||||
|
{currentTab === 'locks' && <LockTools />}
|
||||||
</main>
|
</main>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = Admin;
|
module.exports = Admin;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@import 'naturalcrit/styles/animations.less';
|
@import 'naturalcrit/styles/animations.less';
|
||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
@import 'naturalcrit/styles/tooltip.less';
|
@import 'naturalcrit/styles/tooltip.less';
|
||||||
|
@import './themes/fonts/iconFonts/fontAwesome.less';
|
||||||
|
|
||||||
@import 'font-awesome/css/font-awesome.css';
|
@import 'font-awesome/css/font-awesome.css';
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:where(.admin) {
|
:where(.admin) {
|
||||||
|
padding-bottom : 50px;
|
||||||
header {
|
header {
|
||||||
padding : 20px 0px;
|
padding : 20px 0px;
|
||||||
margin-bottom : 30px;
|
margin-bottom : 30px;
|
||||||
@@ -30,6 +31,7 @@ body {
|
|||||||
color : white;
|
color : white;
|
||||||
background-color : @red;
|
background-color : @red;
|
||||||
i { margin-right : 30px; }
|
i { margin-right : 30px; }
|
||||||
|
a { float : right; }
|
||||||
}
|
}
|
||||||
|
|
||||||
hr { margin : 30px 0px; }
|
hr { margin : 30px 0px; }
|
||||||
@@ -48,21 +50,23 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dl {
|
dl {
|
||||||
@maxItemWidth : 132px;
|
display : grid;
|
||||||
|
grid-template-columns : 120px 1fr;
|
||||||
|
row-gap : 10px;
|
||||||
|
align-items : center;
|
||||||
|
justify-items : start;
|
||||||
|
padding-top : 0.5em;
|
||||||
dt {
|
dt {
|
||||||
float : left;
|
float : left;
|
||||||
width : @maxItemWidth;
|
clear : left;
|
||||||
clear : left;
|
height : fit-content;
|
||||||
text-align : right;
|
font-weight : 900;
|
||||||
|
text-align : right;
|
||||||
&::after { content : ' : '; }
|
&::after { content : ' : '; }
|
||||||
}
|
}
|
||||||
dd {
|
dd { height : fit-content; }
|
||||||
height : 1em;
|
|
||||||
padding : 0 0 0.5em 0;
|
|
||||||
margin-left : @maxItemWidth + 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs button {
|
.tabs button {
|
||||||
margin-right : 3px;
|
margin-right : 3px;
|
||||||
margin-left : 3px;
|
margin-left : 3px;
|
||||||
@@ -90,11 +94,45 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
padding : 10px;
|
||||||
|
|
||||||
|
tr {
|
||||||
|
border-bottom : 1px solid;
|
||||||
|
&:last-of-type { border : none; }
|
||||||
|
&:nth-child(even) { background : #DDDDDD; }
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
background : rgb(193,236,230);
|
||||||
|
border-bottom : 2px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding : 5px 10px;
|
||||||
|
vertical-align : middle;
|
||||||
|
text-align : center;
|
||||||
|
border-right : 1px solid;
|
||||||
|
|
||||||
|
&:last-child { border-right : none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
th { font-weight : 900; }
|
||||||
|
|
||||||
|
td {
|
||||||
|
&:first-child {
|
||||||
|
font-weight : 900;
|
||||||
|
text-align : left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
background: rgb(178, 54, 54);
|
float : right;
|
||||||
color:white;
|
padding : 10px;
|
||||||
font-weight: 900;
|
margin-block : 10px;
|
||||||
margin-block:10px;
|
font-weight : 900;
|
||||||
padding:10px;
|
color : white;
|
||||||
|
background : rgb(178, 54, 54);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
87
client/admin/authorUtils/authorLookup/authorLookup.jsx
Normal file
87
client/admin/authorUtils/authorLookup/authorLookup.jsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import './authorLookup.less';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import request from 'superagent';
|
||||||
|
|
||||||
|
const authorLookup = ()=>{
|
||||||
|
const [author, setAuthor] = React.useState('');
|
||||||
|
const [searching, setSearching] = React.useState(false);
|
||||||
|
const [results, setResults] = React.useState([]);
|
||||||
|
|
||||||
|
const lookup = async ()=>{
|
||||||
|
if(!author) return;
|
||||||
|
|
||||||
|
setSearching(true);
|
||||||
|
setResults([]);
|
||||||
|
|
||||||
|
const brews = await request.get(`/admin/user/list/${author}`);
|
||||||
|
setResults(brews.body);
|
||||||
|
setSearching(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderResults = ()=>{
|
||||||
|
if(results.length == 0) return <>
|
||||||
|
<h2>Results</h2>
|
||||||
|
<p>None found.</p>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<h2>{`Results - ${results.length} brews` }</h2>
|
||||||
|
<table className='resultsTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Share</th>
|
||||||
|
<th>Edit</th>
|
||||||
|
<th>Last Update</th>
|
||||||
|
<th>Storage</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{results
|
||||||
|
.sort((a, b)=>{ // Sort brews from most recently updated
|
||||||
|
if(a.updatedAt > b.updatedAt) return -1;
|
||||||
|
return 1;
|
||||||
|
})
|
||||||
|
.map((brew, idx)=>{
|
||||||
|
return <tr key={idx}>
|
||||||
|
<td><strong>{brew.title}</strong></td>
|
||||||
|
<td><a href={`/share/${brew.shareId}`}>{brew.shareId}</a></td>
|
||||||
|
<td>{brew.editId}</td>
|
||||||
|
<td style={{ width: '200px' }}>{brew.updatedAt}</td>
|
||||||
|
<td>{brew.googleId ? 'Google' : 'Homebrewery'}</td>
|
||||||
|
</tr>;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (evt)=>{
|
||||||
|
if(evt.key === 'Enter') return lookup();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChange = (evt)=>{
|
||||||
|
setAuthor(evt.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='authorLookup'>
|
||||||
|
<div className='authorLookupInputs'>
|
||||||
|
<h2>Author Lookup</h2>
|
||||||
|
<label className='field'>
|
||||||
|
Author Name:
|
||||||
|
<input className='fieldInput' value={author} onKeyDown={handleKeyPress} onChange={handleChange} />
|
||||||
|
<button onClick={lookup}>
|
||||||
|
<i className={`fas ${searching ? 'fa-spin fa-spinner' : 'fa-search'}`} />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className='authorLookupResults'>
|
||||||
|
{renderResults()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = authorLookup;
|
||||||
29
client/admin/authorUtils/authorLookup/authorLookup.less
Normal file
29
client/admin/authorUtils/authorLookup/authorLookup.less
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.authorLookup {
|
||||||
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display : flex;
|
||||||
|
gap : 5px;
|
||||||
|
align-items : center;
|
||||||
|
justify-items : stretch;
|
||||||
|
width : 100%;
|
||||||
|
margin-bottom : 20px;
|
||||||
|
|
||||||
|
|
||||||
|
input {
|
||||||
|
height : 33px;
|
||||||
|
padding : 0px 10px;
|
||||||
|
margin-bottom : unset;
|
||||||
|
font-family : monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width : 50px;
|
||||||
|
|
||||||
|
i { margin-right : 10px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
13
client/admin/authorUtils/authorUtils.jsx
Normal file
13
client/admin/authorUtils/authorUtils.jsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AuthorLookup from './authorLookup/authorLookup.jsx';
|
||||||
|
|
||||||
|
const authorUtils = ()=>{
|
||||||
|
return (
|
||||||
|
<section className='authorUtils'>
|
||||||
|
<AuthorLookup />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = authorUtils;
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
require('./brewCleanup.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|
||||||
const BrewCleanup = createClass({
|
const BrewCleanup = createClass({
|
||||||
displayName : 'BrewCleanup',
|
displayName : 'BrewCleanup',
|
||||||
getDefaultProps(){
|
getDefaultProps(){
|
||||||
@@ -39,9 +37,9 @@ const BrewCleanup = createClass({
|
|||||||
if(!this.state.primed) return;
|
if(!this.state.primed) return;
|
||||||
|
|
||||||
if(!this.state.count){
|
if(!this.state.count){
|
||||||
return <div className='removeBox'>No Matching Brews found.</div>;
|
return <div className='result noBrews'>No Matching Brews found.</div>;
|
||||||
}
|
}
|
||||||
return <div className='removeBox'>
|
return <div className='result'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
@@ -52,7 +50,7 @@ const BrewCleanup = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
render(){
|
render(){
|
||||||
return <div className='BrewCleanup'>
|
return <div className='brewUtil brewCleanup'>
|
||||||
<h2> Brew Cleanup </h2>
|
<h2> Brew Cleanup </h2>
|
||||||
<p>Removes very short brews to tidy up the database</p>
|
<p>Removes very short brews to tidy up the database</p>
|
||||||
|
|
||||||
@@ -65,7 +63,7 @@ const BrewCleanup = createClass({
|
|||||||
{this.renderPrimed()}
|
{this.renderPrimed()}
|
||||||
|
|
||||||
{this.state.error
|
{this.state.error
|
||||||
&& <div className='error'>{this.state.error.toString()}</div>
|
&& <div className='error noBrews'>{this.state.error.toString()}</div>
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
.BrewCleanup {
|
|
||||||
.removeBox {
|
|
||||||
margin-top : 20px;
|
|
||||||
button {
|
|
||||||
margin-right : 10px;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
require('./brewCompress.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|
||||||
const BrewCompress = createClass({
|
const BrewCompress = createClass({
|
||||||
displayName : 'BrewCompress',
|
displayName : 'BrewCompress',
|
||||||
getDefaultProps(){
|
getDefaultProps(){
|
||||||
@@ -53,9 +50,9 @@ const BrewCompress = createClass({
|
|||||||
if(!this.state.primed) return;
|
if(!this.state.primed) return;
|
||||||
|
|
||||||
if(!this.state.count){
|
if(!this.state.count){
|
||||||
return <div className='removeBox'>No Matching Brews found.</div>;
|
return <div className='result noBrews'>No Matching Brews found.</div>;
|
||||||
}
|
}
|
||||||
return <div className='removeBox'>
|
return <div className='result'>
|
||||||
<button onClick={this.cleanup} className='remove'>
|
<button onClick={this.cleanup} className='remove'>
|
||||||
{this.state.pending
|
{this.state.pending
|
||||||
? <i className='fas fa-spin fa-spinner' />
|
? <i className='fas fa-spin fa-spinner' />
|
||||||
@@ -69,7 +66,7 @@ const BrewCompress = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
render(){
|
render(){
|
||||||
return <div className='BrewCompress'>
|
return <div className='brewUtil brewCompress'>
|
||||||
<h2> Brew Compression </h2>
|
<h2> Brew Compression </h2>
|
||||||
<p>Compresses the text in brews to binary</p>
|
<p>Compresses the text in brews to binary</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
.BrewCompress {
|
|
||||||
.removeBox {
|
|
||||||
margin-top : 20px;
|
|
||||||
button {
|
|
||||||
margin-right : 10px;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
require('./brewLookup.less');
|
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
@@ -55,7 +53,7 @@ const BrewLookup = createClass({
|
|||||||
|
|
||||||
renderFoundBrew(){
|
renderFoundBrew(){
|
||||||
const brew = this.state.foundBrew;
|
const brew = this.state.foundBrew;
|
||||||
return <div className='foundBrew'>
|
return <div className='result'>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Title</dt>
|
<dt>Title</dt>
|
||||||
<dd>{brew.title}</dd>
|
<dd>{brew.title}</dd>
|
||||||
@@ -90,7 +88,7 @@ const BrewLookup = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render(){
|
render(){
|
||||||
return <div className='brewLookup'>
|
return <div className='brewUtil brewLookup'>
|
||||||
<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}>
|
||||||
@@ -106,7 +104,7 @@ const BrewLookup = createClass({
|
|||||||
|
|
||||||
{this.state.foundBrew
|
{this.state.foundBrew
|
||||||
? this.renderFoundBrew()
|
? this.renderFoundBrew()
|
||||||
: <div className='noBrew'>No brew found.</div>
|
: <div className='result noBrew'>No brew found.</div>
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
.brewLookup {
|
|
||||||
.cleanButton {
|
|
||||||
display : inline-block;
|
|
||||||
width : 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
require('./brewUtils.less');
|
||||||
|
|
||||||
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
||||||
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
||||||
|
|||||||
29
client/admin/brewUtils/brewUtils.less
Normal file
29
client/admin/brewUtils/brewUtils.less
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.brewUtil {
|
||||||
|
.result {
|
||||||
|
margin-top : 20px;
|
||||||
|
button {
|
||||||
|
margin-right : 10px;
|
||||||
|
background-color : @red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.cleanButton {
|
||||||
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
position : relative;
|
||||||
|
|
||||||
|
.pending {
|
||||||
|
position : absolute;
|
||||||
|
top : 0.5em;
|
||||||
|
left : 100px;
|
||||||
|
width : 100%;
|
||||||
|
height : 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.pending) { opacity : 0.5; }
|
||||||
|
|
||||||
|
dl { grid-template-columns : 200px 250px; }
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
require('./stats.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
|
|
||||||
const Stats = createClass({
|
const Stats = createClass({
|
||||||
displayName : 'Stats',
|
displayName : 'Stats',
|
||||||
getDefaultProps(){
|
getDefaultProps(){
|
||||||
@@ -14,7 +11,8 @@ const Stats = createClass({
|
|||||||
getInitialState(){
|
getInitialState(){
|
||||||
return {
|
return {
|
||||||
stats : {
|
stats : {
|
||||||
totalBrews : 0
|
totalBrews : 0,
|
||||||
|
totalPublishedBrews : 0
|
||||||
},
|
},
|
||||||
fetching : false
|
fetching : false
|
||||||
};
|
};
|
||||||
@@ -29,11 +27,13 @@ const Stats = createClass({
|
|||||||
.finally(()=>this.setState({ fetching: false }));
|
.finally(()=>this.setState({ fetching: false }));
|
||||||
},
|
},
|
||||||
render(){
|
render(){
|
||||||
return <div className='Stats'>
|
return <div className='brewUtil stats'>
|
||||||
<h2> Stats </h2>
|
<h2> Stats </h2>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>Total Brew Count</dt>
|
<dt>Total Brew Count</dt>
|
||||||
<dd>{this.state.stats.totalBrews}</dd>
|
<dd>{this.state.stats.totalBrews}</dd>
|
||||||
|
<dt>Total Brews Published</dt>
|
||||||
|
<dd>{this.state.stats.totalPublishedBrews}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
{this.state.fetching
|
{this.state.fetching
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
.Stats {
|
|
||||||
position : relative;
|
|
||||||
|
|
||||||
.pending {
|
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
left : 0px;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
background-color : rgba(238,238,238, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
342
client/admin/lockTools/lockTools.jsx
Normal file
342
client/admin/lockTools/lockTools.jsx
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
|
require('./lockTools.less');
|
||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
|
import request from '../../homebrew/utils/request-middleware.js';
|
||||||
|
|
||||||
|
const LockTools = createClass({
|
||||||
|
displayName : 'LockTools',
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
fetching : false,
|
||||||
|
reviewCount : 0
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount : function() {
|
||||||
|
this.updateReviewCount();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateReviewCount : async function() {
|
||||||
|
const newCount = await request.get('/api/lock/count')
|
||||||
|
.then((res)=>{return res.body?.count || 'Unknown';});
|
||||||
|
if(newCount != this.state.reviewCount){
|
||||||
|
this.setState({
|
||||||
|
reviewCount : newCount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateLockData : function(lock){
|
||||||
|
this.setState({
|
||||||
|
lock : lock
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function() {
|
||||||
|
return <div className='lockTools'>
|
||||||
|
<h2>Lock Count</h2>
|
||||||
|
<p>Number of brews currently locked: {this.state.reviewCount}</p>
|
||||||
|
<button onClick={this.updateReviewCount}>REFRESH</button>
|
||||||
|
<hr />
|
||||||
|
<LockTable title='Locked Brews' text='Total Locked Brews' resultName='lockedDocuments' fetchURL='/api/locks' propertyNames={['shareId', 'title']} loadBrew={this.updateLockData} ></LockTable>
|
||||||
|
<hr />
|
||||||
|
<LockTable title='Brews Awaiting Review' text='Total Reviews Waiting' resultName='reviewDocuments' fetchURL='/api/lock/reviews' propertyNames={['shareId', 'title']} loadBrew={this.updateLockData} ></LockTable>
|
||||||
|
<hr />
|
||||||
|
<LockBrew key={this.state.lock?.key || 0} lock={this.state.lock}></LockBrew>
|
||||||
|
<hr />
|
||||||
|
<div style={{ columns: 2 }}>
|
||||||
|
<LockLookup title='Unlock Brew' fetchURL='/api/unlock' updateFn={this.updateReviewCount}></LockLookup>
|
||||||
|
<LockLookup title='Clear Review Request' fetchURL='/api/lock/review/remove'></LockLookup>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const LockBrew = createClass({
|
||||||
|
displayName : 'LockBrew',
|
||||||
|
getInitialState : function() {
|
||||||
|
// Default values
|
||||||
|
return {
|
||||||
|
brewId : this.props.lock?.shareId || '',
|
||||||
|
code : this.props.lock?.code || 455,
|
||||||
|
editMessage : this.props.lock?.editMessage || '',
|
||||||
|
shareMessage : this.props.lock?.shareMessage || 'This Brew has been locked.',
|
||||||
|
result : {},
|
||||||
|
overwrite : false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange : function(e, varName) {
|
||||||
|
const output = {};
|
||||||
|
output[varName] = e.target.value;
|
||||||
|
this.setState(output);
|
||||||
|
},
|
||||||
|
|
||||||
|
submit : function(e){
|
||||||
|
e.preventDefault();
|
||||||
|
if(!this.state.editMessage) return;
|
||||||
|
const newLock = {
|
||||||
|
overwrite : this.state.overwrite,
|
||||||
|
code : parseInt(this.state.code) || 100,
|
||||||
|
editMessage : this.state.editMessage,
|
||||||
|
shareMessage : this.state.shareMessage,
|
||||||
|
applied : new Date
|
||||||
|
};
|
||||||
|
|
||||||
|
request.post(`/api/lock/${this.state.brewId}`)
|
||||||
|
.send(newLock)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.then((response)=>{
|
||||||
|
this.setState({ result: response.body });
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
this.setState({ result: err.response.body });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderInput : function (name) {
|
||||||
|
return <input type='text' name={name} value={this.state[name]} onChange={(e)=>this.handleChange(e, name)} autoComplete='off' required/>;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderResult : function(){
|
||||||
|
return <>
|
||||||
|
<h3>Result:</h3>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{Object.keys(this.state.result).map((key, idx)=>{
|
||||||
|
return <tr key={`${idx}-row`}>
|
||||||
|
<td key={`${idx}-key`}>{key}</td>
|
||||||
|
<td key={`${idx}-value`}>{this.state.result[key].toString()}
|
||||||
|
</td>
|
||||||
|
</tr>;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>;
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function() {
|
||||||
|
return <div className='lockBrew'>
|
||||||
|
<div className='lockForm'>
|
||||||
|
<h2>Lock Brew</h2>
|
||||||
|
<form onSubmit={this.submit}>
|
||||||
|
<label>
|
||||||
|
ID:
|
||||||
|
{this.renderInput('brewId')}
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
Error Code:
|
||||||
|
{this.renderInput('code')}
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
Private Message:
|
||||||
|
{this.renderInput('editMessage')}
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label>
|
||||||
|
Public Message:
|
||||||
|
{this.renderInput('shareMessage')}
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label className='checkbox'>
|
||||||
|
Overwrite
|
||||||
|
<input name='overwrite' className='checkbox' type='checkbox' value={this.state.overwrite} onClick={()=>{return this.setState((prevState)=>{return { overwrite: !prevState.overwrite };});}} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type='submit' />
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
{this.state.result && this.renderResult()}
|
||||||
|
</div>
|
||||||
|
<div className='lockSuggestions'>
|
||||||
|
<h2>Suggestions</h2>
|
||||||
|
<div className='lockCodes'>
|
||||||
|
<h3>Codes</h3>
|
||||||
|
<ul>
|
||||||
|
<li>455 - Generic Lock</li>
|
||||||
|
<li>456 - Copyright issues</li>
|
||||||
|
<li>457 - Confidential Information Leakage</li>
|
||||||
|
<li>458 - Sensitive Personal Information</li>
|
||||||
|
<li>459 - Defamation or Libel</li>
|
||||||
|
<li>460 - Hate Speech or Discrimination</li>
|
||||||
|
<li>461 - Illegal Activities</li>
|
||||||
|
<li>462 - Malware or Phishing</li>
|
||||||
|
<li>463 - Plagiarism</li>
|
||||||
|
<li>465 - Misrepresentation</li>
|
||||||
|
<li>466 - Inappropriate Content</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className='lockMessages'>
|
||||||
|
<h3>Messages</h3>
|
||||||
|
<ul>
|
||||||
|
<li><b>Private Message:</b> This is the private message that is ONLY displayed to the authors of the locked brew. This message MUST specify exactly what actions must be taken in order to have the brew unlocked.</li>
|
||||||
|
<li><b>Public Message:</b> This is the public message that is displayed to the EVERYONE that attempts to view the locked brew.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const LockTable = createClass({
|
||||||
|
displayName : 'LockTable',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
title : '',
|
||||||
|
text : '',
|
||||||
|
fetchURL : '/api/locks',
|
||||||
|
resultName : '',
|
||||||
|
propertyNames : ['shareId'],
|
||||||
|
loadBrew : ()=>{}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
result : '',
|
||||||
|
error : '',
|
||||||
|
searching : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
lockKey : React.createRef(0),
|
||||||
|
|
||||||
|
clickFn : function (){
|
||||||
|
this.setState({ searching: true, error: null });
|
||||||
|
|
||||||
|
request.get(this.props.fetchURL)
|
||||||
|
.then((res)=>this.setState({ result: res.body }))
|
||||||
|
.catch((err)=>this.setState({ result: err.response.body }))
|
||||||
|
.finally(()=>{
|
||||||
|
this.setState({ searching: false });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateBrewLockData : function (lockData){
|
||||||
|
this.lockKey.current++;
|
||||||
|
const brewData = {
|
||||||
|
key : this.lockKey.current,
|
||||||
|
shareId : lockData.shareId,
|
||||||
|
code : lockData.lock.code,
|
||||||
|
editMessage : lockData.lock.editMessage,
|
||||||
|
shareMessage : lockData.lock.shareMessage
|
||||||
|
};
|
||||||
|
this.props.loadBrew(brewData);
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function () {
|
||||||
|
return <>
|
||||||
|
<div className='brewsAwaitingReview'>
|
||||||
|
<div className='brewBlock'>
|
||||||
|
<h2>{this.props.title}</h2>
|
||||||
|
<button onClick={this.clickFn}>
|
||||||
|
REFRESH
|
||||||
|
<i className={`fas ${!this.state.searching ? 'fa-search' : 'fa-spin fa-spinner'}`} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{this.state.result[this.props.resultName] &&
|
||||||
|
<>
|
||||||
|
<p>{this.props.text}: {this.state.result[this.props.resultName].length}</p>
|
||||||
|
<table className='lockTable'>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{this.props.propertyNames.map((name, idx)=>{
|
||||||
|
return <th key={idx}>{name}</th>;
|
||||||
|
})}
|
||||||
|
<th>clip</th>
|
||||||
|
<th>load</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.state.result[this.props.resultName].map((result, resultIdx)=>{
|
||||||
|
return <tr className='row' key={`${resultIdx}-row`}>
|
||||||
|
{this.props.propertyNames.map((name, nameIdx)=>{
|
||||||
|
return <td key={`${resultIdx}-${nameIdx}`}>
|
||||||
|
{result[name].toString()}
|
||||||
|
</td>;
|
||||||
|
})}
|
||||||
|
<td className='icon' title='Copy ID to Clipboard' onClick={()=>{navigator.clipboard.writeText(result.shareId.toString());}}><i className='fa-regular fa-clipboard'></i></td>
|
||||||
|
<td className='icon' title='View Lock details' onClick={()=>{this.updateBrewLockData(result);}}><i className='fa-regular fa-circle-down'></i></td>
|
||||||
|
</tr>;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const LockLookup = createClass({
|
||||||
|
displayName : 'LockLookup',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
fetchURL : '/api/lookup'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
query : '',
|
||||||
|
result : '',
|
||||||
|
error : '',
|
||||||
|
searching : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange(e){
|
||||||
|
this.setState({ query: e.target.value });
|
||||||
|
},
|
||||||
|
|
||||||
|
clickFn(){
|
||||||
|
this.setState({ searching: true, error: null });
|
||||||
|
|
||||||
|
request.put(`${this.props.fetchURL}/${this.state.query}`)
|
||||||
|
.then((res)=>this.setState({ result: res.body }))
|
||||||
|
.catch((err)=>this.setState({ result: err.response.body }))
|
||||||
|
.finally(()=>{
|
||||||
|
this.setState({ searching: false });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderResult : function(){
|
||||||
|
return <div className='lockLookup'>
|
||||||
|
<h3>Result:</h3>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{Object.keys(this.state.result).map((key, idx)=>{
|
||||||
|
return <tr key={`${idx}-row`}>
|
||||||
|
<td key={`${idx}-key`}>{key}</td>
|
||||||
|
<td key={`${idx}-value`}>{this.state.result[key].toString()}
|
||||||
|
</td>
|
||||||
|
</tr>;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function() {
|
||||||
|
return <div className='brewLookup'>
|
||||||
|
<h2>{this.props.title}</h2>
|
||||||
|
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='share id' />
|
||||||
|
<button onClick={this.clickFn}>
|
||||||
|
<i className={`fas ${!this.state.searching ? 'fa-search' : 'fa-spin fa-spinner'}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{this.state.error
|
||||||
|
&& <div className='error'>{this.state.error.toString()}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{this.state.result && this.renderResult()}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = LockTools;
|
||||||
66
client/admin/lockTools/lockTools.less
Normal file
66
client/admin/lockTools/lockTools.less
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
.lockTools {
|
||||||
|
.lockBrew {
|
||||||
|
columns : 2;
|
||||||
|
|
||||||
|
.lockForm {
|
||||||
|
break-inside : avoid;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
|
line-height : 2.25em;
|
||||||
|
text-align : right;
|
||||||
|
input {
|
||||||
|
float : right;
|
||||||
|
width : 65%;
|
||||||
|
margin-left : 10px;
|
||||||
|
}
|
||||||
|
&.checkbox {
|
||||||
|
line-height: 1.5em;
|
||||||
|
input {
|
||||||
|
width : 1.5em;
|
||||||
|
height : 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lockSuggestions {
|
||||||
|
line-height : 1.2em;
|
||||||
|
break-inside : avoid;
|
||||||
|
columns : 2;
|
||||||
|
h2 { column-span : all; }
|
||||||
|
h3 { margin-top : 0px; }
|
||||||
|
b { font-weight : 600; }
|
||||||
|
|
||||||
|
.lockCodes { break-inside : avoid; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lockTable {
|
||||||
|
cursor : default;
|
||||||
|
break-inside : avoid;
|
||||||
|
.row:hover {
|
||||||
|
color : #000000;
|
||||||
|
background-color : #CCCCCC;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
cursor : pointer;
|
||||||
|
&:hover { text-shadow : 0px 0px 6px black; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding : 4px 10px;
|
||||||
|
text-align : center;
|
||||||
|
}
|
||||||
|
table, td { border : 1px solid #333333; }
|
||||||
|
|
||||||
|
.brewLookup {
|
||||||
|
min-height : 175px;
|
||||||
|
break-inside : avoid;
|
||||||
|
h2 { margin-top : 0px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
button i { padding-left : 5px; }
|
||||||
|
}
|
||||||
@@ -6,31 +6,32 @@
|
|||||||
|
|
||||||
.field {
|
.field {
|
||||||
display : grid;
|
display : grid;
|
||||||
grid-template-columns : 120px 150px;
|
grid-template-columns : 120px 200px;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
justify-items : stretch;
|
justify-items : stretch;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
margin-bottom : 20px;
|
margin-bottom : 20px;
|
||||||
|
|
||||||
|
|
||||||
input {
|
input {
|
||||||
height : 33px;
|
height : 33px;
|
||||||
padding : 0px 10px;
|
padding : 0px 10px;
|
||||||
margin-bottom : unset;
|
margin-bottom : unset;
|
||||||
font-family : monospace;
|
font-family : monospace;
|
||||||
|
|
||||||
|
&[type='date'] { width : 14ch; }
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width : 50ch;
|
width : 50ch;
|
||||||
min-height : 7em;
|
min-height : 7em;
|
||||||
max-height : 20em;
|
max-height : 20em;
|
||||||
resize : vertical;
|
|
||||||
padding : 10px;
|
padding : 10px;
|
||||||
|
resize : vertical;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
width: 200px;
|
width : 200px;
|
||||||
|
|
||||||
i { margin-right : 10px; }
|
i { margin-right : 10px; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
|
||||||
.notificationLookup {
|
.notificationLookup {
|
||||||
width : 450px;
|
width : 450px;
|
||||||
height : fit-content;
|
height : fit-content;
|
||||||
|
|
||||||
|
.noNotification { margin-block : 20px; }
|
||||||
.notificationList {
|
.notificationList {
|
||||||
display : flex;
|
display : flex;
|
||||||
flex-direction : column;
|
flex-direction : column;
|
||||||
@@ -30,11 +30,6 @@
|
|||||||
font-size : 20px;
|
font-size : 20px;
|
||||||
font-weight : 900;
|
font-weight : 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
dl dt{
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.noNotification { margin-block : 20px; }
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,11 @@ import './Anchored.less';
|
|||||||
// **The Anchor Positioning API is not available in Firefox yet**
|
// **The Anchor Positioning API is not available in Firefox yet**
|
||||||
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.
|
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.
|
||||||
|
|
||||||
|
// When Anchor Positioning is added to Firefox, this can also be rewritten using the Popover API-- add the `popover` attribute
|
||||||
|
// to the container div, which will render the container in the *top level* and give it better interactions like
|
||||||
|
// click outside to dismiss. **Do not** add without Anchor, though, because positioning is very limited with the `popover`
|
||||||
|
// attribute.
|
||||||
|
|
||||||
|
|
||||||
const Anchored = ({ children })=>{
|
const Anchored = ({ children })=>{
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
.anchored-box {
|
.anchored-box {
|
||||||
position:absolute;
|
position : absolute;
|
||||||
@supports (inset-block-start: anchor(bottom)){
|
visibility : hidden;
|
||||||
inset-block-start: anchor(bottom);
|
justify-self : anchor-center;
|
||||||
}
|
@supports (inset-block-start: anchor(bottom)) {
|
||||||
justify-self: anchor-center;
|
inset-block-start : anchor(bottom);
|
||||||
visibility: hidden;
|
|
||||||
&.active {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
}
|
||||||
|
&.active { visibility : visible; }
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,7 @@ const Combobox = createClass({
|
|||||||
},
|
},
|
||||||
handleDropdown : function(show){
|
handleDropdown : function(show){
|
||||||
this.setState({
|
this.setState({
|
||||||
|
value : show ? '' : this.props.default,
|
||||||
showDropdown : show,
|
showDropdown : show,
|
||||||
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
||||||
});
|
});
|
||||||
@@ -58,10 +59,10 @@ const Combobox = createClass({
|
|||||||
this.props.onEntry(e);
|
this.props.onEntry(e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSelect : function(e){
|
handleSelect : function(value, data=value){
|
||||||
this.setState({
|
this.setState({
|
||||||
value : e.currentTarget.getAttribute('data-value')
|
value : value
|
||||||
}, ()=>{this.props.onSelect(this.state.value);});
|
}, ()=>{this.props.onSelect(data);});
|
||||||
;
|
;
|
||||||
},
|
},
|
||||||
renderTextInput : function(){
|
renderTextInput : function(){
|
||||||
@@ -78,10 +79,11 @@ const Combobox = createClass({
|
|||||||
if(!e.target.checkValidity()){
|
if(!e.target.checkValidity()){
|
||||||
this.setState({
|
this.setState({
|
||||||
value : this.props.default
|
value : this.props.default
|
||||||
}, ()=>this.props.onEntry(e));
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<i className='fas fa-caret-down'/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -92,11 +94,10 @@ const Combobox = createClass({
|
|||||||
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
||||||
const filteredArrays = filterOn.map((attr)=>{
|
const filteredArrays = filterOn.map((attr)=>{
|
||||||
const children = dropdownChildren.filter((item)=>{
|
const children = dropdownChildren.filter((item)=>{
|
||||||
if(suggestMethod === 'includes'){
|
if(suggestMethod === 'includes')
|
||||||
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
||||||
} else if(suggestMethod === 'startsWith'){
|
if(suggestMethod === 'startsWith')
|
||||||
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return children;
|
return children;
|
||||||
});
|
});
|
||||||
@@ -111,7 +112,7 @@ const Combobox = createClass({
|
|||||||
},
|
},
|
||||||
render : function () {
|
render : function () {
|
||||||
const dropdownChildren = this.state.options.map((child, i)=>{
|
const dropdownChildren = this.state.options.map((child, i)=>{
|
||||||
const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
|
const clone = React.cloneElement(child, { onClick: ()=>this.handleSelect(child.props.value, child.props.data) });
|
||||||
return clone;
|
return clone;
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,50 +1,46 @@
|
|||||||
.dropdown-container {
|
.dropdown-container {
|
||||||
position:relative;
|
position : relative;
|
||||||
input {
|
input { width : 100%; }
|
||||||
width: 100%;
|
.item i {
|
||||||
}
|
position : absolute;
|
||||||
.dropdown-options {
|
right : 10px;
|
||||||
position:absolute;
|
color : black;
|
||||||
background-color: white;
|
}
|
||||||
z-index: 100;
|
.dropdown-options {
|
||||||
width: 100%;
|
position : absolute;
|
||||||
border: 1px solid gray;
|
z-index : 100;
|
||||||
overflow-y: auto;
|
width : 100%;
|
||||||
max-height: 200px;
|
max-height : 200px;
|
||||||
|
overflow-y : auto;
|
||||||
|
background-color : white;
|
||||||
|
border : 1px solid gray;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar { width : 14px; }
|
||||||
width: 14px;
|
&::-webkit-scrollbar-track { background : #FFFFFF; }
|
||||||
}
|
&::-webkit-scrollbar-thumb {
|
||||||
&::-webkit-scrollbar-track {
|
background-color : #949494;
|
||||||
background: #ffffff;
|
border : 3px solid #FFFFFF;
|
||||||
}
|
border-radius : 10px;
|
||||||
&::-webkit-scrollbar-thumb {
|
}
|
||||||
background-color: #949494;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 3px solid #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
position:relative;
|
|
||||||
font-size: 11px;
|
|
||||||
font-family: Open Sans;
|
|
||||||
padding: 5px;
|
|
||||||
cursor: default;
|
|
||||||
margin: 0 3px;
|
|
||||||
//border-bottom: 1px solid darkgray;
|
|
||||||
&:hover {
|
|
||||||
filter: brightness(120%);
|
|
||||||
background-color: rgb(163, 163, 163);
|
|
||||||
}
|
|
||||||
.detail {
|
|
||||||
width:100%;
|
|
||||||
text-align: left;
|
|
||||||
color: rgb(124, 124, 124);
|
|
||||||
font-style:italic;
|
|
||||||
font-size: 9px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
.item {
|
||||||
|
position : relative;
|
||||||
|
padding : 5px;
|
||||||
|
margin : 0 3px;
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-size : 11px;
|
||||||
|
cursor : default;
|
||||||
|
&:hover {
|
||||||
|
background-color : rgb(163, 163, 163);
|
||||||
|
filter : brightness(120%);
|
||||||
|
}
|
||||||
|
.detail {
|
||||||
|
width : 100%;
|
||||||
|
font-size : 9px;
|
||||||
|
font-style : italic;
|
||||||
|
color : rgb(124, 124, 124);
|
||||||
|
text-align : left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ function Dialog({ dismisskeys = [], closeText = 'Close', blocking = false, ...re
|
|||||||
const dialogRef = useRef(null);
|
const dialogRef = useRef(null);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(dismisskeys.length !== 0) {
|
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||||
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
}, []);
|
||||||
}
|
|
||||||
}, [dialogRef.current, dismisskeys]);
|
|
||||||
|
|
||||||
const dismiss = ()=>{
|
const dismiss = ()=>{
|
||||||
dismisskeys.forEach((key)=>{
|
dismisskeys.forEach((key)=>{
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ require('./splitPane.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useEffect } = React;
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
const storageKey = 'naturalcrit-pane-split';
|
const PANE_WIDTH_KEY = 'HB_editor_splitWidth';
|
||||||
|
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
|
||||||
|
|
||||||
const SplitPane = (props)=>{
|
const SplitPane = (props)=>{
|
||||||
const {
|
const {
|
||||||
@@ -18,9 +19,9 @@ const SplitPane = (props)=>{
|
|||||||
const [liveScroll, setLiveScroll] = useState(false);
|
const [liveScroll, setLiveScroll] = useState(false);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
const savedPos = window.localStorage.getItem(storageKey);
|
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
|
||||||
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
|
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
|
||||||
setLiveScroll(window.localStorage.getItem('liveScroll') === 'true');
|
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
return ()=>window.removeEventListener('resize', handleResize);
|
return ()=>window.removeEventListener('resize', handleResize);
|
||||||
@@ -29,13 +30,13 @@ const SplitPane = (props)=>{
|
|||||||
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
|
||||||
|
|
||||||
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
|
||||||
const handleResize = () =>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
|
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
|
||||||
|
|
||||||
const handleUp =(e)=>{
|
const handleUp =(e)=>{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if(isDragging) {
|
if(isDragging) {
|
||||||
onDragFinish(dividerPos);
|
onDragFinish(dividerPos);
|
||||||
window.localStorage.setItem(storageKey, dividerPos);
|
window.localStorage.setItem(PANE_WIDTH_KEY, dividerPos);
|
||||||
}
|
}
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
};
|
};
|
||||||
@@ -52,7 +53,7 @@ const SplitPane = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const liveScrollToggle = ()=>{
|
const liveScrollToggle = ()=>{
|
||||||
window.localStorage.setItem('liveScroll', String(!liveScroll));
|
window.localStorage.setItem(LIVE_SCROLL_KEY, String(!liveScroll));
|
||||||
setLiveScroll(!liveScroll);
|
setLiveScroll(!liveScroll);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
background-color : #BBBBBB;
|
background-color : #BBBBBB;
|
||||||
.dots {
|
.dots {
|
||||||
display : table-cell;
|
display : table-cell;
|
||||||
text-align : center;
|
|
||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
|
text-align : center;
|
||||||
i {
|
i {
|
||||||
display : block !important;
|
display : block !important;
|
||||||
margin : 10px 0px;
|
margin : 10px 0px;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./brewRenderer.less');
|
require('./brewRenderer.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useRef, useCallback, useMemo } = React;
|
const { useState, useRef, useMemo, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
@@ -16,13 +16,18 @@ const Frame = require('react-frame-component').default;
|
|||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
|
import HeaderNav from './headerNav/headerNav.jsx';
|
||||||
import { safeHTML } from './safeHTML.js';
|
import { safeHTML } from './safeHTML.js';
|
||||||
|
|
||||||
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
|
const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
|
||||||
|
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
|
|
||||||
|
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
|
||||||
|
|
||||||
const INITIAL_CONTENT = dedent`
|
const INITIAL_CONTENT = dedent`
|
||||||
<!DOCTYPE html><html><head>
|
<!DOCTYPE html><html><head>
|
||||||
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" 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="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
|
||||||
<base target=_blank>
|
<base target=_blank>
|
||||||
@@ -36,8 +41,46 @@ const BrewPage = (props)=>{
|
|||||||
index : 0,
|
index : 0,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
const pageRef = useRef(null);
|
||||||
const cleanText = safeHTML(props.contents);
|
const cleanText = safeHTML(props.contents);
|
||||||
return <div className={props.className} id={`p${props.index + 1}`} style={props.style}>
|
|
||||||
|
useEffect(()=>{
|
||||||
|
if(!pageRef.current) return;
|
||||||
|
|
||||||
|
// Observer for tracking pages within the `.pages` div
|
||||||
|
const visibleObserver = new IntersectionObserver(
|
||||||
|
(entries)=>{
|
||||||
|
entries.forEach((entry)=>{
|
||||||
|
if(entry.isIntersecting)
|
||||||
|
props.onVisibilityChange(props.index + 1, true, false); // add page to array of visible pages.
|
||||||
|
else
|
||||||
|
props.onVisibilityChange(props.index + 1, false, false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: .3, rootMargin: '0px 0px 0px 0px' } // detect when >30% of page is within bounds.
|
||||||
|
);
|
||||||
|
|
||||||
|
// Observer for tracking the page at the center of the iframe.
|
||||||
|
const centerObserver = new IntersectionObserver(
|
||||||
|
(entries)=>{
|
||||||
|
entries.forEach((entry)=>{
|
||||||
|
if(entry.isIntersecting)
|
||||||
|
props.onVisibilityChange(props.index + 1, true, true); // Set this page as the center page
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ threshold: 0, rootMargin: '-50% 0px -50% 0px' } // Detect when the page is at the center
|
||||||
|
);
|
||||||
|
|
||||||
|
// attach observers to each `.page`
|
||||||
|
visibleObserver.observe(pageRef.current);
|
||||||
|
centerObserver.observe(pageRef.current);
|
||||||
|
return ()=>{
|
||||||
|
visibleObserver.disconnect();
|
||||||
|
centerObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div className={props.className} id={`p${props.index + 1}`} data-index={props.index} ref={pageRef} style={props.style} {...props.attributes}>
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
@@ -64,53 +107,55 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
visibility : 'hidden'
|
visibility : 'hidden',
|
||||||
|
visiblePages : [],
|
||||||
|
centerPage : 1
|
||||||
});
|
});
|
||||||
|
|
||||||
const [displayOptions, setDisplayOptions] = useState({
|
const [displayOptions, setDisplayOptions] = useState({
|
||||||
zoomLevel : 100,
|
zoomLevel : 100,
|
||||||
spread : 'single',
|
spread : 'single',
|
||||||
startOnRight : true,
|
startOnRight : true,
|
||||||
pageShadows : true
|
pageShadows : true,
|
||||||
|
rowGap : 5,
|
||||||
|
columnGap : 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//useEffect to store or gather toolbar state from storage
|
||||||
|
useEffect(()=>{
|
||||||
|
const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY));
|
||||||
|
toolbarState && setDisplayOptions(toolbarState);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [headerState, setHeaderState] = useState(false);
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
|
const pagesRef = useRef(null);
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
rawPages = props.text.split('\\page');
|
rawPages = props.text.split(PAGEBREAK_REGEX_LEGACY);
|
||||||
} else {
|
} else {
|
||||||
rawPages = props.text.split(/^\\page$/gm);
|
rawPages = props.text.split(PAGEBREAK_REGEX_V3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToHash = (hash)=>{
|
const handlePageVisibilityChange = (pageNum, isVisible, isCenter)=>{
|
||||||
if(!hash) return;
|
setState((prevState)=>{
|
||||||
|
const updatedVisiblePages = new Set(prevState.visiblePages);
|
||||||
|
if(!isCenter)
|
||||||
|
isVisible ? updatedVisiblePages.add(pageNum) : updatedVisiblePages.delete(pageNum);
|
||||||
|
|
||||||
const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
|
return {
|
||||||
let anchor = iframeDoc.querySelector(hash);
|
...prevState,
|
||||||
|
visiblePages : [...updatedVisiblePages].sort((a, b)=>a - b),
|
||||||
|
centerPage : isCenter ? pageNum : prevState.centerPage
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if(anchor) {
|
if(isCenter)
|
||||||
anchor.scrollIntoView({ behavior: 'smooth' });
|
props.onPageChange(pageNum);
|
||||||
} else {
|
|
||||||
// Use MutationObserver to wait for the element if it's not immediately available
|
|
||||||
new MutationObserver((mutations, obs)=>{
|
|
||||||
anchor = iframeDoc.querySelector(hash);
|
|
||||||
if(anchor) {
|
|
||||||
anchor.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
obs.disconnect();
|
|
||||||
}
|
|
||||||
}).observe(iframeDoc, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
|
||||||
const { scrollTop, clientHeight, scrollHeight } = e.target;
|
|
||||||
const totalScrollableHeight = scrollHeight - clientHeight;
|
|
||||||
const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
|
|
||||||
|
|
||||||
props.onPageChange(currentPageNumber);
|
|
||||||
}, 200), []);
|
|
||||||
|
|
||||||
const isInView = (index)=>{
|
const isInView = (index)=>{
|
||||||
if(!state.isMounted)
|
if(!state.isMounted)
|
||||||
return false;
|
return false;
|
||||||
@@ -137,19 +182,38 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderPage = (pageText, index)=>{
|
const renderPage = (pageText, index)=>{
|
||||||
|
|
||||||
|
let styles = {
|
||||||
|
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
|
||||||
|
// Add more conditions as needed
|
||||||
|
};
|
||||||
|
let classes = 'page';
|
||||||
|
let attributes = {};
|
||||||
|
|
||||||
if(props.renderer == 'legacy') {
|
if(props.renderer == 'legacy') {
|
||||||
|
pageText.replace(COLUMNBREAK_REGEX_LEGACY, '```\n````\n'); // Allow Legacy brews to use `\column(break)`
|
||||||
const html = MarkdownLegacy.render(pageText);
|
const html = MarkdownLegacy.render(pageText);
|
||||||
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
|
|
||||||
|
return <BrewPage className='page phb' index={index} key={index} contents={html} style={styles} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
} else {
|
} else {
|
||||||
|
if(pageText.startsWith('\\page')) {
|
||||||
|
const firstLineTokens = Markdown.marked.lexer(pageText.split('\n', 1)[0])[0].tokens;
|
||||||
|
const injectedTags = firstLineTokens?.find((obj)=>obj.injectedTags !== undefined)?.injectedTags;
|
||||||
|
if(injectedTags) {
|
||||||
|
styles = { ...styles, ...injectedTags.styles };
|
||||||
|
styles = _.mapKeys(styles, (v, k)=>k.startsWith('--') ? k : _.camelCase(k)); // Convert CSS to camelCase for React
|
||||||
|
classes = [classes, injectedTags.classes].join(' ').trim();
|
||||||
|
attributes = injectedTags.attributes;
|
||||||
|
}
|
||||||
|
pageText = pageText.includes('\n') ? pageText.substring(pageText.indexOf('\n') + 1) : ''; // Remove the \page line
|
||||||
|
}
|
||||||
|
|
||||||
|
// DO NOT REMOVE!!! REQUIRED FOR BACKWARDS COMPATIBILITY WITH NON-UPGRADABLE VERSIONS OF CHROME.
|
||||||
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)
|
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)
|
||||||
|
|
||||||
const html = Markdown.render(pageText, index);
|
const html = Markdown.render(pageText, index);
|
||||||
|
|
||||||
const styles = {
|
return <BrewPage className={classes} index={index} key={index} contents={html} style={styles} attributes={attributes} onVisibilityChange={handlePageVisibilityChange} />;
|
||||||
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
|
|
||||||
// Add more conditions as needed
|
|
||||||
};
|
|
||||||
|
|
||||||
return <BrewPage className='page' index={index} key={index} contents={html} style={styles} />;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -182,6 +246,26 @@ const BrewRenderer = (props)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const scrollToHash = (hash)=>{
|
||||||
|
if(!hash) return;
|
||||||
|
|
||||||
|
const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
|
||||||
|
let anchor = iframeDoc.querySelector(hash);
|
||||||
|
|
||||||
|
if(anchor) {
|
||||||
|
anchor.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
} else {
|
||||||
|
// Use MutationObserver to wait for the element if it's not immediately available
|
||||||
|
new MutationObserver((mutations, obs)=>{
|
||||||
|
anchor = iframeDoc.querySelector(hash);
|
||||||
|
if(anchor) {
|
||||||
|
anchor.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
obs.disconnect();
|
||||||
|
}
|
||||||
|
}).observe(iframeDoc, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
scrollToHash(window.location.hash);
|
scrollToHash(window.location.hash);
|
||||||
|
|
||||||
@@ -202,6 +286,7 @@ const BrewRenderer = (props)=>{
|
|||||||
|
|
||||||
const handleDisplayOptionsChange = (newDisplayOptions)=>{
|
const handleDisplayOptionsChange = (newDisplayOptions)=>{
|
||||||
setDisplayOptions(newDisplayOptions);
|
setDisplayOptions(newDisplayOptions);
|
||||||
|
localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions));
|
||||||
};
|
};
|
||||||
|
|
||||||
const pagesStyle = {
|
const pagesStyle = {
|
||||||
@@ -217,13 +302,13 @@ const BrewRenderer = (props)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
||||||
renderedPages = useMemo(()=>renderPages(), [displayOptions.pageShadows, props.text]);
|
renderedPages = useMemo(()=>renderPages(), [props.text, displayOptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*render dummy page while iFrame is mounting.*/}
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
{!state.isMounted
|
{!state.isMounted
|
||||||
? <div className='brewRenderer' onScroll={updateCurrentPage}>
|
? <div className='brewRenderer'>
|
||||||
<div className='pages'>
|
<div className='pages'>
|
||||||
{renderDummyPage(1)}
|
{renderDummyPage(1)}
|
||||||
</div>
|
</div>
|
||||||
@@ -236,7 +321,7 @@ const BrewRenderer = (props)=>{
|
|||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolBar displayOptions={displayOptions} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length} onDisplayOptionsChange={handleDisplayOptionsChange} />
|
<ToolBar displayOptions={displayOptions} onDisplayOptionsChange={handleDisplayOptionsChange} visiblePages={state.visiblePages.length > 0 ? state.visiblePages : [state.centerPage]} totalPages={rawPages.length} headerState={headerState} setHeaderState={setHeaderState}/>
|
||||||
|
|
||||||
{/*render in iFrame so broken code doesn't crash the site.*/}
|
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||||
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||||
@@ -245,23 +330,23 @@ const BrewRenderer = (props)=>{
|
|||||||
onClick={()=>{emitClick();}}
|
onClick={()=>{emitClick();}}
|
||||||
>
|
>
|
||||||
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
||||||
onScroll={updateCurrentPage}
|
|
||||||
onKeyDown={handleControlKeys}
|
onKeyDown={handleControlKeys}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
style={ styleObject }>
|
style={ styleObject }
|
||||||
|
>
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderedStyle}
|
{renderedStyle}
|
||||||
<div lang={`${props.lang || 'en'}`} style={pagesStyle} className={
|
<div className={`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}`} lang={`${props.lang || 'en'}`} style={pagesStyle} ref={pagesRef}>
|
||||||
`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}` } >
|
|
||||||
{renderedPages}
|
{renderedPages}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{headerState ? <HeaderNav ref={pagesRef} /> : <></>}
|
||||||
</Frame>
|
</Frame>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,43 +1,39 @@
|
|||||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||||
|
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
|
height : 100vh;
|
||||||
|
padding-top : 60px;
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
padding-top : 60px;
|
&:has(.facing, .flow) { padding : 60px 30px; }
|
||||||
height : 100vh;
|
&.deployment { background-color : darkred; }
|
||||||
&:has(.facing, .flow) {
|
|
||||||
padding : 60px 30px;
|
|
||||||
}
|
|
||||||
&.deployment {
|
|
||||||
background-color: darkred;
|
|
||||||
}
|
|
||||||
:where(.pages) {
|
:where(.pages) {
|
||||||
&.facing {
|
&.facing {
|
||||||
display: grid;
|
display : grid;
|
||||||
grid-template-columns: repeat(2, auto);
|
grid-template-rows : repeat(3, auto);
|
||||||
grid-template-rows: repeat(3, auto);
|
grid-template-columns : repeat(2, auto);
|
||||||
gap: 10px 10px;
|
gap : 10px 10px;
|
||||||
justify-content: center;
|
justify-content : safe center;
|
||||||
&.recto .page:first-child {
|
&.recto .page:first-child {
|
||||||
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
|
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
|
||||||
// todo: add a checkbox to toggle this setting
|
// todo: add a checkbox to toggle this setting
|
||||||
grid-column-start: 2;
|
grid-column-start : 2;
|
||||||
}
|
}
|
||||||
& :where(.page) {
|
& :where(.page) {
|
||||||
margin-left: unset !important;
|
margin-right : unset !important;
|
||||||
margin-right: unset !important;
|
margin-left : unset !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.flow {
|
&.flow {
|
||||||
display: flex;
|
display : flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap : wrap;
|
||||||
gap: 10px;
|
gap : 10px;
|
||||||
justify-content: flex-start;
|
justify-content : safe center;
|
||||||
& :where(.page) {
|
& :where(.page) {
|
||||||
flex: 0 0 auto;
|
flex : 0 0 auto;
|
||||||
margin-left: unset !important;
|
margin-right : unset !important;
|
||||||
margin-right: unset !important;
|
margin-left : unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -50,9 +46,7 @@
|
|||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
box-shadow : 1px 4px 14px #000000;
|
box-shadow : 1px 4px 14px #000000;
|
||||||
}
|
}
|
||||||
*[id] {
|
*[id] { scroll-margin-top : 100px; }
|
||||||
scroll-margin-top:100px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width : 20px;
|
width : 20px;
|
||||||
@@ -70,16 +64,22 @@
|
|||||||
|
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
|
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.toolBar { display : none; }
|
.toolBar { display : none; }
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
padding-top : unset;
|
padding : unset;
|
||||||
overflow-y : unset;
|
overflow-y : unset;
|
||||||
|
&:has(.facing, .flow) {
|
||||||
|
padding : unset;
|
||||||
|
}
|
||||||
.pages {
|
.pages {
|
||||||
margin : 0px;
|
margin : 0px;
|
||||||
zoom: 100% !important;
|
zoom : 100% !important;
|
||||||
|
display : block;
|
||||||
& > .page { box-shadow : unset; }
|
& > .page { box-shadow : unset; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.headerNav { visibility : hidden; }
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,53 @@
|
|||||||
require('./errorBar.less');
|
require('./errorBar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const ErrorBar = createClass({
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
displayName : 'ErrorBar',
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
errors : []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
hasOpenError : false,
|
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||||
hasCloseError : false,
|
|
||||||
hasMatchError : false,
|
|
||||||
|
|
||||||
renderErrors : function(){
|
const ErrorBar = (props)=>{
|
||||||
this.hasOpenError = false;
|
if(!props.errors.length) return null;
|
||||||
this.hasCloseError = false;
|
let hasOpenError = false, hasCloseError = false, hasMatchError = false;
|
||||||
this.hasMatchError = false;
|
|
||||||
|
|
||||||
|
props.errors.map((err)=>{
|
||||||
|
if(err.id === 'OPEN') hasOpenError = true;
|
||||||
|
if(err.id === 'CLOSE') hasCloseError = true;
|
||||||
|
if(err.id === 'MISMATCH') hasMatchError = true;
|
||||||
|
});
|
||||||
|
|
||||||
const errors = _.map(this.props.errors, (err, idx)=>{
|
const renderErrors = ()=>(
|
||||||
if(err.id == 'OPEN') this.hasOpenError = true;
|
<ul>
|
||||||
if(err.id == 'CLOSE') this.hasCloseError = true;
|
{props.errors.map((err, idx)=>{
|
||||||
if(err.id == 'MISMATCH') this.hasMatchError = true;
|
return <li key={idx}>
|
||||||
return <li key={idx}>
|
Line {err.line} : {err.text}, '{err.type}' tag
|
||||||
Line {err.line} : {err.text}, '{err.type}' tag
|
</li>;
|
||||||
</li>;
|
})}
|
||||||
});
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
return <ul>{errors}</ul>;
|
const renderProtip = ()=>(
|
||||||
},
|
<div className='protips'>
|
||||||
|
|
||||||
renderProtip : function(){
|
|
||||||
const msg = [];
|
|
||||||
if(this.hasOpenError){
|
|
||||||
msg.push(<div>
|
|
||||||
An unmatched opening tag means there's an opened tag that isn't closed. You need to close your tags, like this {'</div>'}. Make sure to match types!
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasCloseError){
|
|
||||||
msg.push(<div>
|
|
||||||
An unmatched closing tag means you closed a tag without opening it. Either remove it, or check to where you think you opened it.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasMatchError){
|
|
||||||
msg.push(<div>
|
|
||||||
A type mismatch means you closed a tag, but the last open tag was a different type.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
return <div className='protips'>
|
|
||||||
<h4>Protips!</h4>
|
<h4>Protips!</h4>
|
||||||
{msg}
|
{hasOpenError && <div>Unmatched opening tag. Close your tags, like this {'</div>'}. Match types!</div>}
|
||||||
</div>;
|
{hasCloseError && <div>Unmatched closing tag. Either remove it or check where it was opened.</div>}
|
||||||
},
|
{hasMatchError && <div>Type mismatch. Closed a tag with a different type.</div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
if(!this.props.errors.length) return null;
|
<Dialog className='errorBar' closeText={DISMISS_BUTTON} >
|
||||||
|
<div>
|
||||||
return <div className='errorBar'>
|
<i className='fas fa-exclamation-triangle' />
|
||||||
<i className='fas fa-exclamation-triangle' />
|
<h2> There are HTML errors in your markup</h2>
|
||||||
<h3> There are HTML errors in your markup</h3>
|
<small>
|
||||||
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
If these aren't fixed your brew will not render properly when you print it to PDF or share it
|
||||||
{this.renderErrors()}
|
</small>
|
||||||
|
{renderErrors()}
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
{this.renderProtip()}
|
{renderProtip()}
|
||||||
</div>;
|
</Dialog>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = ErrorBar;
|
module.exports = ErrorBar;
|
||||||
|
|||||||
@@ -1,60 +1,58 @@
|
|||||||
|
|
||||||
.errorBar{
|
.errorBar {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 10000;
|
top : 32px;
|
||||||
box-sizing : border-box;
|
z-index : 1;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
margin-right : 13px;
|
|
||||||
padding : 20px;
|
|
||||||
padding-bottom : 10px;
|
|
||||||
padding-left : 100px;
|
|
||||||
background-color : @red;
|
|
||||||
color : white;
|
color : white;
|
||||||
i{
|
background-color : @red;
|
||||||
position : absolute;
|
border : unset;
|
||||||
left : 30px;
|
|
||||||
opacity : 0.8;
|
div {
|
||||||
font-size : 3em;
|
> i {
|
||||||
}
|
float : left;
|
||||||
h3{
|
margin-right : 10px;
|
||||||
font-size : 1.1em;
|
margin-bottom : 20px;
|
||||||
font-weight : 800;
|
font-size : 3em;
|
||||||
}
|
opacity : 0.8;
|
||||||
ul{
|
}
|
||||||
margin-top : 15px;
|
h2 { font-weight : 800; }
|
||||||
font-size : 0.8em;
|
ul {
|
||||||
list-style-position : inside;
|
margin-top : 15px;
|
||||||
list-style-type : disc;
|
font-size : 0.8em;
|
||||||
li{
|
list-style-position : inside;
|
||||||
line-height : 1.6em;
|
list-style-type : disc;
|
||||||
|
li { line-height : 1.6em; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
hr{
|
hr {
|
||||||
box-sizing : border-box;
|
|
||||||
height : 2px;
|
height : 2px;
|
||||||
width : 150%;
|
|
||||||
margin-top : 25px;
|
margin-top : 25px;
|
||||||
margin-bottom : 15px;
|
margin-bottom : 15px;
|
||||||
margin-left : -100px;
|
|
||||||
background-color : darken(@red, 8%);
|
background-color : darken(@red, 8%);
|
||||||
border : none;
|
border : none;
|
||||||
}
|
}
|
||||||
small{
|
small {
|
||||||
font-size: 0.6em;
|
font-size : 0.6em;
|
||||||
opacity: 0.7;
|
opacity : 0.7;
|
||||||
}
|
}
|
||||||
.protips{
|
.protips {
|
||||||
margin-left : -80px;
|
font-size : 0.6em;
|
||||||
font-size : 0.6em;
|
line-height : 1.2em;
|
||||||
&>div{
|
h4 {
|
||||||
margin-bottom : 10px;
|
|
||||||
line-height : 1.2em;
|
|
||||||
}
|
|
||||||
h4{
|
|
||||||
opacity : 0.8;
|
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
button.dismiss {
|
||||||
|
position : absolute;
|
||||||
|
top : 20px;
|
||||||
|
right : 30px;
|
||||||
|
padding : unset;
|
||||||
|
font-size : 40px;
|
||||||
|
background-color : transparent;
|
||||||
|
opacity : 0.6;
|
||||||
|
&:hover { opacity : 1; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
113
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal file
113
client/homebrew/brewRenderer/headerNav/headerNav.jsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
require('./headerNav.less');
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
|
||||||
|
const MAX_TEXT_LENGTH = 40;
|
||||||
|
|
||||||
|
const HeaderNav = React.forwardRef(({}, pagesRef)=>{
|
||||||
|
|
||||||
|
const renderHeaderLinks = ()=>{
|
||||||
|
if(!pagesRef.current) return;
|
||||||
|
|
||||||
|
// Top Level Pages
|
||||||
|
// Pages that contain an element with a specified class (e.g. cover pages, table of contents)
|
||||||
|
// will NOT have its content scanned for navigation headers, instead displaying a custom label
|
||||||
|
// ---
|
||||||
|
// The property name is class that will be used for detecting the page is a top level page
|
||||||
|
// The property value is a function that returns the text to be used
|
||||||
|
|
||||||
|
const topLevelPages = {
|
||||||
|
'.frontCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Cover: ${text}` : 'Cover Page'; },
|
||||||
|
'.insideCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Interior: ${text}` : 'Interior Cover Page'; },
|
||||||
|
'.partCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Section: ${text}` : 'Section Cover Page'; },
|
||||||
|
'.backCover' : (el, pageType)=>{ const text = getHeaderContent(el); return text ? `Back: ${text}` : 'Rear Cover Page'; },
|
||||||
|
'.toc' : ()=>{ return 'Table of Contents'; },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeaderContent = (el)=>el.querySelector('h1')?.textContent;
|
||||||
|
|
||||||
|
const topLevelPageSelector = Object.keys(topLevelPages).join(',');
|
||||||
|
|
||||||
|
const selector = [
|
||||||
|
'.pages > .page', // All page elements, which by definition have IDs
|
||||||
|
`.page:not(:has(${topLevelPageSelector})) > [id]`, // All direct children of non-excluded .pages with an ID (Legacy)
|
||||||
|
`.page:not(:has(${topLevelPageSelector})) > .columnWrapper > [id]`, // All direct children of non-excluded .page > .columnWrapper with an ID (V3)
|
||||||
|
`.page:not(:has(${topLevelPageSelector})) h2`, // All non-excluded H2 titles, like Monster frame titles
|
||||||
|
];
|
||||||
|
const elements = pagesRef.current.querySelectorAll(selector.join(','));
|
||||||
|
if(!elements) return;
|
||||||
|
const navList = [];
|
||||||
|
|
||||||
|
// navList is a list of objects which have the following structure:
|
||||||
|
// {
|
||||||
|
// depth : how deeply indented the item should be
|
||||||
|
// text : the text to display in the nav link
|
||||||
|
// link : the hyperlink to navigate to when clicked
|
||||||
|
// className : [optional] the class to apply to the nav link for styling
|
||||||
|
// }
|
||||||
|
|
||||||
|
elements.forEach((el)=>{
|
||||||
|
const navEntry = { // Default structure of a navList entry
|
||||||
|
depth : 7, // All unmatched elements with IDs are set to the maximum depth (7)
|
||||||
|
text : el.textContent, // Use `textContent` because `innerText` is affected by rendering, e.g. 'content-visibility: auto'
|
||||||
|
link : el.id
|
||||||
|
};
|
||||||
|
if(el.classList.contains('page')) {
|
||||||
|
let text = `Page ${el.id.slice(1)}`; // Get the page # by trimming off the 'p' from the ID
|
||||||
|
const pageType = Object.keys(topLevelPages).find((pageType)=>el.querySelector(pageType));
|
||||||
|
if(pageType)
|
||||||
|
text += ` - ${topLevelPages[pageType](el, pageType)}`; // If a Top Level Page, add extra label
|
||||||
|
|
||||||
|
navEntry.depth = 0; // Pages are always at the least indented level
|
||||||
|
navEntry.text = text;
|
||||||
|
navEntry.className = 'pageLink';
|
||||||
|
} else if(el.localName.match(/^h[1-6]/)){ // Header elements H1 through H6
|
||||||
|
navEntry.depth = el.localName[1]; // Depth is set by the header level
|
||||||
|
}
|
||||||
|
navList.push(navEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
return _.map(navList, (navItem, index)=><HeaderNavItem {...navItem} key={index} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <nav className='headerNav'>
|
||||||
|
<ul>
|
||||||
|
{renderHeaderLinks()}
|
||||||
|
</ul>
|
||||||
|
</nav>;
|
||||||
|
});
|
||||||
|
|
||||||
|
const HeaderNavItem = ({ link, text, depth, className })=>{
|
||||||
|
|
||||||
|
const trimString = (text, prefixLength = 0)=>{
|
||||||
|
// Sanity check nav link strings
|
||||||
|
let output = text;
|
||||||
|
|
||||||
|
// If the string has a line break, only use the first line
|
||||||
|
if(text.indexOf('\n')){
|
||||||
|
output = text.split('\n')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim unecessary spaces from string
|
||||||
|
output = output.trim();
|
||||||
|
|
||||||
|
// Reduce excessively long strings
|
||||||
|
const maxLength = MAX_TEXT_LENGTH - prefixLength;
|
||||||
|
if(output.length > maxLength){
|
||||||
|
return `${output.slice(0, maxLength).trim()}...`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
if(!link || !text) return;
|
||||||
|
|
||||||
|
return <li>
|
||||||
|
<a href={`#${link}`} target='_self' className={`depth-${depth} ${className ?? ''}`}>
|
||||||
|
{trimString(text, depth)}
|
||||||
|
</a>
|
||||||
|
</li>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeaderNav;
|
||||||
39
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal file
39
client/homebrew/brewRenderer/headerNav/headerNav.less
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.headerNav {
|
||||||
|
position : fixed;
|
||||||
|
top : 32px;
|
||||||
|
left : 0px;
|
||||||
|
max-width : 40vw;
|
||||||
|
max-height : calc(100vh - 32px);
|
||||||
|
padding : 5px 10px;
|
||||||
|
overflow-y : auto;
|
||||||
|
background-color : #CCCCCC;
|
||||||
|
border-radius : 5px;
|
||||||
|
&.active {
|
||||||
|
padding-bottom : 10px;
|
||||||
|
.navIcon { padding-bottom : 10px; }
|
||||||
|
}
|
||||||
|
.navIcon { cursor : pointer; }
|
||||||
|
li {
|
||||||
|
list-style-type : none;
|
||||||
|
a {
|
||||||
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
|
padding : 2px;
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-size : 12px;
|
||||||
|
color : inherit;
|
||||||
|
text-decoration : none;
|
||||||
|
cursor : pointer;
|
||||||
|
&:hover { text-decoration : underline; }
|
||||||
|
&.pageLink { font-weight : 900; }
|
||||||
|
|
||||||
|
@depths: 0,1,2,3,4,5,6,7;
|
||||||
|
|
||||||
|
each(@depths, {
|
||||||
|
&.depth-@{value} {
|
||||||
|
padding-left: ((@value) * 0.5em);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
require('./notificationPopup.less');
|
require('./notificationPopup.less');
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
import Dialog from '../../../components/dialog.jsx';
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
|
|
||||||
@@ -40,15 +41,15 @@ const NotificationPopup = ()=>{
|
|||||||
|
|
||||||
const renderNotificationsList = ()=>{
|
const renderNotificationsList = ()=>{
|
||||||
if(error) return <div className='error'>{error}</div>;
|
if(error) return <div className='error'>{error}</div>;
|
||||||
|
|
||||||
return notifications.map((notification)=>(
|
return notifications.map((notification)=>(
|
||||||
<li key={notification.dismissKey} >
|
<li key={notification.dismissKey} >
|
||||||
<em>{notification.title}</em><br />
|
<em>{notification.title}</em><br />
|
||||||
<p dangerouslySetInnerHTML={{ __html: notification.text }}></p>
|
<p dangerouslySetInnerHTML={{ __html: Markdown.render(notification.text) }}></p>
|
||||||
</li>
|
</li>
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if(!notifications.length) return;
|
||||||
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
||||||
<div className='header'>
|
<div className='header'>
|
||||||
<i className='fas fa-info-circle info'></i>
|
<i className='fas fa-info-circle info'></i>
|
||||||
|
|||||||
@@ -48,17 +48,46 @@
|
|||||||
}
|
}
|
||||||
ul {
|
ul {
|
||||||
margin-top : 15px;
|
margin-top : 15px;
|
||||||
font-size : 0.8em;
|
font-size : 0.9em;
|
||||||
list-style-position : outside;
|
list-style-position : outside;
|
||||||
list-style-type : disc;
|
list-style-type : disc;
|
||||||
li {
|
li {
|
||||||
margin-top : 1.4em;
|
padding-left : 1em;
|
||||||
font-size : 0.8em;
|
margin-top : 1.5em;
|
||||||
line-height : 1.4em;
|
font-size : 0.9em;
|
||||||
em {
|
line-height : 1.5em;
|
||||||
text-transform:capitalize;
|
em {
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
|
text-transform : capitalize;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin-top : 0;
|
||||||
|
line-height : 1.2em;
|
||||||
|
list-style-type : square;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ul ul,ol ol,ul ol,ol ul {
|
||||||
|
margin-bottom : 0px;
|
||||||
|
margin-left : 1.5em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/* Markdown styling */
|
||||||
|
code {
|
||||||
|
padding : 0.1em 0.5em;
|
||||||
|
font-family : 'Courier New', 'Courier', monospace;
|
||||||
|
overflow-wrap : break-word;
|
||||||
|
white-space : pre-wrap;
|
||||||
|
background : #08115A;
|
||||||
|
border-radius : 2px;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
|
}
|
||||||
|
.blank {
|
||||||
|
height : 1em;
|
||||||
|
margin-top : 0;
|
||||||
|
& + * { margin-top : 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,38 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
require('./toolBar.less');
|
require('./toolBar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useEffect } = React;
|
const { useState, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
|
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
|
||||||
// import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx';
|
|
||||||
|
|
||||||
const MAX_ZOOM = 300;
|
const MAX_ZOOM = 300;
|
||||||
const MIN_ZOOM = 10;
|
const MIN_ZOOM = 10;
|
||||||
|
|
||||||
const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChange })=>{
|
const TOOLBAR_VISIBILITY = 'HB_renderer_toolbarVisibility';
|
||||||
|
|
||||||
const [pageNum, setPageNum] = useState(currentPage);
|
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
|
||||||
|
|
||||||
|
const [pageNum, setPageNum] = useState(1);
|
||||||
const [toolsVisible, setToolsVisible] = useState(true);
|
const [toolsVisible, setToolsVisible] = useState(true);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
setPageNum(currentPage);
|
// format multiple visible pages as a range (e.g. "150-153")
|
||||||
}, [currentPage]);
|
const pageRange = visiblePages.length === 1 ? `${visiblePages[0]}` : `${visiblePages[0]} - ${visiblePages.at(-1)}`;
|
||||||
|
setPageNum(pageRange);
|
||||||
|
}, [visiblePages]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const Visibility = localStorage.getItem(TOOLBAR_VISIBILITY);
|
||||||
|
if(Visibility) setToolsVisible(Visibility === 'true');
|
||||||
|
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleZoomButton = (zoom)=>{
|
const handleZoomButton = (zoom)=>{
|
||||||
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOptionChange = (optionKey, newValue)=>{
|
const handleOptionChange = (optionKey, newValue)=>{
|
||||||
//setDisplayOptions(prevOptions => ({ ...prevOptions, [optionKey]: newValue }));
|
|
||||||
onDisplayOptionsChange({ ...displayOptions, [optionKey]: newValue });
|
onDisplayOptionsChange({ ...displayOptions, [optionKey]: newValue });
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,16 +41,16 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
|
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// scroll to a page, used in the Prev/Next Page buttons.
|
||||||
const scrollToPage = (pageNumber)=>{
|
const scrollToPage = (pageNumber)=>{
|
||||||
|
if(typeof pageNumber !== 'number') return;
|
||||||
pageNumber = _.clamp(pageNumber, 1, totalPages);
|
pageNumber = _.clamp(pageNumber, 1, totalPages);
|
||||||
const iframe = document.getElementById('BrewRenderer');
|
const iframe = document.getElementById('BrewRenderer');
|
||||||
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
|
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
|
||||||
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
|
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
|
||||||
page?.scrollIntoView({ block: 'start' });
|
page?.scrollIntoView({ block: 'start' });
|
||||||
setPageNum(pageNumber);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const calculateChange = (mode)=>{
|
const calculateChange = (mode)=>{
|
||||||
const iframe = document.getElementById('BrewRenderer');
|
const iframe = document.getElementById('BrewRenderer');
|
||||||
const iframeWidth = iframe.getBoundingClientRect().width;
|
const iframeWidth = iframe.getBoundingClientRect().width;
|
||||||
@@ -54,11 +63,30 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
||||||
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
||||||
|
|
||||||
desiredZoom = (iframeWidth / widestPage) * 100;
|
if(displayOptions.spread === 'facing')
|
||||||
|
desiredZoom = (iframeWidth / ((widestPage * 2) + parseInt(displayOptions.columnGap))) * 100;
|
||||||
|
else
|
||||||
|
desiredZoom = (iframeWidth / (widestPage + 20)) * 100;
|
||||||
|
|
||||||
} else if(mode == 'fit'){
|
} else if(mode == 'fit'){
|
||||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
||||||
const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
let minDimRatio;
|
||||||
|
if(displayOptions.spread === 'single')
|
||||||
|
minDimRatio = [...pages].reduce(
|
||||||
|
(minRatio, page)=>Math.min(minRatio,
|
||||||
|
iframeWidth / page.offsetWidth,
|
||||||
|
iframeHeight / page.offsetHeight
|
||||||
|
),
|
||||||
|
Infinity
|
||||||
|
);
|
||||||
|
else
|
||||||
|
minDimRatio = [...pages].reduce(
|
||||||
|
(minRatio, page)=>Math.min(minRatio,
|
||||||
|
iframeWidth / ((page.offsetWidth * 2) + parseInt(displayOptions.columnGap)),
|
||||||
|
iframeHeight / page.offsetHeight
|
||||||
|
),
|
||||||
|
Infinity
|
||||||
|
);
|
||||||
|
|
||||||
desiredZoom = minDimRatio * 100;
|
desiredZoom = minDimRatio * 100;
|
||||||
}
|
}
|
||||||
@@ -71,7 +99,13 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
||||||
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
<div className='toggleButton'>
|
||||||
|
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
|
||||||
|
setToolsVisible(!toolsVisible);
|
||||||
|
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible);
|
||||||
|
}}><i className='fas fa-glasses' /></button>
|
||||||
|
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
|
||||||
|
</div>
|
||||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
||||||
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
||||||
<button
|
<button
|
||||||
@@ -134,7 +168,7 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
id='single-spread'
|
id='single-spread'
|
||||||
className='tool'
|
className='tool'
|
||||||
title='Single Page'
|
title='Single Page'
|
||||||
onClick={()=>{handleOptionChange('spread', 'active');}}
|
onClick={()=>{handleOptionChange('spread', 'single');}}
|
||||||
aria-checked={displayOptions.spread === 'single'}
|
aria-checked={displayOptions.spread === 'single'}
|
||||||
><i className='fac single-spread' /></button>
|
><i className='fac single-spread' /></button>
|
||||||
<button role='radio'
|
<button role='radio'
|
||||||
@@ -159,11 +193,11 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
<h1>Options</h1>
|
<h1>Options</h1>
|
||||||
<label title='Modify the horizontal space between pages.'>
|
<label title='Modify the horizontal space between pages.'>
|
||||||
Column gap
|
Column gap
|
||||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
<input type='range' min={0} max={200} defaultValue={displayOptions.columnGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label title='Modify the vertical space between rows of pages.'>
|
<label title='Modify the vertical space between rows of pages.'>
|
||||||
Row gap
|
Row gap
|
||||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
<input type='range' min={0} max={200} defaultValue={displayOptions.rowGap || 10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label title='Start 1st page on the right side, such as if you have cover page.'>
|
<label title='Start 1st page on the right side, such as if you have cover page.'>
|
||||||
Start on right
|
Start on right
|
||||||
@@ -185,8 +219,8 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
className='previousPage tool'
|
className='previousPage tool'
|
||||||
type='button'
|
type='button'
|
||||||
title='Previous Page(s)'
|
title='Previous Page(s)'
|
||||||
onClick={()=>scrollToPage(pageNum - 1)}
|
onClick={()=>scrollToPage(_.min(visiblePages) - visiblePages.length)}
|
||||||
disabled={pageNum <= 1}
|
disabled={visiblePages.includes(1)}
|
||||||
>
|
>
|
||||||
<i className='fas fa-arrow-left'></i>
|
<i className='fas fa-arrow-left'></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -205,6 +239,7 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
onChange={(e)=>handlePageInput(e.target.value)}
|
onChange={(e)=>handlePageInput(e.target.value)}
|
||||||
onBlur={()=>scrollToPage(pageNum)}
|
onBlur={()=>scrollToPage(pageNum)}
|
||||||
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
|
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
|
||||||
|
style={{ width: `${pageNum.length}ch` }}
|
||||||
/>
|
/>
|
||||||
<span id='page-count' title='Total Page Count'>/ {totalPages}</span>
|
<span id='page-count' title='Total Page Count'>/ {totalPages}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,8 +249,8 @@ const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChan
|
|||||||
className='tool'
|
className='tool'
|
||||||
type='button'
|
type='button'
|
||||||
title='Next Page(s)'
|
title='Next Page(s)'
|
||||||
onClick={()=>scrollToPage(pageNum + 1)}
|
onClick={()=>scrollToPage(_.max(visiblePages) + 1)}
|
||||||
disabled={pageNum >= totalPages}
|
disabled={visiblePages.includes(totalPages)}
|
||||||
>
|
>
|
||||||
<i className='fas fa-arrow-right'></i>
|
<i className='fas fa-arrow-right'></i>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -6,12 +6,12 @@
|
|||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
display : flex;
|
display : flex;
|
||||||
flex-wrap : wrap;
|
flex-wrap : wrap;
|
||||||
gap : 8px 30px;
|
gap : 8px 20px;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : auto;
|
height : auto;
|
||||||
padding : 2px 0;
|
padding : 2px 10px 2px 90px;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
font-size : 13px;
|
font-size : 13px;
|
||||||
color : #CCCCCC;
|
color : #CCCCCC;
|
||||||
@@ -104,9 +104,9 @@
|
|||||||
height : 1.5em;
|
height : 1.5em;
|
||||||
padding : 2px 5px;
|
padding : 2px 5px;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
color : #000000;
|
color : inherit;
|
||||||
background : #EEEEEE;
|
background : #3B3B3B;
|
||||||
border : 1px solid gray;
|
border : none;
|
||||||
&:focus { outline : 1px solid #D3D3D3; }
|
&:focus { outline : 1px solid #D3D3D3; }
|
||||||
|
|
||||||
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
|
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
|
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
|
||||||
&#page-input {
|
&#page-input {
|
||||||
width : 4ch;
|
min-width : 5ch;
|
||||||
margin-right : 1ch;
|
margin-right : 1ch;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
@@ -153,10 +153,10 @@
|
|||||||
align-items : center;
|
align-items : center;
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
width : auto;
|
width : auto;
|
||||||
min-width : 46px;
|
min-width : 40px;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
&:hover { background-color : #444444; }
|
&:hover { background-color : #444444; }
|
||||||
&:focus { border : 1px solid #D3D3D3;outline : none;}
|
&:focus {outline : none; border : 1px solid #D3D3D3;}
|
||||||
&:disabled {
|
&:disabled {
|
||||||
color : #777777;
|
color : #777777;
|
||||||
background-color : unset !important;
|
background-color : unset !important;
|
||||||
@@ -166,22 +166,26 @@
|
|||||||
|
|
||||||
&.hidden {
|
&.hidden {
|
||||||
flex-wrap : nowrap;
|
flex-wrap : nowrap;
|
||||||
width : 32px;
|
width : 92px;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
background-color : unset;
|
background-color : unset;
|
||||||
opacity : 0.5;
|
opacity : 0.7;
|
||||||
transition : all 0.3s ease;
|
transition : all 0.3s ease;
|
||||||
& > *:not(.toggleButton) {
|
& > *:not(.toggleButton) {
|
||||||
opacity : 0;
|
opacity : 0;
|
||||||
transition : all 0.2s ease;
|
transition : all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleButton button i {
|
||||||
|
filter: drop-shadow(0 0 2px black) drop-shadow(0 0 1px black);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button.toggleButton {
|
.toggleButton {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : 0;
|
left : 0;
|
||||||
z-index : 5;
|
z-index : 5;
|
||||||
width : 32px;
|
display : flex;
|
||||||
min-width : unset;
|
height : 100%;
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,10 @@ 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 EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
|
const EDITOR_THEME_KEY = 'HB_editor_theme';
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
|
||||||
|
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
|
||||||
const DEFAULT_STYLE_TEXT = dedent`
|
const DEFAULT_STYLE_TEXT = dedent`
|
||||||
/*=======--- Example CSS styling ---=======*/
|
/*=======--- Example CSS styling ---=======*/
|
||||||
/* Any CSS here will apply to your document! */
|
/* Any CSS here will apply to your document! */
|
||||||
@@ -21,6 +22,13 @@ const DEFAULT_STYLE_TEXT = dedent`
|
|||||||
color: black;
|
color: black;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
|
const DEFAULT_SNIPPET_TEXT = dedent`
|
||||||
|
\snippet example snippet
|
||||||
|
|
||||||
|
The text between \`\snippet title\` lines will become a snippet of name \`title\` as this example provides.
|
||||||
|
|
||||||
|
This snippet is accessible in the brew tab, and will be inherited if the brew is used as a theme.
|
||||||
|
`;
|
||||||
let isJumping = false;
|
let isJumping = false;
|
||||||
|
|
||||||
const Editor = createClass({
|
const Editor = createClass({
|
||||||
@@ -32,10 +40,8 @@ const Editor = createClass({
|
|||||||
style : ''
|
style : ''
|
||||||
},
|
},
|
||||||
|
|
||||||
onTextChange : ()=>{},
|
onBrewChange : ()=>{},
|
||||||
onStyleChange : ()=>{},
|
reportError : ()=>{},
|
||||||
onMetaChange : ()=>{},
|
|
||||||
reportError : ()=>{},
|
|
||||||
|
|
||||||
onCursorPageChange : ()=>{},
|
onCursorPageChange : ()=>{},
|
||||||
onViewPageChange : ()=>{},
|
onViewPageChange : ()=>{},
|
||||||
@@ -50,8 +56,9 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
editorTheme : this.props.editorTheme,
|
editorTheme : this.props.editorTheme,
|
||||||
view : 'text' //'text', 'style', 'meta'
|
view : 'text', //'text', 'style', 'meta', 'snippet'
|
||||||
|
snippetbarHeight : 25
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -61,12 +68,11 @@ const Editor = createClass({
|
|||||||
isText : function() {return this.state.view == 'text';},
|
isText : function() {return this.state.view == 'text';},
|
||||||
isStyle : function() {return this.state.view == 'style';},
|
isStyle : function() {return this.state.view == 'style';},
|
||||||
isMeta : function() {return this.state.view == 'meta';},
|
isMeta : function() {return this.state.view == 'meta';},
|
||||||
|
isSnip : function() {return this.state.view == 'snippet';},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
|
|
||||||
this.updateEditorSize();
|
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
|
||||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
|
|
||||||
@@ -79,10 +85,7 @@ const Editor = createClass({
|
|||||||
editorTheme : editorTheme
|
editorTheme : editorTheme
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
this.setState({ snippetbarHeight: document.querySelector('.editor > .snippetBar').offsetHeight });
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
window.removeEventListener('resize', this.updateEditorSize);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||||
@@ -117,24 +120,16 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
|
||||||
if(this.codeEditor.current) {
|
|
||||||
let paneHeight = this.editor.current.parentNode.clientHeight;
|
|
||||||
paneHeight -= SNIPPETBAR_HEIGHT;
|
|
||||||
this.codeEditor.current.codeMirror.setSize(null, paneHeight);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCurrentCursorPage : function(cursor) {
|
updateCurrentCursorPage : function(cursor) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
|
const lines = this.props.brew.text.split('\n').slice(1, cursor.line + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
this.props.onCursorPageChange(currentPage);
|
this.props.onCursorPageChange(currentPage);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentViewPage : function(topScrollLine) {
|
updateCurrentViewPage : function(topScrollLine) {
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
|
const lines = this.props.brew.text.split('\n').slice(1, topScrollLine + 1);
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
const pageRegex = this.props.brew.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
||||||
this.props.onViewPageChange(currentPage);
|
this.props.onViewPageChange(currentPage);
|
||||||
},
|
},
|
||||||
@@ -145,17 +140,17 @@ const Editor = createClass({
|
|||||||
|
|
||||||
handleViewChange : function(newView){
|
handleViewChange : function(newView){
|
||||||
this.props.setMoveArrows(newView === 'text');
|
this.props.setMoveArrows(newView === 'text');
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
view : newView
|
view : newView
|
||||||
}, ()=>{
|
}, ()=>{
|
||||||
this.codeEditor.current?.codeMirror.focus();
|
this.codeEditor.current?.codeMirror.focus();
|
||||||
this.updateEditorSize();
|
});
|
||||||
}); //TODO: not sure if updateeditorsize needed
|
|
||||||
},
|
},
|
||||||
|
|
||||||
highlightCustomMarkdown : function(){
|
highlightCustomMarkdown : function(){
|
||||||
if(!this.codeEditor.current) return;
|
if(!this.codeEditor.current) return;
|
||||||
if(this.state.view === 'text') {
|
if((this.state.view === 'text') ||(this.state.view === 'snippet')) {
|
||||||
const codeMirror = this.codeEditor.current.codeMirror;
|
const codeMirror = this.codeEditor.current.codeMirror;
|
||||||
|
|
||||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||||
@@ -174,12 +169,18 @@ const Editor = createClass({
|
|||||||
|
|
||||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||||
|
|
||||||
let editorPageCount = 2; // start page count from page 2
|
let userSnippetCount = 1; // start snippet count from snippet 1
|
||||||
|
let editorPageCount = 1; // start page count from page 1
|
||||||
|
|
||||||
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
const whichSource = this.state.view === 'text' ? this.props.brew.text : this.props.brew.snippets;
|
||||||
|
_.forEach(whichSource?.split('\n'), (line, lineNumber)=>{
|
||||||
|
|
||||||
|
const tabHighlight = this.state.view === 'text' ? 'pageLine' : 'snippetLine';
|
||||||
|
const textOrSnip = this.state.view === 'text';
|
||||||
|
|
||||||
//reset custom line styles
|
//reset custom line styles
|
||||||
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
||||||
|
codeMirror.removeLineClass(lineNumber, 'background', 'snippetLine');
|
||||||
codeMirror.removeLineClass(lineNumber, 'text');
|
codeMirror.removeLineClass(lineNumber, 'text');
|
||||||
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||||
|
|
||||||
@@ -190,22 +191,25 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// Styling for \page breaks
|
// Styling for \page breaks
|
||||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
(this.props.renderer == 'V3' && line.match(textOrSnip ? PAGEBREAK_REGEX_V3 : SNIPPETBREAK_REGEX_V3))) {
|
||||||
|
|
||||||
|
if((lineNumber > 0) && (textOrSnip)) // Since \page is optional on first line of document,
|
||||||
|
editorPageCount += 1; // don't use it to increment page count; stay at 1
|
||||||
|
else if(this.state.view !== 'text') userSnippetCount += 1;
|
||||||
|
|
||||||
// add back the original class 'background' but also add the new class '.pageline'
|
// add back the original class 'background' but also add the new class '.pageline'
|
||||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
codeMirror.addLineClass(lineNumber, 'background', tabHighlight);
|
||||||
const pageCountElement = Object.assign(document.createElement('span'), {
|
const pageCountElement = Object.assign(document.createElement('span'), {
|
||||||
className : 'editor-page-count',
|
className : 'editor-page-count',
|
||||||
textContent : editorPageCount
|
textContent : textOrSnip ? editorPageCount : userSnippetCount
|
||||||
});
|
});
|
||||||
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||||
|
|
||||||
editorPageCount += 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// New Codemirror styling for V3 renderer
|
// New Codemirror styling for V3 renderer
|
||||||
if(this.props.renderer == 'V3') {
|
if(this.props.renderer === 'V3') {
|
||||||
if(line.match(/^\\column$/)){
|
if(line.match(/^\\column(?:break)?$/)){
|
||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,7 +362,7 @@ const Editor = createClass({
|
|||||||
if(!this.isText() || isJumping)
|
if(!this.isText() || isJumping)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
const textSplit = this.props.renderer == 'V3' ? PAGEBREAK_REGEX_V3 : /\\page/;
|
||||||
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
||||||
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
||||||
|
|
||||||
@@ -406,6 +410,9 @@ 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.codeEditor.current?.updateSize();
|
this.codeEditor.current?.updateSize();
|
||||||
|
const snipHeight = document.querySelector('.editor > .snippetBar').offsetHeight;
|
||||||
|
if(snipHeight !== this.state.snippetbarHeight)
|
||||||
|
this.setState({ snippetbarHeight: snipHeight });
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorTheme : function(newTheme){
|
updateEditorTheme : function(newTheme){
|
||||||
@@ -428,9 +435,10 @@ const Editor = createClass({
|
|||||||
language='gfm'
|
language='gfm'
|
||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.text}
|
value={this.props.brew.text}
|
||||||
onChange={this.props.onTextChange}
|
onChange={this.props.onBrewChange('text')}
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent}
|
||||||
|
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isStyle()){
|
if(this.isStyle()){
|
||||||
@@ -440,10 +448,11 @@ const Editor = createClass({
|
|||||||
language='css'
|
language='css'
|
||||||
view={this.state.view}
|
view={this.state.view}
|
||||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||||
onChange={this.props.onStyleChange}
|
onChange={this.props.onBrewChange('style')}
|
||||||
enableFolding={true}
|
enableFolding={true}
|
||||||
editorTheme={this.state.editorTheme}
|
editorTheme={this.state.editorTheme}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent}
|
||||||
|
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isMeta()){
|
if(this.isMeta()){
|
||||||
@@ -454,11 +463,28 @@ const Editor = createClass({
|
|||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
onChange={this.props.onMetaChange}
|
themeBundle={this.props.themeBundle}
|
||||||
|
onChange={this.props.onBrewChange('metadata')}
|
||||||
reportError={this.props.reportError}
|
reportError={this.props.reportError}
|
||||||
userThemes={this.props.userThemes}/>
|
userThemes={this.props.userThemes}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.isSnip()){
|
||||||
|
if(!this.props.brew.snippets) { this.props.brew.snippets = DEFAULT_SNIPPET_TEXT; }
|
||||||
|
return <>
|
||||||
|
<CodeEditor key='codeEditor'
|
||||||
|
ref={this.codeEditor}
|
||||||
|
language='gfm'
|
||||||
|
view={this.state.view}
|
||||||
|
value={this.props.brew.snippets}
|
||||||
|
onChange={this.props.onBrewChange('snippets')}
|
||||||
|
enableFolding={true}
|
||||||
|
editorTheme={this.state.editorTheme}
|
||||||
|
rerenderParent={this.rerenderParent}
|
||||||
|
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
|
||||||
|
</>;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
redo : function(){
|
redo : function(){
|
||||||
@@ -499,7 +525,7 @@ const Editor = createClass({
|
|||||||
historySize={this.historySize()}
|
historySize={this.historySize()}
|
||||||
currentEditorTheme={this.state.editorTheme}
|
currentEditorTheme={this.state.editorTheme}
|
||||||
updateEditorTheme={this.updateEditorTheme}
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
snippetBundle={this.props.snippetBundle}
|
themeBundle={this.props.themeBundle}
|
||||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
||||||
updateBrew={this.props.updateBrew}
|
updateBrew={this.props.updateBrew}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
@import 'themes/codeMirror/customEditorStyles.less';
|
@import 'themes/codeMirror/customEditorStyles.less';
|
||||||
.editor {
|
.editor {
|
||||||
position : relative;
|
position : relative;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
container: editor / inline-size;
|
height : 100%;
|
||||||
|
container : editor / inline-size;
|
||||||
.codeEditor {
|
.codeEditor {
|
||||||
height : 100%;
|
height : calc(100% - 25px);
|
||||||
.pageLine {
|
.CodeMirror { height : 100%; }
|
||||||
|
.pageLine, .snippetLine {
|
||||||
background : #33333328;
|
background : #33333328;
|
||||||
border-top : #333399 solid 1px;
|
border-top : #333399 solid 1px;
|
||||||
}
|
}
|
||||||
@@ -14,6 +15,10 @@
|
|||||||
float : right;
|
float : right;
|
||||||
color : grey;
|
color : grey;
|
||||||
}
|
}
|
||||||
|
.editor-snippet-count {
|
||||||
|
float : right;
|
||||||
|
color : grey;
|
||||||
|
}
|
||||||
.columnSplit {
|
.columnSplit {
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
color : grey;
|
color : grey;
|
||||||
@@ -45,26 +50,26 @@
|
|||||||
color : green;
|
color : green;
|
||||||
}
|
}
|
||||||
.emoji:not(.cm-comment) {
|
.emoji:not(.cm-comment) {
|
||||||
margin-left : 2px;
|
|
||||||
color : #360034;
|
|
||||||
background : #ffc8ff;
|
|
||||||
border-radius : 6px;
|
|
||||||
font-weight : bold;
|
|
||||||
padding-bottom : 1px;
|
padding-bottom : 1px;
|
||||||
|
margin-left : 2px;
|
||||||
|
font-weight : bold;
|
||||||
|
color : #360034;
|
||||||
|
outline : solid 2px #FF96FC;
|
||||||
outline-offset : -2px;
|
outline-offset : -2px;
|
||||||
outline : solid 2px #ff96fc;
|
background : #FFC8FF;
|
||||||
|
border-radius : 6px;
|
||||||
}
|
}
|
||||||
.superscript:not(.cm-comment) {
|
.superscript:not(.cm-comment) {
|
||||||
font-weight : bold;
|
|
||||||
color : goldenrod;
|
|
||||||
vertical-align : super;
|
|
||||||
font-size : 0.9em;
|
font-size : 0.9em;
|
||||||
|
font-weight : bold;
|
||||||
|
vertical-align : super;
|
||||||
|
color : goldenrod;
|
||||||
}
|
}
|
||||||
.subscript:not(.cm-comment) {
|
.subscript:not(.cm-comment) {
|
||||||
font-weight : bold;
|
|
||||||
color : rgb(123, 123, 15);
|
|
||||||
vertical-align : sub;
|
|
||||||
font-size : 0.9em;
|
font-size : 0.9em;
|
||||||
|
font-weight : bold;
|
||||||
|
vertical-align : sub;
|
||||||
|
color : rgb(123, 123, 15);
|
||||||
}
|
}
|
||||||
.dl-highlight {
|
.dl-highlight {
|
||||||
&.dl-colon-highlight {
|
&.dl-colon-highlight {
|
||||||
@@ -103,4 +108,4 @@
|
|||||||
span { padding : 2px 5px; }
|
span { padding : 2px 5px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
const Combobox = require('client/components/combobox.jsx');
|
const Combobox = require('client/components/combobox.jsx');
|
||||||
const TagInput = require('../tagInput/tagInput.jsx');
|
const TagInput = require('../tagInput/tagInput.jsx');
|
||||||
|
|
||||||
@@ -40,6 +39,7 @@ const MetadataEditor = createClass({
|
|||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : 'en'
|
lang : 'en'
|
||||||
},
|
},
|
||||||
|
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
reportError : ()=>{}
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
@@ -67,6 +67,11 @@ const MetadataEditor = createClass({
|
|||||||
const inputRules = validations[name] ?? [];
|
const inputRules = validations[name] ?? [];
|
||||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||||
|
|
||||||
|
const debouncedReportValidity = _.debounce((target, errMessage)=>{
|
||||||
|
callIfExists(target, 'setCustomValidity', errMessage);
|
||||||
|
callIfExists(target, 'reportValidity');
|
||||||
|
}, 300); // 300ms debounce delay, adjust as needed
|
||||||
|
|
||||||
// if no validation rules, save to props
|
// if no validation rules, save to props
|
||||||
if(validationErr.length === 0){
|
if(validationErr.length === 0){
|
||||||
callIfExists(e.target, 'setCustomValidity', '');
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
@@ -74,14 +79,16 @@ const MetadataEditor = createClass({
|
|||||||
...this.props.metadata,
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// if validation issues, display built-in browser error popup with each error.
|
// if validation issues, display built-in browser error popup with each error.
|
||||||
const errMessage = validationErr.map((err)=>{
|
const errMessage = validationErr.map((err)=>{
|
||||||
return `- ${err}`;
|
return `- ${err}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
callIfExists(e.target, 'setCustomValidity', errMessage);
|
|
||||||
callIfExists(e.target, 'reportValidity');
|
debouncedReportValidity(e.target, errMessage);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -102,6 +109,7 @@ const MetadataEditor = createClass({
|
|||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata, 'renderer');
|
this.props.onChange(this.props.metadata, 'renderer');
|
||||||
},
|
},
|
||||||
|
|
||||||
handlePublish : function(val){
|
handlePublish : function(val){
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.metadata,
|
...this.props.metadata,
|
||||||
@@ -112,6 +120,14 @@ const MetadataEditor = createClass({
|
|||||||
handleTheme : function(theme){
|
handleTheme : function(theme){
|
||||||
this.props.metadata.renderer = theme.renderer;
|
this.props.metadata.renderer = theme.renderer;
|
||||||
this.props.metadata.theme = theme.path;
|
this.props.metadata.theme = theme.path;
|
||||||
|
|
||||||
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
|
},
|
||||||
|
|
||||||
|
handleThemeWritein : function(e) {
|
||||||
|
const shareId = e.target.value.split('/').pop(); //Extract just the ID if a URL was pasted in
|
||||||
|
this.props.metadata.theme = shareId;
|
||||||
|
|
||||||
this.props.onChange(this.props.metadata, 'theme');
|
this.props.onChange(this.props.metadata, 'theme');
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -200,7 +216,7 @@ const MetadataEditor = createClass({
|
|||||||
if(theme.path == this.props.metadata.shareId) return;
|
if(theme.path == this.props.metadata.shareId) return;
|
||||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
||||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
||||||
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
return <div className='item' key={`${renderer}_${theme.name}`} value={`${theme.author ?? renderer} : ${theme.name}`} data={theme} title={''}>
|
||||||
{theme.author ?? renderer} : {theme.name}
|
{theme.author ?? renderer} : {theme.name}
|
||||||
<div className='texture-container'>
|
<div className='texture-container'>
|
||||||
<img src={texture}/>
|
<img src={texture}/>
|
||||||
@@ -210,26 +226,40 @@ const MetadataEditor = createClass({
|
|||||||
<img src={preview}/>
|
<img src={preview}/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
}).filter(Boolean);
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentRenderer = this.props.metadata.renderer;
|
const currentRenderer = this.props.metadata.renderer;
|
||||||
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
const currentThemeDisplay = this.props.themeBundle?.name ? `${this.props.themeBundle.author ?? currentRenderer} : ${this.props.themeBundle.name}` : 'No Theme Selected';
|
||||||
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
|
||||||
let dropdown;
|
let dropdown;
|
||||||
|
|
||||||
if(currentRenderer == 'legacy') {
|
if(currentRenderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
<div className='disabled value' trigger='disabled'>
|
||||||
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
<div> Themes are not supported in the Legacy Renderer </div>
|
||||||
</Nav.dropdown>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='value' trigger='click'>
|
<div className='value'>
|
||||||
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
<Combobox trigger='click'
|
||||||
|
className='themes-dropdown'
|
||||||
{listThemes(currentRenderer)}
|
default={currentThemeDisplay}
|
||||||
</Nav.dropdown>;
|
placeholder='Select from below, or enter the Share URL or ID of a brew with the meta:theme tag'
|
||||||
|
onSelect={(value)=>this.handleTheme(value)}
|
||||||
|
onEntry={(e)=>{
|
||||||
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
|
if(this.handleFieldChange('theme', e))
|
||||||
|
this.handleThemeWritein(e);
|
||||||
|
}}
|
||||||
|
options={listThemes(currentRenderer)}
|
||||||
|
autoSuggest={{
|
||||||
|
suggestMethod : 'includes',
|
||||||
|
clearAutoSuggestOnClick : true,
|
||||||
|
filterOn : ['value', 'title']
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<small>Select from the list below (built-in themes and brews you have tagged "meta:theme"), or paste in the Share URL or Share ID of any brew.</small>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className='field themes'>
|
return <div className='field themes'>
|
||||||
@@ -244,15 +274,13 @@ const MetadataEditor = createClass({
|
|||||||
return _.map(langCodes.sort(), (code, index)=>{
|
return _.map(langCodes.sort(), (code, index)=>{
|
||||||
const localName = new Intl.DisplayNames([code], { type: 'language' });
|
const localName = new Intl.DisplayNames([code], { type: 'language' });
|
||||||
const englishName = new Intl.DisplayNames('en', { type: 'language' });
|
const englishName = new Intl.DisplayNames('en', { type: 'language' });
|
||||||
return <div className='item' title={`${englishName.of(code)}`} key={`${index}`} data-value={`${code}`} data-detail={`${localName.of(code)}`}>
|
return <div className='item' title={englishName.of(code)} key={`${index}`} value={code} detail={localName.of(code)}>
|
||||||
{`${code}`}
|
{code}
|
||||||
<div className='detail'>{`${localName.of(code)}`}</div>
|
<div className='detail'>{localName.of(code)}</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
|
|
||||||
|
|
||||||
return <div className='field language'>
|
return <div className='field language'>
|
||||||
<label>language</label>
|
<label>language</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -263,16 +291,15 @@ const MetadataEditor = createClass({
|
|||||||
onSelect={(value)=>this.handleLanguage(value)}
|
onSelect={(value)=>this.handleLanguage(value)}
|
||||||
onEntry={(e)=>{
|
onEntry={(e)=>{
|
||||||
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
debouncedHandleFieldChange('lang', e);
|
this.handleFieldChange('lang', e);
|
||||||
}}
|
}}
|
||||||
options={listLanguages()}
|
options={listLanguages()}
|
||||||
autoSuggest={{
|
autoSuggest={{
|
||||||
suggestMethod : 'startsWith',
|
suggestMethod : 'startsWith',
|
||||||
clearAutoSuggestOnClick : true,
|
clearAutoSuggestOnClick : true,
|
||||||
filterOn : ['data-value', 'data-detail', 'title']
|
filterOn : ['value', 'detail', 'title']
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
</Combobox>
|
|
||||||
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -345,7 +372,7 @@ const MetadataEditor = createClass({
|
|||||||
placeholder='add tag' unique={true}
|
placeholder='add tag' unique={true}
|
||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
onChange={(e)=>this.handleFieldChange('tags', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
@@ -370,7 +397,7 @@ const MetadataEditor = createClass({
|
|||||||
values={this.props.metadata.invitedAuthors}
|
values={this.props.metadata.invitedAuthors}
|
||||||
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>Privacy</h2>
|
<h2>Privacy</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
|
.userThemeName {
|
||||||
|
padding-right : 10px;
|
||||||
|
padding-left : 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.metadataEditor {
|
.metadataEditor {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 5;
|
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||||
padding : 25px;
|
padding : 25px;
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
|
font-size : 13px;
|
||||||
background-color : #999999;
|
background-color : #999999;
|
||||||
font-size : 13px;
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0 0 40px;
|
margin : 0 0 40px;
|
||||||
font-weight: bold;
|
font-weight : bold;
|
||||||
text-transform: uppercase;
|
text-transform : uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin : 20px 0;
|
margin : 20px 0;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
border-bottom: 2px solid gray;
|
color : #555555;
|
||||||
color: #555;
|
border-bottom : 2px solid gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div { margin-bottom : 10px; }
|
& > div { margin-bottom : 10px; }
|
||||||
@@ -51,10 +54,10 @@
|
|||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
& > label {
|
& > label {
|
||||||
width : 80px;
|
width : 80px;
|
||||||
|
font-size : 0.9em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.8em;
|
line-height : 1.8em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
font-size: .9em;
|
|
||||||
}
|
}
|
||||||
& > .value {
|
& > .value {
|
||||||
flex : 1 1 auto;
|
flex : 1 1 auto;
|
||||||
@@ -71,8 +74,7 @@
|
|||||||
border : 1px solid gray;
|
border : 1px solid gray;
|
||||||
&:focus { outline : 1px solid #444444; }
|
&:focus { outline : 1px solid #444444; }
|
||||||
}
|
}
|
||||||
&.thumbnail {
|
&.thumbnail, &.themes {
|
||||||
height : 1.4em;
|
|
||||||
label { line-height : 2.0em; }
|
label { line-height : 2.0em; }
|
||||||
.value {
|
.value {
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
@@ -88,6 +90,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.themes {
|
||||||
|
.value {
|
||||||
|
overflow : visible;
|
||||||
|
text-overflow : auto;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
padding-right : 5px;
|
||||||
|
padding-left : 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.description {
|
&.description {
|
||||||
flex : 1;
|
flex : 1;
|
||||||
textarea.value {
|
textarea.value {
|
||||||
@@ -123,8 +136,8 @@
|
|||||||
margin-right : 15px;
|
margin-right : 15px;
|
||||||
font-size : 0.9em;
|
font-size : 0.9em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
white-space : nowrap;
|
|
||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
|
white-space : nowrap;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
user-select : none;
|
user-select : none;
|
||||||
}
|
}
|
||||||
@@ -151,94 +164,74 @@
|
|||||||
.colorButton(@red);
|
.colorButton(@red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.authors.field .value {
|
.authors.field .value { line-height : 1.5em; }
|
||||||
line-height : 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.themes.field {
|
.themes.field {
|
||||||
.navDropdownContainer {
|
& .dropdown-container {
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 100;
|
z-index : 100;
|
||||||
background-color : white;
|
background-color : white;
|
||||||
&.disabled {
|
}
|
||||||
font-style : italic;
|
& .dropdown-options { overflow-y : visible; }
|
||||||
color : dimgray;
|
.disabled {
|
||||||
background-color : darkgray;
|
font-style : italic;
|
||||||
}
|
color : dimgray;
|
||||||
& > div:first-child {
|
background-color : darkgray;
|
||||||
padding : 3px 3px;
|
}
|
||||||
background-color : inherit;
|
.item {
|
||||||
border : 1px solid gray;
|
position : relative;
|
||||||
i { float : right; }
|
padding : 3px 3px;
|
||||||
&:hover {
|
overflow : visible;
|
||||||
color : white;
|
background-color : white;
|
||||||
background-color : @blue;
|
border-top : 1px solid rgb(118, 118, 118);
|
||||||
|
.preview {
|
||||||
|
position : absolute;
|
||||||
|
top : 0;
|
||||||
|
right : 0;
|
||||||
|
z-index : 1;
|
||||||
|
display : flex;
|
||||||
|
flex-direction : column;
|
||||||
|
width : 200px;
|
||||||
|
overflow : hidden;
|
||||||
|
color : black;
|
||||||
|
background : #CCCCCC;
|
||||||
|
border-radius : 5px;
|
||||||
|
box-shadow : 0 0 5px black;
|
||||||
|
opacity : 0;
|
||||||
|
transition : opacity 250ms ease;
|
||||||
|
h6 {
|
||||||
|
padding-block : 0.5em;
|
||||||
|
padding-inline : 1em;
|
||||||
|
font-weight : 900;
|
||||||
|
border-bottom : 2px solid hsl(0,0%,40%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navDropdown .item > p {
|
|
||||||
width : 45%;
|
.texture-container {
|
||||||
height : 1.1em;
|
position : absolute;
|
||||||
overflow : hidden;
|
top : 0;
|
||||||
text-overflow : ellipsis;
|
left : 0;
|
||||||
white-space : nowrap;
|
width : 100%;
|
||||||
}
|
height : 100%;
|
||||||
.navDropdown {
|
min-height : 100%;
|
||||||
position : absolute;
|
overflow : hidden;
|
||||||
width : 100%;
|
> img {
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
position : absolute;
|
||||||
.item {
|
top : 0;
|
||||||
position : relative;
|
right : 0;
|
||||||
padding : 3px 3px;
|
width : 50%;
|
||||||
overflow : visible;
|
min-height : 100%;
|
||||||
background-color : white;
|
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
border-top : 1px solid rgb(118, 118, 118);
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
.preview {
|
|
||||||
position : absolute;
|
|
||||||
top : 0;
|
|
||||||
right : 0;
|
|
||||||
z-index : 1;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
width : 200px;
|
|
||||||
overflow : hidden;
|
|
||||||
color : black;
|
|
||||||
background : #CCCCCC;
|
|
||||||
border-radius : 5px;
|
|
||||||
box-shadow : 0 0 5px black;
|
|
||||||
opacity : 0;
|
|
||||||
transition : opacity 250ms ease;
|
|
||||||
h6 {
|
|
||||||
padding-block : 0.5em;
|
|
||||||
padding-inline : 1em;
|
|
||||||
font-weight : 900;
|
|
||||||
border-bottom : 2px solid hsl(0,0%,40%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
color : white;
|
|
||||||
background-color : @blue;
|
|
||||||
}
|
|
||||||
&:hover > .preview { opacity : 1; }
|
|
||||||
.texture-container {
|
|
||||||
position : absolute;
|
|
||||||
top : 0;
|
|
||||||
left : 0;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
min-height : 100%;
|
|
||||||
overflow : hidden;
|
|
||||||
> img {
|
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
right : 0;
|
|
||||||
width : 50%;
|
|
||||||
min-height : 100%;
|
|
||||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
|
||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color : white;
|
||||||
|
background-color : @blue;
|
||||||
|
filter : unset;
|
||||||
|
}
|
||||||
|
&:hover > .preview { opacity : 1; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ module.exports = {
|
|||||||
(value)=>{
|
(value)=>{
|
||||||
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
|
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
theme : [
|
||||||
|
(value)=>{
|
||||||
|
const URL = global.config.baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'); //Escape any regex characters
|
||||||
|
const shareIDPattern = '[a-zA-Z0-9-_]{12}';
|
||||||
|
const shareURLRegex = new RegExp(`^${URL}\\/share\\/${shareIDPattern}$`);
|
||||||
|
const shareIDRegex = new RegExp(`^${shareIDPattern}$`);
|
||||||
|
if(value?.length === 0) return null;
|
||||||
|
if(shareURLRegex.test(value)) return null;
|
||||||
|
if(shareIDRegex.test(value)) return null;
|
||||||
|
|
||||||
|
return 'Must be a valid Share URL or a 12-character ID.';
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const _ = require('lodash');
|
|||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
import { loadHistory } from '../../utils/versionHistory.js';
|
import { loadHistory } from '../../utils/versionHistory.js';
|
||||||
|
import { brewSnippetsToJSON } from '../../../../shared/helpers.js';
|
||||||
|
|
||||||
//Import all themes
|
//Import all themes
|
||||||
const ThemeSnippets = {};
|
const ThemeSnippets = {};
|
||||||
@@ -40,7 +41,7 @@ const Snippetbar = createClass({
|
|||||||
unfoldCode : ()=>{},
|
unfoldCode : ()=>{},
|
||||||
updateEditorTheme : ()=>{},
|
updateEditorTheme : ()=>{},
|
||||||
cursorPos : {},
|
cursorPos : {},
|
||||||
snippetBundle : [],
|
themeBundle : [],
|
||||||
updateBrew : ()=>{}
|
updateBrew : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -64,7 +65,10 @@ const Snippetbar = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : async function(prevProps, prevState) {
|
componentDidUpdate : async function(prevProps, prevState) {
|
||||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
if(prevProps.renderer != this.props.renderer ||
|
||||||
|
prevProps.theme != this.props.theme ||
|
||||||
|
prevProps.themeBundle != this.props.themeBundle ||
|
||||||
|
prevProps.brew.snippets != this.props.brew.snippets) {
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : this.compileSnippets()
|
snippets : this.compileSnippets()
|
||||||
});
|
});
|
||||||
@@ -97,7 +101,7 @@ const Snippetbar = createClass({
|
|||||||
if(key == 'snippets') {
|
if(key == 'snippets') {
|
||||||
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
||||||
return result.filter((snip)=>snip.gen || snip.subsnippets);
|
return result.filter((snip)=>snip.gen || snip.subsnippets);
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
compileSnippets : function() {
|
compileSnippets : function() {
|
||||||
@@ -105,15 +109,21 @@ const Snippetbar = createClass({
|
|||||||
|
|
||||||
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||||
|
|
||||||
for (let snippets of this.props.snippetBundle) {
|
if(this.props.themeBundle.snippets) {
|
||||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
for (let snippets of this.props.themeBundle.snippets) {
|
||||||
snippets = ThemeSnippets[snippets];
|
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
||||||
|
snippets = ThemeSnippets[snippets];
|
||||||
|
|
||||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
||||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
||||||
|
|
||||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userSnippetsasJSON = brewSnippetsToJSON(this.props.brew.title || 'New Document', this.props.brew.snippets, this.props.themeBundle.snippets);
|
||||||
|
compiledSnippets.push(userSnippetsasJSON);
|
||||||
|
|
||||||
return compiledSnippets;
|
return compiledSnippets;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -207,59 +217,60 @@ const Snippetbar = createClass({
|
|||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='editors'>
|
<div className='editors'>
|
||||||
{this.props.view !== 'meta' && <><div className='historyTools'>
|
{this.props.view !== 'meta' && <><div className='historyTools'>
|
||||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
||||||
onClick={this.toggleHistoryMenu} >
|
onClick={this.toggleHistoryMenu} >
|
||||||
<i className='fas fa-clock-rotate-left' />
|
<i className='fas fa-clock-rotate-left' />
|
||||||
{ this.state.showHistory && this.renderHistoryItems() }
|
{ this.state.showHistory && this.renderHistoryItems() }
|
||||||
|
</div>
|
||||||
|
<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>
|
</div>
|
||||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
<div className='codeTools'>
|
||||||
onClick={this.props.undo} >
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
<i className='fas fa-undo' />
|
onClick={this.props.foldCode} >
|
||||||
</div>
|
<i className='fas fa-compress-alt' />
|
||||||
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
|
</div>
|
||||||
onClick={this.props.redo} >
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
<i className='fas fa-redo' />
|
onClick={this.props.unfoldCode} >
|
||||||
</div>
|
<i className='fas fa-expand-alt' />
|
||||||
</div>
|
</div>
|
||||||
<div className='codeTools'>
|
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
onClick={this.toggleThemeSelector} >
|
||||||
onClick={this.props.foldCode} >
|
<i className='fas fa-palette' />
|
||||||
<i className='fas fa-compress-alt' />
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
</div>
|
</div>
|
||||||
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
</div></>}
|
||||||
onClick={this.props.unfoldCode} >
|
|
||||||
<i className='fas fa-expand-alt' />
|
|
||||||
</div>
|
|
||||||
<div className={`editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
|
||||||
onClick={this.toggleThemeSelector} >
|
|
||||||
<i className='fas fa-palette' />
|
|
||||||
{this.state.themeSelector && this.renderThemeSelector()}
|
|
||||||
</div>
|
|
||||||
</div></>}
|
|
||||||
|
|
||||||
|
|
||||||
<div className='tabs'>
|
<div className='tabs'>
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
onClick={()=>this.props.onViewChange('text')}>
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
<i className='fa fa-beer' />
|
<i className='fa fa-beer' />
|
||||||
|
</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('snippet', { selected: this.props.view === 'snippet' })}
|
||||||
|
onClick={()=>this.props.onViewChange('snippet')}>
|
||||||
|
<i className='fas fa-th-list' />
|
||||||
|
</div>
|
||||||
|
<div className={cx('meta', { selected: this.props.view === 'meta' })}
|
||||||
|
onClick={()=>this.props.onViewChange('meta')}>
|
||||||
|
<i className='fas fa-info-circle' />
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
@@ -272,11 +283,6 @@ const Snippetbar = createClass({
|
|||||||
|
|
||||||
module.exports = Snippetbar;
|
module.exports = Snippetbar;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const SnippetGroup = createClass({
|
const SnippetGroup = createClass({
|
||||||
displayName : 'SnippetGroup',
|
displayName : 'SnippetGroup',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -310,7 +316,8 @@ const SnippetGroup = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='snippetGroup snippetBarButton'>
|
const snippetGroup = `snippetGroup snippetBarButton ${this.props.snippets.length === 0 ? 'disabledSnippets' : ''}`;
|
||||||
|
return <div className={snippetGroup}>
|
||||||
<div className='text'>
|
<div className='text'>
|
||||||
<i className={this.props.icon} />
|
<i className={this.props.icon} />
|
||||||
<span className='groupName'>{this.props.groupName}</span>
|
<span className='groupName'>{this.props.groupName}</span>
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
.snippets {
|
.snippets {
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : flex-start;
|
justify-content : flex-start;
|
||||||
min-width : 327.58px;
|
min-width : 432.18px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
|
||||||
}
|
}
|
||||||
|
|
||||||
.editors {
|
.editors {
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : flex-end;
|
justify-content : flex-end;
|
||||||
min-width : 225px;
|
min-width : 250px; //must be controlled every time an item is added, must be hardcoded for the wrapping as it is applied
|
||||||
|
|
||||||
&:only-child { margin-left : auto;min-width:unset;}
|
&:only-child {min-width : unset; margin-left : auto;}
|
||||||
|
|
||||||
>div {
|
>div {
|
||||||
display : flex;
|
display : flex;
|
||||||
@@ -39,9 +39,7 @@
|
|||||||
text-align : center;
|
text-align : center;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|
||||||
&.editorTool:not(.active) {
|
&.editorTool:not(.active) { cursor : not-allowed; }
|
||||||
cursor:not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,&.selected { background-color : #999999; }
|
&:hover,&.selected { background-color : #999999; }
|
||||||
&.text {
|
&.text {
|
||||||
@@ -53,6 +51,9 @@
|
|||||||
&.meta {
|
&.meta {
|
||||||
.tooltipLeft('Properties');
|
.tooltipLeft('Properties');
|
||||||
}
|
}
|
||||||
|
&.snippet {
|
||||||
|
.tooltipLeft('Snippets');
|
||||||
|
}
|
||||||
&.undo {
|
&.undo {
|
||||||
.tooltipLeft('Undo');
|
.tooltipLeft('Undo');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
@@ -92,7 +93,7 @@
|
|||||||
&.editorTheme {
|
&.editorTheme {
|
||||||
.tooltipLeft('Editor Themes');
|
.tooltipLeft('Editor Themes');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : black;
|
color : inherit;
|
||||||
&.active {
|
&.active {
|
||||||
position : relative;
|
position : relative;
|
||||||
background-color : #999999;
|
background-color : #999999;
|
||||||
@@ -151,9 +152,9 @@
|
|||||||
position : absolute;
|
position : absolute;
|
||||||
top : 100%;
|
top : 100%;
|
||||||
z-index : 1000;
|
z-index : 1000;
|
||||||
|
visibility : hidden;
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin-left : -5px;
|
margin-left : -5px;
|
||||||
visibility : hidden;
|
|
||||||
background-color : #DDDDDD;
|
background-color : #DDDDDD;
|
||||||
.snippet {
|
.snippet {
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -228,8 +229,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.disabledSnippets {
|
||||||
|
color: grey;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
&:hover { background-color: #DDDDDD;}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@container editor (width < 553px) {
|
@container editor (width < 683px) {
|
||||||
.snippetBar {
|
.snippetBar {
|
||||||
.editors {
|
.editors {
|
||||||
flex : 1;
|
flex : 1;
|
||||||
|
|||||||
@@ -3,43 +3,43 @@ const React = require('react');
|
|||||||
const { useState, useEffect } = React;
|
const { useState, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const TagInput = ({ unique = true, values = [], ...props }) => {
|
const TagInput = ({ unique = true, values = [], ...props })=>{
|
||||||
const [tempInputText, setTempInputText] = useState('');
|
const [tempInputText, setTempInputText] = useState('');
|
||||||
const [tagList, setTagList] = useState(values.map((value) => ({ value, editing: false })));
|
const [tagList, setTagList] = useState(values.map((value)=>({ value, editing: false })));
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
handleChange(tagList.map((context)=>context.value))
|
handleChange(tagList.map((context)=>context.value));
|
||||||
}, [tagList])
|
}, [tagList]);
|
||||||
|
|
||||||
const handleChange = (value)=>{
|
const handleChange = (value)=>{
|
||||||
props.onChange({
|
props.onChange({
|
||||||
target : { value }
|
target : { value }
|
||||||
})
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInputKeyDown = ({ evt, value, index, options = {} }) => {
|
const handleInputKeyDown = ({ evt, value, index, options = {} })=>{
|
||||||
if (_.includes(['Enter', ','], evt.key)) {
|
if(_.includes(['Enter', ','], evt.key)) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
submitTag(evt.target.value, value, index);
|
submitTag(evt.target.value, value, index);
|
||||||
if (options.clear) {
|
if(options.clear) {
|
||||||
setTempInputText('');
|
setTempInputText('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitTag = (newValue, originalValue, index) => {
|
const submitTag = (newValue, originalValue, index)=>{
|
||||||
setTagList((prevContext) => {
|
setTagList((prevContext)=>{
|
||||||
// remove existing tag
|
// remove existing tag
|
||||||
if(newValue === null){
|
if(newValue === null){
|
||||||
return [...prevContext].filter((context, i)=>i !== index);
|
return [...prevContext].filter((context, i)=>i !== index);
|
||||||
}
|
}
|
||||||
// add new tag
|
// add new tag
|
||||||
if(originalValue === null){
|
if(originalValue === null){
|
||||||
return [...prevContext, { value: newValue, editing: false }]
|
return [...prevContext, { value: newValue, editing: false }];
|
||||||
}
|
}
|
||||||
// update existing tag
|
// update existing tag
|
||||||
return prevContext.map((context, i) => {
|
return prevContext.map((context, i)=>{
|
||||||
if (i === index) {
|
if(i === index) {
|
||||||
return { ...context, value: newValue, editing: false };
|
return { ...context, value: newValue, editing: false };
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
@@ -47,10 +47,10 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const editTag = (index) => {
|
const editTag = (index)=>{
|
||||||
setTagList((prevContext) => {
|
setTagList((prevContext)=>{
|
||||||
return prevContext.map((context, i) => {
|
return prevContext.map((context, i)=>{
|
||||||
if (i === index) {
|
if(i === index) {
|
||||||
return { ...context, editing: true };
|
return { ...context, editing: true };
|
||||||
}
|
}
|
||||||
return { ...context, editing: false };
|
return { ...context, editing: false };
|
||||||
@@ -58,25 +58,25 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderReadTag = (context, index) => {
|
const renderReadTag = (context, index)=>{
|
||||||
return (
|
return (
|
||||||
<li key={index}
|
<li key={index}
|
||||||
data-value={context.value}
|
data-value={context.value}
|
||||||
className='tag'
|
className='tag'
|
||||||
onClick={() => editTag(index)}>
|
onClick={()=>editTag(index)}>
|
||||||
{context.value}
|
{context.value}
|
||||||
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index)}}><i className='fa fa-times fa-fw'/></button>
|
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index);}}><i className='fa fa-times fa-fw'/></button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderWriteTag = (context, index) => {
|
const renderWriteTag = (context, index)=>{
|
||||||
return (
|
return (
|
||||||
<input type='text'
|
<input type='text'
|
||||||
key={index}
|
key={index}
|
||||||
defaultValue={context.value}
|
defaultValue={context.value}
|
||||||
onKeyDown={(evt) => handleInputKeyDown({evt, value: context.value, index: index})}
|
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: context.value, index: index })}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -86,7 +86,7 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
|||||||
<label>{props.label}</label>
|
<label>{props.label}</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
<ul className='list'>
|
<ul className='list'>
|
||||||
{tagList.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
{tagList.map((context, index)=>{ return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@@ -94,8 +94,8 @@ const TagInput = ({ unique = true, values = [], ...props }) => {
|
|||||||
className='value'
|
className='value'
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
value={tempInputText}
|
value={tempInputText}
|
||||||
onChange={(e) => setTempInputText(e.target.value)}
|
onChange={(e)=>setTempInputText(e.target.value)}
|
||||||
onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
onKeyDown={(evt)=>handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,95 +1,79 @@
|
|||||||
//╔===--------------- Polyfills --------------===╗//
|
/* eslint-disable camelcase */
|
||||||
import 'core-js/es/string/to-well-formed.js';
|
import 'core-js/es/string/to-well-formed.js'; //Polyfill for older browsers
|
||||||
//╚===--------------- ---------------===╝//
|
import './homebrew.less';
|
||||||
|
import React from 'react';
|
||||||
|
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
require('./homebrew.less');
|
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
|
||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const { StaticRouter:Router } = require('react-router');
|
|
||||||
const { Route, Routes, useParams, useSearchParams } = require('react-router');
|
|
||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
import HomePage from './pages/homePage/homePage.jsx';
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
import EditPage from './pages/editPage/editPage.jsx';
|
||||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
import UserPage from './pages/userPage/userPage.jsx';
|
||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
import SharePage from './pages/sharePage/sharePage.jsx';
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
import NewPage from './pages/newPage/newPage.jsx';
|
||||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
import ErrorPage from './pages/errorPage/errorPage.jsx';
|
||||||
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
|
import VaultPage from './pages/vaultPage/vaultPage.jsx';
|
||||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
import AccountPage from './pages/accountPage/accountPage.jsx';
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = ({ el: Element, ...rest })=>{
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const queryParams = {};
|
const queryParams = Object.fromEntries(searchParams?.entries() || []);
|
||||||
for (const [key, value] of searchParams?.entries() || []) {
|
|
||||||
queryParams[key] = value;
|
return <Element {...rest} {...params} query={queryParams} />;
|
||||||
}
|
|
||||||
const Element = props.el;
|
|
||||||
const allProps = {
|
|
||||||
...props,
|
|
||||||
...params,
|
|
||||||
query : queryParams,
|
|
||||||
el : undefined
|
|
||||||
};
|
|
||||||
return <Element {...allProps} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Homebrew = createClass({
|
const Homebrew = (props)=>{
|
||||||
displayName : 'Homebrewery',
|
const {
|
||||||
getDefaultProps : function() {
|
url = '',
|
||||||
return {
|
version = '0.0.0',
|
||||||
url : '',
|
account = null,
|
||||||
welcomeText : '',
|
enable_v3 = false,
|
||||||
changelog : '',
|
enable_themes,
|
||||||
version : '0.0.0',
|
config,
|
||||||
account : null,
|
brew = {
|
||||||
enable_v3 : false,
|
title : '',
|
||||||
brew : {
|
text : '',
|
||||||
title : '',
|
shareId : null,
|
||||||
text : '',
|
editId : null,
|
||||||
shareId : null,
|
createdAt : null,
|
||||||
editId : null,
|
updatedAt : null,
|
||||||
createdAt : null,
|
lang : ''
|
||||||
updatedAt : null,
|
},
|
||||||
lang : ''
|
userThemes,
|
||||||
}
|
brews
|
||||||
};
|
} = props;
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
global.account = account;
|
||||||
global.account = this.props.account;
|
global.version = version;
|
||||||
global.version = this.props.version;
|
global.enable_v3 = enable_v3;
|
||||||
global.enable_v3 = this.props.enable_v3;
|
global.enable_themes = enable_themes;
|
||||||
global.enable_themes = this.props.enable_themes;
|
global.config = config;
|
||||||
global.config = this.props.config;
|
|
||||||
|
|
||||||
return {};
|
updateLocalStorage();
|
||||||
},
|
|
||||||
|
|
||||||
render : function (){
|
return (
|
||||||
return (
|
<Router location={url}>
|
||||||
<Router location={this.props.url}>
|
<div className='homebrew'>
|
||||||
<div className='homebrew'>
|
<Routes>
|
||||||
<Routes>
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />
|
||||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={brew} userThemes={userThemes}/>} />
|
||||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
<Route path='/new' element={<WithRoute el={NewPage} userThemes={userThemes}/> } />
|
||||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={brews} />} />
|
||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
||||||
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
<Route path='/migrate' element={<WithRoute el={SharePage} brew={brew} disableMeta={true} />} />
|
||||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
<Route path='/account' element={<WithRoute el={AccountPage} brew={brew} accountDetails={brew.accountDetails} />} />
|
||||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/error' element={<WithRoute el={ErrorPage} brew={brew} />} />
|
||||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
<Route path='/' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/*' element={<WithRoute el={HomePage} brew={brew} />} />
|
||||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
</Routes>
|
||||||
</Routes>
|
</div>
|
||||||
</div>
|
</Router>
|
||||||
</Router>
|
);
|
||||||
);
|
};
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = Homebrew;
|
module.exports = Homebrew;
|
||||||
@@ -1,36 +1,32 @@
|
|||||||
@import 'naturalcrit/styles/core.less';
|
@import 'naturalcrit/styles/core.less';
|
||||||
.homebrew{
|
.homebrew {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.sitePage{
|
.sitePage {
|
||||||
display : flex;
|
display : flex;
|
||||||
height : 100%;
|
|
||||||
background-color : @steel;
|
|
||||||
flex-direction : column;
|
flex-direction : column;
|
||||||
|
height : 100%;
|
||||||
overflow-y : hidden;
|
overflow-y : hidden;
|
||||||
.content{
|
background-color : @steel;
|
||||||
|
.content {
|
||||||
position : relative;
|
position : relative;
|
||||||
height : calc(~"100% - 29px"); //Navbar height
|
|
||||||
flex : auto;
|
flex : auto;
|
||||||
|
height : calc(~'100% - 29px'); //Navbar height
|
||||||
overflow-y : hidden;
|
overflow-y : hidden;
|
||||||
}
|
}
|
||||||
&.listPage .content {
|
&.listPage .content {
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 20px;
|
width : 20px;
|
||||||
&:horizontal{
|
&:horizontal {
|
||||||
height: 20px;
|
width : auto;
|
||||||
width:auto;
|
height : 20px;
|
||||||
}
|
}
|
||||||
&-thumb {
|
&-thumb {
|
||||||
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
|
||||||
&:horizontal{
|
&:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
|
||||||
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-corner {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
}
|
||||||
|
&-corner { visibility : hidden; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,131 +1,138 @@
|
|||||||
require('./error-navitem.less');
|
require('./error-navitem.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const createClass = require('create-react-class');
|
|
||||||
|
|
||||||
const ErrorNavItem = createClass({
|
const ErrorNavItem = ({error = '', clearError})=>{
|
||||||
getDefaultProps : function() {
|
const response = error.response;
|
||||||
return {
|
const errorCode = error.code
|
||||||
error : '',
|
const status = response?.status;
|
||||||
parent : null
|
const HBErrorCode = response?.body?.HBErrorCode;
|
||||||
};
|
const message = response?.body?.message;
|
||||||
},
|
|
||||||
render : function() {
|
|
||||||
const clearError = ()=>{
|
|
||||||
const state = {
|
|
||||||
error : null
|
|
||||||
};
|
|
||||||
if(this.props.parent.state.isSaving) {
|
|
||||||
state.isSaving = false;
|
|
||||||
}
|
|
||||||
this.props.parent.setState(state);
|
|
||||||
};
|
|
||||||
|
|
||||||
const error = this.props.error;
|
let errMsg = '';
|
||||||
const response = error.response;
|
try {
|
||||||
const status = response.status;
|
errMsg += `${error.toString()}\n\n`;
|
||||||
const HBErrorCode = response.body?.HBErrorCode;
|
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||||
const message = response.body?.message;
|
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
|
||||||
let errMsg = '';
|
console.log(errMsg);
|
||||||
try {
|
} catch (e){}
|
||||||
errMsg += `${error.toString()}\n\n`;
|
|
||||||
errMsg += `\`\`\`\n${error.stack}\n`;
|
|
||||||
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
|
||||||
console.log(errMsg);
|
|
||||||
} catch (e){}
|
|
||||||
|
|
||||||
if(status === 409) {
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={clearError}>
|
|
||||||
{message ?? 'Conflict: please refresh to get latest changes'}
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(status === 412) {
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={clearError}>
|
|
||||||
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(HBErrorCode === '04') {
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={clearError}>
|
|
||||||
You are no longer signed in as an author of
|
|
||||||
this brew! Were you signed out from a different
|
|
||||||
window? Visit our log in page, then try again!
|
|
||||||
<br></br>
|
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
|
||||||
<div className='confirm'>
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div className='deny'>
|
|
||||||
Not Now
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={clearError}>
|
|
||||||
Can't save because your Google Drive seems to be full!
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={clearError}>
|
|
||||||
Looks like your Google credentials have
|
|
||||||
expired! Visit our log in page to sign out
|
|
||||||
and sign back in with Google,
|
|
||||||
then try saving again!
|
|
||||||
<br></br>
|
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
|
||||||
<div className='confirm'>
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div className='deny'>
|
|
||||||
Not Now
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(HBErrorCode === '09') {
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={clearError}>
|
|
||||||
Looks like there was a problem retreiving
|
|
||||||
the theme, or a theme that it inherits,
|
|
||||||
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
|
||||||
{response.body.brewId}</a> still exists!
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if(status === 409) {
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer' onClick={clearError}>
|
||||||
Looks like there was a problem saving. <br />
|
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||||
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
|
||||||
here
|
|
||||||
</a>.
|
|
||||||
</div>
|
</div>
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if(status === 412) {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '04') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
You are no longer signed in as an author of
|
||||||
|
this brew! Were you signed out from a different
|
||||||
|
window? Visit our log in page, then try again!
|
||||||
|
<br></br>
|
||||||
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
|
<div className='confirm'>
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className='deny'>
|
||||||
|
Not Now
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response?.body?.errors?.[0].reason == 'storageQuotaExceeded') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Can't save because your Google Drive seems to be full!
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response?.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like your Google credentials have
|
||||||
|
expired! Visit our log in page to sign out
|
||||||
|
and sign back in with Google,
|
||||||
|
then try saving again!
|
||||||
|
<br></br>
|
||||||
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
|
<div className='confirm'>
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className='deny'>
|
||||||
|
Not Now
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '09') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like there was a problem retreiving
|
||||||
|
the theme, or a theme that it inherits,
|
||||||
|
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||||
|
{response.body.brewId}</a> still exists!
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(HBErrorCode === '10') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like the brew you have selected
|
||||||
|
as a theme is not tagged for use as a
|
||||||
|
theme. Verify that
|
||||||
|
brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
||||||
|
{response.body.brewId}</a> has the <span className='lowercase'>meta:theme</span> tag!
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(errorCode === 'ECONNABORTED') {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
The request to the server was interrupted or timed out.
|
||||||
|
This can happen due to a network issue, or if
|
||||||
|
trying to save a particularly large brew.
|
||||||
|
Please check your internet connection and try again.
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer'>
|
||||||
|
Looks like there was a problem saving. <br />
|
||||||
|
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||||
|
here
|
||||||
|
</a>.
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = ErrorNavItem;
|
module.exports = ErrorNavItem;
|
||||||
|
|||||||
@@ -1,78 +1,70 @@
|
|||||||
.navItem.error {
|
.navItem.error {
|
||||||
position : relative;
|
position : relative;
|
||||||
background-color : @red;
|
background-color : @red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorContainer{
|
.errorContainer {
|
||||||
animation-name: glideDown;
|
position : absolute;
|
||||||
animation-duration: 0.4s;
|
top : 100%;
|
||||||
position : absolute;
|
left : 50%;
|
||||||
top : 100%;
|
z-index : 1000;
|
||||||
left : 50%;
|
width : 140px;
|
||||||
z-index : 1000;
|
padding : 3px;
|
||||||
width : 140px;
|
font-size : 10px;
|
||||||
padding : 3px;
|
font-weight : 800;
|
||||||
color : white;
|
color : white;
|
||||||
background-color : #333;
|
text-align : center;
|
||||||
border : 3px solid #444;
|
text-transform : uppercase;
|
||||||
border-radius : 5px;
|
background-color : #333333;
|
||||||
transform : translate(-50% + 3px, 10px);
|
border : 3px solid #444444;
|
||||||
text-align : center;
|
border-radius : 5px;
|
||||||
font-size : 10px;
|
transform : translate(-50% + 3px, 10px);
|
||||||
font-weight : 800;
|
animation-name : glideDown;
|
||||||
text-transform : uppercase;
|
animation-duration : 0.4s;
|
||||||
.lowercase {
|
.lowercase { text-transform : none; }
|
||||||
text-transform : none;
|
a { color : @teal; }
|
||||||
|
&::before {
|
||||||
|
position : absolute;
|
||||||
|
top : -23px;
|
||||||
|
left : 53px;
|
||||||
|
width : 0px;
|
||||||
|
height : 0px;
|
||||||
|
content : '';
|
||||||
|
border-top : 10px solid transparent;
|
||||||
|
border-right : 10px solid transparent;
|
||||||
|
border-bottom : 10px solid #444444;
|
||||||
|
border-left : 10px solid transparent;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
position : absolute;
|
||||||
|
top : -19px;
|
||||||
|
left : 53px;
|
||||||
|
width : 0px;
|
||||||
|
height : 0px;
|
||||||
|
content : '';
|
||||||
|
border-top : 10px solid transparent;
|
||||||
|
border-right : 10px solid transparent;
|
||||||
|
border-bottom : 10px solid #333333;
|
||||||
|
border-left : 10px solid transparent;
|
||||||
|
}
|
||||||
|
.deny {
|
||||||
|
display : inline-block;
|
||||||
|
width : 48%;
|
||||||
|
padding : 5px;
|
||||||
|
margin : 1px;
|
||||||
|
background-color : #333333;
|
||||||
|
border-left : 1px solid #666666;
|
||||||
|
.animate(background-color);
|
||||||
|
&:hover { background-color : red; }
|
||||||
|
}
|
||||||
|
.confirm {
|
||||||
|
display : inline-block;
|
||||||
|
width : 48%;
|
||||||
|
padding : 5px;
|
||||||
|
margin : 1px;
|
||||||
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
|
.animate(background-color);
|
||||||
|
&:hover { background-color : teal; }
|
||||||
}
|
}
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.homebrew nav {
|
.homebrew nav {
|
||||||
|
position : relative;
|
||||||
|
z-index : 2;
|
||||||
|
display : flex;
|
||||||
|
justify-content : space-between;
|
||||||
background-color : #333333;
|
background-color : #333333;
|
||||||
position : relative;
|
|
||||||
z-index : 2;
|
|
||||||
display : flex;
|
|
||||||
justify-content : space-between;
|
|
||||||
|
|
||||||
.navSection {
|
.navSection {
|
||||||
display : flex;
|
display : flex;
|
||||||
@@ -82,8 +82,8 @@
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 13px;
|
line-height : 13px;
|
||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
|
text-decoration : none;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
background-color : #333333;
|
background-color : #333333;
|
||||||
i {
|
i {
|
||||||
@@ -106,11 +106,11 @@
|
|||||||
display : block;
|
display : block;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
overflow : hidden;
|
overflow : hidden;
|
||||||
|
text-overflow : ellipsis;
|
||||||
font-size : 12px;
|
font-size : 12px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
color : white;
|
color : white;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
text-overflow : ellipsis;
|
|
||||||
text-transform : initial;
|
text-transform : initial;
|
||||||
white-space : nowrap;
|
white-space : nowrap;
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
@@ -170,16 +170,16 @@
|
|||||||
h4 {
|
h4 {
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
display : block;
|
display : block;
|
||||||
flex-basis : 20%;
|
|
||||||
flex-grow : 1;
|
flex-grow : 1;
|
||||||
|
flex-basis : 20%;
|
||||||
min-width : 76px;
|
min-width : 76px;
|
||||||
padding : 5px 0;
|
padding : 5px 0;
|
||||||
color : #BBBBBB;
|
color : #BBBBBB;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
flex-basis : 80%;
|
|
||||||
flex-grow : 1;
|
flex-grow : 1;
|
||||||
|
flex-basis : 80%;
|
||||||
padding : 5px 0;
|
padding : 5px 0;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
@@ -215,10 +215,10 @@
|
|||||||
z-index : 10000;
|
z-index : 10000;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
display : block;
|
display : block;
|
||||||
|
visibility : hidden;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
padding : 13px 5px;
|
padding : 13px 5px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
visibility : hidden;
|
|
||||||
background-color : #333333;
|
background-color : #333333;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,33 +5,45 @@ const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); //
|
|||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
|
||||||
const NewBrew = ()=>{
|
const NewBrew = ()=>{
|
||||||
const handleFileChange = (e)=>{
|
const handleFileChange = (e)=>{
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if(file) {
|
if(!file) return;
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e)=>{
|
const currentNew = localStorage.getItem(BREWKEY);
|
||||||
const fileContent = e.target.result;
|
if(currentNew && !confirm(
|
||||||
const newBrew = {
|
`You have some text in the new brew space, if you load a file that text will be lost, are you sure you want to load the file?`
|
||||||
text : fileContent,
|
)) return;
|
||||||
style : ''
|
|
||||||
};
|
const reader = new FileReader();
|
||||||
if(fileContent.startsWith('```metadata')) {
|
reader.onload = (e)=>{
|
||||||
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
|
const fileContent = e.target.result;
|
||||||
localStorage.setItem(BREWKEY, newBrew.text);
|
const newBrew = { text: fileContent, style: '' };
|
||||||
localStorage.setItem(STYLEKEY, newBrew.style);
|
|
||||||
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
|
if(fileContent.startsWith('```metadata')) {
|
||||||
window.location.href = '/new';
|
splitTextStyleAndMetadata(newBrew);
|
||||||
} else {
|
localStorage.setItem(BREWKEY, newBrew.text);
|
||||||
alert('This file is invalid, please, enter a valid file');
|
localStorage.setItem(STYLEKEY, newBrew.style);
|
||||||
}
|
localStorage.setItem(METAKEY, JSON.stringify(
|
||||||
};
|
_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])
|
||||||
reader.readAsText(file);
|
));
|
||||||
}
|
window.location.href = '/new';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = file.name.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
|
||||||
|
|
||||||
|
|
||||||
|
console.log(file);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
<Nav.item
|
<Nav.item
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ const Moment = require('moment');
|
|||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
const EDIT_KEY = 'homebrewery-recently-edited';
|
const EDIT_KEY = 'HB_nav_recentlyEdited';
|
||||||
const VIEW_KEY = 'homebrewery-recently-viewed';
|
const VIEW_KEY = 'HB_nav_recentlyViewed';
|
||||||
|
|
||||||
|
|
||||||
const RecentItems = createClass({
|
const RecentItems = createClass({
|
||||||
|
|||||||
35
client/homebrew/navbar/share.navitem.jsx
Normal file
35
client/homebrew/navbar/share.navitem.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import dedent from 'dedent-tabs';
|
||||||
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
|
|
||||||
|
const getShareId = (brew)=>(
|
||||||
|
brew.googleId && !brew.stubbed
|
||||||
|
? brew.googleId + brew.shareId
|
||||||
|
: brew.shareId
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRedditLink = (brew)=>{
|
||||||
|
const text = dedent`
|
||||||
|
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||||
|
|
||||||
|
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
|
||||||
|
|
||||||
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ({brew}) => (
|
||||||
|
<Nav.dropdown>
|
||||||
|
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||||
|
share
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' href={`/share/${getShareId(brew)}`}>
|
||||||
|
view
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
|
||||||
|
copy url
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' href={getRedditLink(brew)} newTab rel='noopener noreferrer'>
|
||||||
|
post to reddit
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>
|
||||||
|
);
|
||||||
@@ -13,7 +13,7 @@ const AccountPage = (props)=>{
|
|||||||
// initialize save location from local storage based on user id
|
// initialize save location from local storage based on user id
|
||||||
React.useEffect(()=>{
|
React.useEffect(()=>{
|
||||||
if(!saveLocation && accountDetails.username) {
|
if(!saveLocation && accountDetails.username) {
|
||||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
|
SAVEKEY = `HB_editor_defaultSave_${accountDetails.username}`;
|
||||||
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
|
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
|
||||||
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
let saveLocation = window.localStorage.getItem(SAVEKEY);
|
||||||
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
|
||||||
|
|||||||
@@ -1,183 +1,179 @@
|
|||||||
require('./brewItem.less');
|
require('./brewItem.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useCallback } = React;
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
import request from '../../../../utils/request-middleware.js';
|
import request from '../../../../utils/request-middleware.js';
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
const homebreweryIcon = require('../../../../thumbnail.svg');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = ({
|
||||||
displayName : 'BrewItem',
|
brew = {
|
||||||
getDefaultProps : function() {
|
title : '',
|
||||||
return {
|
description : '',
|
||||||
brew : {
|
authors : [],
|
||||||
title : '',
|
stubbed : true,
|
||||||
description : '',
|
|
||||||
authors : [],
|
|
||||||
stubbed : true
|
|
||||||
},
|
|
||||||
updateListFilter : ()=>{},
|
|
||||||
reportError : ()=>{},
|
|
||||||
renderStorage : true
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
updateListFilter = ()=>{},
|
||||||
|
reportError = ()=>{},
|
||||||
|
renderStorage = true,
|
||||||
|
})=>{
|
||||||
|
|
||||||
deleteBrew : function(){
|
const deleteBrew = useCallback(()=>{
|
||||||
if(this.props.brew.authors.length <= 1){
|
if(brew.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(!window.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(!window.confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||||
} else {
|
} else {
|
||||||
if(!confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
if(!window.confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!window.confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
request.delete(`/api/${brew.googleId ?? ''}${brew.editId}`).send().end((err, res)=>{
|
||||||
.send()
|
if(err) reportError(err); else window.location.reload();
|
||||||
.end((err, res)=>{
|
});
|
||||||
if(err) {
|
}, [brew, reportError]);
|
||||||
this.props.reportError(err);
|
|
||||||
} else {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
updateFilter : function(type, term){
|
const updateFilter = useCallback((type, term)=>updateListFilter(type, term), [updateListFilter]);
|
||||||
this.props.updateListFilter(type, term);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDeleteBrewLink : function(){
|
const renderDeleteBrewLink = ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
if(!brew.editId) return null;
|
||||||
|
|
||||||
return <a className='deleteLink' onClick={this.deleteBrew}>
|
return (
|
||||||
<i className='fas fa-trash-alt' title='Delete' />
|
<a className='deleteLink' onClick={deleteBrew}>
|
||||||
</a>;
|
<i className='fas fa-trash-alt' title='Delete' />
|
||||||
},
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderEditLink : function(){
|
const renderEditLink = ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
if(!brew.editId) return null;
|
||||||
|
|
||||||
let editLink = this.props.brew.editId;
|
let editLink = brew.editId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
if(brew.googleId && !brew.stubbed) editLink = brew.googleId + editLink;
|
||||||
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderShareLink = ()=>{
|
||||||
|
if(!brew.shareId) return null;
|
||||||
|
|
||||||
|
let shareLink = brew.shareId;
|
||||||
|
if(brew.googleId && !brew.stubbed) {
|
||||||
|
shareLink = brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
return (
|
||||||
<i className='fas fa-pencil-alt' title='Edit' />
|
<a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||||
</a>;
|
<i className='fas fa-share-alt' title='Share' />
|
||||||
},
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderShareLink : function(){
|
const renderDownloadLink = ()=>{
|
||||||
if(!this.props.brew.shareId) return;
|
if(!brew.shareId) return null;
|
||||||
|
|
||||||
let shareLink = this.props.brew.shareId;
|
let shareLink = brew.shareId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
if(brew.googleId && !brew.stubbed) {
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
return (
|
||||||
<i className='fas fa-share-alt' title='Share' />
|
<a className='downloadLink' href={`/download/${shareLink}`}>
|
||||||
</a>;
|
<i className='fas fa-download' title='Download' />
|
||||||
},
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderDownloadLink : function(){
|
const renderStorageIcon = ()=>{
|
||||||
if(!this.props.brew.shareId) return;
|
if(!renderStorage) return null;
|
||||||
|
if(brew.googleId) {
|
||||||
let shareLink = this.props.brew.shareId;
|
return (
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
<span title={brew.webViewLink ? 'Your Google Drive Storage' : 'Another User\'s Google Drive Storage'}>
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
<a href={brew.webViewLink} target='_blank'>
|
||||||
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <a className='downloadLink' href={`/download/${shareLink}`}>
|
return (
|
||||||
<i className='fas fa-download' title='Download' />
|
<span title='Homebrewery Storage'>
|
||||||
</a>;
|
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
||||||
},
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
renderStorageIcon : function(){
|
if(Array.isArray(brew.tags)) {
|
||||||
if(!this.props.renderStorage) return;
|
brew.tags = brew.tags?.filter((tag)=>tag); // remove tags that are empty strings
|
||||||
if(this.props.brew.googleId) {
|
brew.tags.sort((a, b)=>{
|
||||||
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
return a.indexOf(':') - b.indexOf(':') !== 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
||||||
<a href={this.props.brew.webViewLink} target='_blank'>
|
});
|
||||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
}
|
||||||
</a>
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span title='Homebrewery Storage'>
|
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||||
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
|
|
||||||
</span>;
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
const brew = this.props.brew;
|
<div className='brewItem'>
|
||||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
{brew.thumbnail && <div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }}></div>}
|
||||||
brew.tags = brew.tags?.filter((tag)=>tag); //remove tags that are empty strings
|
|
||||||
brew.tags.sort((a, b)=>{
|
|
||||||
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
|
||||||
|
|
||||||
return <div className='brewItem'>
|
|
||||||
{brew.thumbnail &&
|
|
||||||
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className='text'>
|
<div className='text'>
|
||||||
<h2>{brew.title}</h2>
|
<h2>{brew.title}</h2>
|
||||||
<p className='description'>{brew.description}</p>
|
<p className='description'>{brew.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
|
{brew.tags?.length ? (
|
||||||
{brew.tags?.length ? <>
|
|
||||||
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
|
||||||
<i className='fas fa-tags'/>
|
<i className='fas fa-tags' />
|
||||||
{brew.tags.map((tag, idx)=>{
|
{brew.tags.map((tag, idx)=>{
|
||||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
return <span key={idx} className={matches[1]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
|
return <span key={idx} className={matches[1]} onClick={()=>updateFilter(tag)}>{matches[2]}</span>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</> : <></>
|
) : null}
|
||||||
}
|
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
<i className='fas fa-user' />{' '}
|
||||||
|
{brew.authors?.map((author, index)=>(
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
{author === 'hidden'
|
{author === 'hidden' ? (
|
||||||
? <span title="Username contained an email address; hidden to protect user's privacy">{author}</span>
|
<span title="Username contained an email address; hidden to protect user's privacy">
|
||||||
: <a href={`/user/${author}`}>{author}</a>
|
{author}
|
||||||
}
|
</span>
|
||||||
|
) : (<a href={`/user/${author}`}>{author}</a>)}
|
||||||
{index < brew.authors.length - 1 && ', '}
|
{index < brew.authors.length - 1 && ', '}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
<i className='fas fa-eye'/> {brew.views}
|
<i className='fas fa-eye' /> {brew.views}
|
||||||
</span>
|
</span>
|
||||||
{brew.pageCount &&
|
{brew.pageCount && (
|
||||||
<span title={`Page count: ${brew.pageCount}`}>
|
<span title={`Page count: ${brew.pageCount}`}>
|
||||||
<i className='far fa-file' /> {brew.pageCount}
|
<i className='far fa-file' /> {brew.pageCount}
|
||||||
</span>
|
</span>
|
||||||
}
|
)}
|
||||||
<span title={dedent`
|
<span
|
||||||
Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
title={dedent` Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
||||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}
|
||||||
|
>
|
||||||
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
|
||||||
</span>
|
</span>
|
||||||
{this.renderStorageIcon()}
|
{renderStorageIcon()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='links'>
|
<div className='links'>
|
||||||
{this.renderShareLink()}
|
{renderShareLink()}
|
||||||
{this.renderEditLink()}
|
{renderEditLink()}
|
||||||
{this.renderDownloadLink()}
|
{renderDownloadLink()}
|
||||||
{this.renderDeleteBrewLink()}
|
{renderDeleteBrewLink()}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = BrewItem;
|
module.exports = BrewItem;
|
||||||
|
|||||||
@@ -1,148 +1,129 @@
|
|||||||
|
|
||||||
.brewItem{
|
.brewItem {
|
||||||
position : relative;
|
position : relative;
|
||||||
|
box-sizing : border-box;
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
vertical-align : top;
|
|
||||||
box-sizing : border-box;
|
|
||||||
box-sizing : border-box;
|
|
||||||
overflow : hidden;
|
|
||||||
width : 48%;
|
width : 48%;
|
||||||
min-height : 105px;
|
min-height : 105px;
|
||||||
margin-right : 15px;
|
|
||||||
margin-bottom : 15px;
|
|
||||||
padding : 5px 15px 2px 6px;
|
padding : 5px 15px 2px 6px;
|
||||||
padding-right : 15px;
|
padding-right : 15px;
|
||||||
border : 1px solid #c9ad6a;
|
margin-right : 15px;
|
||||||
|
margin-bottom : 15px;
|
||||||
|
overflow : hidden;
|
||||||
|
vertical-align : top;
|
||||||
|
background-color : #CAB2802E;
|
||||||
|
border : 1px solid #C9AD6A;
|
||||||
border-radius : 5px;
|
border-radius : 5px;
|
||||||
|
box-shadow : 0px 4px 5px 0px #333333;
|
||||||
|
break-inside : avoid;
|
||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
.thumbnail {
|
||||||
box-shadow : 0px 4px 5px 0px #333;
|
position : absolute;
|
||||||
background-color : #cab2802e;
|
top : 0;
|
||||||
.thumbnail {
|
right : 0;
|
||||||
position: absolute;
|
z-index : -1;
|
||||||
width: 150px;
|
width : 150px;
|
||||||
height: 100%;
|
height : 100%;
|
||||||
top: 0;
|
background-repeat : no-repeat;
|
||||||
right: 0;
|
background-position : right top;
|
||||||
z-index: -1;
|
background-size : contain;
|
||||||
background-size: contain;
|
opacity : 50%;
|
||||||
background-repeat: no-repeat;
|
-webkit-mask-image : linear-gradient(80deg, #00000000 20%, #005500 40%);
|
||||||
background-position: right top;
|
mask-image : linear-gradient(80deg, #00000000 20%, #005500 40%);
|
||||||
mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
|
||||||
-webkit-mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
}
|
||||||
.text {
|
.text {
|
||||||
min-height : 54px;
|
min-height : 54px;
|
||||||
h4{
|
h4 {
|
||||||
margin-bottom : 5px;
|
margin-bottom : 5px;
|
||||||
font-size : 2.2em;
|
font-size : 2.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.info{
|
.info {
|
||||||
position: initial;
|
position : initial;
|
||||||
bottom: 2px;
|
bottom : 2px;
|
||||||
font-family : ScalySansRemake;
|
font-family : "ScalySansRemake";
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
&>span{
|
& > span {
|
||||||
margin-right : 12px;
|
margin-right : 12px;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
|
|
||||||
a {
|
a { color : inherit; }
|
||||||
color:inherit;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.brewTags span {
|
.brewTags span {
|
||||||
background-color: #c8ac6e3b;
|
display : inline-block;
|
||||||
margin: 2px;
|
padding : 2px;
|
||||||
padding: 2px;
|
margin : 2px;
|
||||||
border: 1px solid #c8ac6e;
|
font-weight : bold;
|
||||||
border-radius: 4px;
|
white-space : nowrap;
|
||||||
white-space: nowrap;
|
cursor : pointer;
|
||||||
display: inline-block;
|
background-color : #C8AC6E3B;
|
||||||
font-weight: bold;
|
border : 1px solid #C8AC6E;
|
||||||
border-color: currentColor;
|
border-color : currentColor;
|
||||||
cursor : pointer;
|
border-radius : 4px;
|
||||||
&:before {
|
&::before {
|
||||||
font-family: 'Font Awesome 5 Free';
|
margin-right : 3px;
|
||||||
font-size: 12px;
|
font-family : 'Font Awesome 6 Free';
|
||||||
margin-right: 3px;
|
font-size : 12px;
|
||||||
}
|
}
|
||||||
&.type {
|
&.type {
|
||||||
background-color: #0080003b;
|
color : #008000;
|
||||||
color: #008000;
|
background-color : #0080003B;
|
||||||
&:before{
|
&::before { content : '\f0ad'; }
|
||||||
content: '\f0ad';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.group {
|
&.group {
|
||||||
background-color: #5050503b;
|
color : #000000;
|
||||||
color: #000000;
|
background-color : #5050503B;
|
||||||
&:before{
|
&::before { content : '\f500'; }
|
||||||
content: '\f500';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.meta {
|
&.meta {
|
||||||
background-color: #0000803b;
|
color : #000080;
|
||||||
color: #000080;
|
background-color : #0000803B;
|
||||||
&:before{
|
&::before { content : '\f05a'; }
|
||||||
content: '\f05a';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.system {
|
&.system {
|
||||||
background-color: #8000003b;
|
color : #800000;
|
||||||
color: #800000;
|
background-color : #8000003B;
|
||||||
&:before{
|
&::before { content : '\f518'; }
|
||||||
content: '\f518';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover {
|
||||||
.links{
|
.links { opacity : 1; }
|
||||||
opacity : 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&:nth-child(2n + 1){
|
&:nth-child(2n + 1) { margin-right : 0px; }
|
||||||
margin-right : 0px;
|
.links {
|
||||||
}
|
|
||||||
.links{
|
|
||||||
.animate(opacity);
|
.animate(opacity);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 0px;
|
top : 0px;
|
||||||
right : 0px;
|
right : 0px;
|
||||||
height : 100%;
|
|
||||||
width : 2em;
|
width : 2em;
|
||||||
opacity : 0;
|
height : 100%;
|
||||||
background-color : fade(black, 60%);
|
|
||||||
text-align : center;
|
text-align : center;
|
||||||
a{
|
background-color : fade(black, 60%);
|
||||||
|
opacity : 0;
|
||||||
|
a {
|
||||||
.animate(opacity);
|
.animate(opacity);
|
||||||
display : block;
|
display : block;
|
||||||
margin : 8px 0px;
|
margin : 8px 0px;
|
||||||
opacity : 0.6;
|
|
||||||
font-size : 1.3em;
|
font-size : 1.3em;
|
||||||
color : white;
|
color : white;
|
||||||
text-decoration : unset;
|
text-decoration : unset;
|
||||||
&:hover{
|
opacity : 0.6;
|
||||||
opacity : 1;
|
&:hover { opacity : 1; }
|
||||||
}
|
i { cursor : pointer; }
|
||||||
i{
|
|
||||||
cursor : pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.googleDriveIcon {
|
.googleDriveIcon {
|
||||||
height : 18px;
|
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
|
height : 18px;
|
||||||
}
|
}
|
||||||
.homebreweryIcon {
|
.homebreweryIcon {
|
||||||
mix-blend-mode : darken;
|
position : relative;
|
||||||
height : 24px;
|
padding : 0px;
|
||||||
position : relative;
|
top : 5px;
|
||||||
top : 5px;
|
left : -7.5px;
|
||||||
left : -5px;
|
height : 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ const moment = require('moment');
|
|||||||
|
|
||||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||||
|
|
||||||
const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
|
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir';
|
||||||
|
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
|
||||||
|
const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
|
||||||
|
|
||||||
const DEFAULT_SORT_TYPE = 'alpha';
|
const DEFAULT_SORT_TYPE = 'alpha';
|
||||||
const DEFAULT_SORT_DIR = 'asc';
|
const DEFAULT_SORT_DIR = 'asc';
|
||||||
@@ -50,12 +52,12 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
// LOAD FROM LOCAL STORAGE
|
// LOAD FROM LOCAL STORAGE
|
||||||
if(typeof window !== 'undefined') {
|
if(typeof window !== 'undefined') {
|
||||||
const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
|
const newSortType = (this.state.sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE));
|
||||||
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
|
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR));
|
||||||
this.updateUrl(this.state.filterString, newSortType, newSortDir);
|
this.updateUrl(this.state.filterString, newSortType, newSortDir);
|
||||||
|
|
||||||
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||||
brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
|
brewGroup.visible = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true')=='true';
|
||||||
return brewGroup;
|
return brewGroup;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,10 +75,10 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
saveToLocalStorage : function() {
|
saveToLocalStorage : function() {
|
||||||
this.state.brewCollection.map((brewGroup)=>{
|
this.state.brewCollection.map((brewGroup)=>{
|
||||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
|
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${brewGroup.visible}`);
|
||||||
});
|
});
|
||||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
|
localStorage.setItem(USERPAGE_SORT_TYPE, this.state.sortType);
|
||||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
|
localStorage.setItem(USERPAGE_SORT_DIR, this.state.sortDir);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderBrews : function(brews){
|
renderBrews : function(brews){
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
.noColumns(){
|
.noColumns() {
|
||||||
column-count : auto;
|
column-count : auto;
|
||||||
column-fill : auto;
|
column-fill : auto;
|
||||||
column-gap : normal;
|
column-gap : normal;
|
||||||
@@ -13,177 +13,151 @@
|
|||||||
height : auto;
|
height : auto;
|
||||||
min-height : 279.4mm;
|
min-height : 279.4mm;
|
||||||
margin : 20px auto;
|
margin : 20px auto;
|
||||||
contain : unset;
|
contain : unset;
|
||||||
}
|
}
|
||||||
.listPage{
|
.listPage {
|
||||||
.content{
|
.content {
|
||||||
z-index : 1;
|
z-index : 1;
|
||||||
.page{
|
.page {
|
||||||
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
|
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
|
||||||
&::after{
|
&::after { display : none; }
|
||||||
display : none;
|
.noBrews {
|
||||||
}
|
|
||||||
.noBrews{
|
|
||||||
margin : 10px 0px;
|
margin : 10px 0px;
|
||||||
font-size : 1.3em;
|
font-size : 1.3em;
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
}
|
}
|
||||||
.brewCollection {
|
.brewCollection {
|
||||||
h1:hover{
|
h1:hover { cursor : pointer; }
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.active::before, .inactive::before {
|
.active::before, .inactive::before {
|
||||||
font-family: 'Font Awesome 5 Free';
|
padding-right : 0.5em;
|
||||||
font-weight: 900;
|
font-family : 'Font Awesome 6 Free';
|
||||||
font-size: 0.6cm;
|
font-size : 0.6cm;
|
||||||
padding-right: 0.5em;
|
font-weight : 900;
|
||||||
}
|
|
||||||
.active {
|
|
||||||
color: var(--HB_Color_HeaderText);
|
|
||||||
}
|
|
||||||
.active::before {
|
|
||||||
content: '\f107';
|
|
||||||
}
|
|
||||||
.inactive {
|
|
||||||
color: #707070;
|
|
||||||
}
|
|
||||||
.inactive::before {
|
|
||||||
content: '\f105';
|
|
||||||
}
|
}
|
||||||
|
.active { color : var(--HB_Color_HeaderText); }
|
||||||
|
.active::before { content : '\f107'; }
|
||||||
|
.inactive { color : #707070; }
|
||||||
|
.inactive::before { content : '\f105'; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sort-container {
|
.sort-container {
|
||||||
font-family : 'Open Sans', sans-serif;
|
position : sticky;
|
||||||
position : sticky;
|
top : 0;
|
||||||
top : 0;
|
left : 0;
|
||||||
left : 0;
|
z-index : 1;
|
||||||
width : 100%;
|
display : flex;
|
||||||
height : 30px;
|
flex-wrap : wrap;
|
||||||
background-color : #555;
|
row-gap : 5px;
|
||||||
border-top : 1px solid #666;
|
column-gap : 15px;
|
||||||
border-bottom : 1px solid #666;
|
align-items : baseline;
|
||||||
color : white;
|
justify-content : center;
|
||||||
text-align : center;
|
width : 100%;
|
||||||
z-index : 1;
|
height : 30px;
|
||||||
display : flex;
|
font-family : 'Open Sans', sans-serif;
|
||||||
justify-content : center;
|
color : white;
|
||||||
align-items : baseline;
|
text-align : center;
|
||||||
column-gap : 15px;
|
background-color : #555555;
|
||||||
row-gap : 5px;
|
border-top : 1px solid #666666;
|
||||||
flex-wrap : wrap;
|
border-bottom : 1px solid #666666;
|
||||||
h6{
|
h6 {
|
||||||
text-transform : uppercase;
|
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
font-size : 11px;
|
font-size : 11px;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
|
text-transform : uppercase;
|
||||||
}
|
}
|
||||||
.sort-option {
|
.sort-option {
|
||||||
display: flex;
|
display : flex;
|
||||||
align-items: center;
|
align-items : center;
|
||||||
padding: 0 8px;
|
height : 100%;
|
||||||
color: #ccc;
|
padding : 0 8px;
|
||||||
height: 100%;
|
color : #CCCCCC;
|
||||||
|
|
||||||
&:hover{
|
&:hover { background-color : #444444; }
|
||||||
background-color : #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
font-weight: bold;
|
font-weight : bold;
|
||||||
color: #ddd;
|
color : #DDDDDD;
|
||||||
background-color: #333;
|
background-color : #333333;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
color: white;
|
height : 100%;
|
||||||
font-weight: 800;
|
font-weight : 800;
|
||||||
height: 100%;
|
color : white;
|
||||||
& + .sortDir {
|
& + .sortDir { padding-left : 5px; }
|
||||||
padding-left: 5px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.filter-option {
|
.filter-option {
|
||||||
margin-left: 20px;
|
margin-left : 20px;
|
||||||
background-color : transparent !important;
|
|
||||||
font-size : 11px;
|
font-size : 11px;
|
||||||
i{
|
background-color : transparent !important;
|
||||||
padding-right : 5px;
|
i { padding-right : 5px; }
|
||||||
}
|
}
|
||||||
|
button {
|
||||||
|
padding : 0;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 11px;
|
||||||
|
font-weight : normal;
|
||||||
|
color : #CCCCCC;
|
||||||
|
text-transform : uppercase;
|
||||||
|
background-color : transparent;
|
||||||
}
|
}
|
||||||
button{
|
|
||||||
background-color : transparent;
|
|
||||||
font-family : 'Open Sans', sans-serif;
|
|
||||||
text-transform : uppercase;
|
|
||||||
font-weight : normal;
|
|
||||||
font-size : 11px;
|
|
||||||
color : #ccc;
|
|
||||||
padding : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.tags-container {
|
.tags-container {
|
||||||
height : 30px;
|
|
||||||
background-color : #555;
|
|
||||||
border-top : 1px solid #666;
|
|
||||||
border-bottom : 1px solid #666;
|
|
||||||
color : white;
|
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : center;
|
|
||||||
align-items : center;
|
|
||||||
column-gap : 15px;
|
|
||||||
row-gap : 5px;
|
|
||||||
flex-wrap : wrap;
|
flex-wrap : wrap;
|
||||||
|
row-gap : 5px;
|
||||||
|
column-gap : 15px;
|
||||||
|
align-items : center;
|
||||||
|
justify-content : center;
|
||||||
|
height : 30px;
|
||||||
|
color : white;
|
||||||
|
background-color : #555555;
|
||||||
|
border-top : 1px solid #666666;
|
||||||
|
border-bottom : 1px solid #666666;
|
||||||
span {
|
span {
|
||||||
|
padding : 3px;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
font-size : 11px;
|
font-size : 11px;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
|
color : #DFDFDF;
|
||||||
|
cursor : pointer;
|
||||||
border : 1px solid;
|
border : 1px solid;
|
||||||
border-radius : 3px;
|
border-radius : 3px;
|
||||||
padding : 3px;
|
&::before {
|
||||||
cursor : pointer;
|
margin-right : 3px;
|
||||||
color: #dfdfdf;
|
font-family : 'Font Awesome 6 Free';
|
||||||
&:before {
|
font-size : 12px;
|
||||||
font-family: 'Font Awesome 5 Free';
|
|
||||||
font-size: 12px;
|
|
||||||
margin-right: 3px;
|
|
||||||
}
|
}
|
||||||
&:after {
|
&::after {
|
||||||
content: '\f00d';
|
margin-left : 3px;
|
||||||
font-family: 'Font Awesome 5 Free';
|
font-family : 'Font Awesome 6 Free';
|
||||||
font-size: 12px;
|
font-size : 12px;
|
||||||
margin-left: 3px;
|
content : '\f00d';
|
||||||
}
|
}
|
||||||
&.type {
|
&.type {
|
||||||
background-color: #008000;
|
background-color : #008000;
|
||||||
border-color: #00a000;
|
border-color : #00A000;
|
||||||
&:before{
|
&::before { content : '\f0ad'; }
|
||||||
content: '\f0ad';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.group {
|
&.group {
|
||||||
background-color: #505050;
|
background-color : #505050;
|
||||||
border-color: #000000;
|
border-color : #000000;
|
||||||
&:before{
|
&::before { content : '\f500'; }
|
||||||
content: '\f500';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.meta {
|
&.meta {
|
||||||
background-color: #000080;
|
background-color : #000080;
|
||||||
border-color: #0000a0;
|
border-color : #0000A0;
|
||||||
&:before{
|
&::before { content : '\f05a'; }
|
||||||
content: '\f05a';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.system {
|
&.system {
|
||||||
background-color: #800000;
|
background-color : #800000;
|
||||||
border-color: #a00000;
|
border-color : #A00000;
|
||||||
&:before{
|
&::before { content : '\f518'; }
|
||||||
content: '\f518';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.homebrew {
|
.homebrew {
|
||||||
.uiPage.sitePage {
|
.uiPage.sitePage {
|
||||||
.content {
|
.content {
|
||||||
width : ~"min(90vw, 1000px)";
|
width : ~'min(90vw, 1000px)';
|
||||||
padding : 2% 4%;
|
padding : 2% 4%;
|
||||||
margin-top : 25px;
|
margin-top : 25px;
|
||||||
margin-right : auto;
|
margin-right : auto;
|
||||||
@@ -17,19 +17,20 @@
|
|||||||
border : 2px solid black;
|
border : 2px solid black;
|
||||||
border-radius : 5px;
|
border-radius : 5px;
|
||||||
button {
|
button {
|
||||||
|
width : 125px;
|
||||||
|
margin-right : 5px;
|
||||||
|
color : black;
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
border : 1px solid black;
|
border : 1px solid black;
|
||||||
border-radius : 5px;
|
border-radius : 5px;
|
||||||
width : 125px;
|
|
||||||
color : black;
|
|
||||||
margin-right : 5px;
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #0007;
|
color : white;
|
||||||
color: white;
|
background-color : #00000077;
|
||||||
&:before {
|
&::before {
|
||||||
content: '\f00c';
|
margin-right : 5px;
|
||||||
font-family: 'FONT AWESOME 5 FREE';
|
font-family : 'Font Awesome 6 Free';
|
||||||
margin-right: 5px;
|
font-weight : 900;
|
||||||
|
content : '\f00c';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,474 +1,411 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
require('./editPage.less');
|
import './editPage.less';
|
||||||
const React = require('react');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
|
|
||||||
import request from '../../utils/request-middleware.js';
|
// Common imports
|
||||||
const { Meta } = require('vitreum/headtags');
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import request from '../../utils/request-middleware.js';
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||||
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
import Editor from '../../editor/editor.jsx';
|
||||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
const Editor = require('../../editor/editor.jsx');
|
import Navbar from '../../navbar/navbar.jsx';
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
||||||
|
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||||
|
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||||
|
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||||
|
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
||||||
|
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
||||||
|
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||||
|
|
||||||
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
// Page specific imports
|
||||||
|
import { Meta } from 'vitreum/headtags';
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
import _ from 'lodash';
|
||||||
|
import { md5 } from 'hash-wasm';
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
import { gzipSync, strToU8 } from 'fflate';
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
|
||||||
|
|
||||||
|
import ShareNavItem from '../../navbar/share.navitem.jsx';
|
||||||
|
import LockNotification from './lockNotification/lockNotification.jsx';
|
||||||
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
||||||
|
import googleDriveIcon from '../../googleDrive.svg';
|
||||||
const googleDriveIcon = require('../../googleDrive.svg');
|
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 10000;
|
const SAVE_TIMEOUT = 10000;
|
||||||
|
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
|
||||||
|
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
|
||||||
|
|
||||||
const EditPage = createClass({
|
|
||||||
displayName : 'EditPage',
|
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
brew : DEFAULT_BREW_LOAD
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
|
||||||
return {
|
const BREWKEY = 'HB_newPage_content';
|
||||||
brew : this.props.brew,
|
const STYLEKEY = 'HB_newPage_style';
|
||||||
isSaving : false,
|
const SNIPKEY = 'HB_newPage_snippets';
|
||||||
isPending : false,
|
const METAKEY = 'HB_newPage_meta';
|
||||||
alertTrashedGoogleBrew : this.props.brew.trashed,
|
|
||||||
alertLoginToTransfer : false,
|
|
||||||
saveGoogle : this.props.brew.googleId ? true : false,
|
|
||||||
confirmGoogleTransfer : false,
|
|
||||||
error : null,
|
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
|
||||||
url : '',
|
|
||||||
autoSave : true,
|
|
||||||
autoSaveWarning : false,
|
|
||||||
unsavedTime : new Date(),
|
|
||||||
currentEditorViewPageNum : 1,
|
|
||||||
currentEditorCursorPageNum : 1,
|
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
displayLockMessage : this.props.brew.lock || false,
|
|
||||||
themeBundle : {}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
editor : React.createRef(null),
|
|
||||||
savedBrew : null,
|
|
||||||
|
|
||||||
componentDidMount : function(){
|
const useLocalStorage = false;
|
||||||
this.setState({
|
|
||||||
url : window.location.href
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
const EditPage = (props)=>{
|
||||||
|
props = {
|
||||||
|
brew : DEFAULT_BREW_LOAD,
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
|
||||||
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
const [currentBrew , setCurrentBrew ] = useState(props.brew);
|
||||||
if(this.state.autoSave){
|
const [isSaving , setIsSaving ] = useState(false);
|
||||||
this.trySave();
|
const [lastSavedTime , setLastSavedTime ] = useState(new Date());
|
||||||
} else {
|
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId);
|
||||||
this.setState({ autoSaveWarning: true });
|
const [error , setError ] = useState(null);
|
||||||
|
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
|
||||||
|
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
|
||||||
|
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||||
|
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||||
|
const [themeBundle , setThemeBundle ] = useState({});
|
||||||
|
const [unsavedChanges , setUnsavedChanges ] = useState(false);
|
||||||
|
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
|
||||||
|
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
|
||||||
|
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
|
||||||
|
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
|
||||||
|
const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true);
|
||||||
|
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
|
||||||
|
const saveTimeout = useRef(null);
|
||||||
|
const warnUnsavedTimeout = useRef(null);
|
||||||
|
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
|
||||||
|
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
|
||||||
|
setAutoSaveEnabled(autoSavePref);
|
||||||
|
setWarnUnsavedChanges(!autoSavePref);
|
||||||
|
setHTMLErrors(Markdown.validate(currentBrew.text));
|
||||||
|
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||||
|
|
||||||
|
const handleControlKeys = (e)=>{
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
if(e.keyCode === 83) trySaveRef.current(true);
|
||||||
|
if(e.keyCode === 80) printCurrentBrew();
|
||||||
|
if([83, 80].includes(e.keyCode)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
window.onbeforeunload = ()=>{
|
window.onbeforeunload = ()=>{
|
||||||
if(this.state.isSaving || this.state.isPending){
|
if(unsavedChangesRef.current)
|
||||||
return 'You have unsaved changes!';
|
return 'You have unsaved changes!';
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
return ()=>{
|
||||||
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
|
window.onBeforeUnload = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
useEffect(()=>{
|
||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
trySaveRef.current = trySave;
|
||||||
}));
|
unsavedChangesRef.current = unsavedChanges;
|
||||||
|
});
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
useEffect(()=>{
|
||||||
|
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
|
||||||
|
setUnsavedChanges(hasChange);
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
if(autoSaveEnabled) trySave(false, hasChange);
|
||||||
},
|
}, [currentBrew]);
|
||||||
componentWillUnmount : function() {
|
|
||||||
window.onbeforeunload = function(){};
|
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
const handleSplitMove = ()=>{
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
editorRef.current?.update();
|
||||||
const S_KEY = 83;
|
};
|
||||||
const P_KEY = 80;
|
|
||||||
if(e.keyCode == S_KEY) this.trySave(true);
|
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
||||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
if (subfield == 'renderer' || subfield == 'theme')
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||||
|
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||||
|
setHTMLErrors(Markdown.validate(value));
|
||||||
|
|
||||||
|
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
|
||||||
|
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
|
if(useLocalStorage) {
|
||||||
|
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||||
|
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||||
|
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||||
|
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||||
|
renderer : value.renderer,
|
||||||
|
theme : value.theme,
|
||||||
|
lang : value.lang
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
handleSplitMove : function(){
|
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
|
||||||
this.editor.current.update();
|
...prevBrew,
|
||||||
},
|
style : newData.style,
|
||||||
|
text : newData.text,
|
||||||
|
snippets : newData.snippets
|
||||||
|
}));
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
const resetWarnUnsavedTimer = ()=>{
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
|
||||||
},
|
clearTimeout(warnUnsavedTimeout.current);
|
||||||
|
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
|
||||||
|
};
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
const handleGoogleClick = ()=>{
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
|
||||||
//If there are errors, run the validator on every change to give quick feedback
|
|
||||||
let htmlErrors = this.state.htmlErrors;
|
|
||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : { ...prevState.brew, text: text },
|
|
||||||
isPending : true,
|
|
||||||
htmlErrors : htmlErrors,
|
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleStyleChange : function(style){
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : { ...prevState.brew, style: style },
|
|
||||||
isPending : true
|
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMetaChange : function(metadata, field=undefined){
|
|
||||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
|
||||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : {
|
|
||||||
...prevState.brew,
|
|
||||||
...metadata
|
|
||||||
},
|
|
||||||
isPending : true,
|
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
|
||||||
},
|
|
||||||
|
|
||||||
hasChanges : function(){
|
|
||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateBrew : function(newData){
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : {
|
|
||||||
...prevState.brew,
|
|
||||||
style : newData.style,
|
|
||||||
text : newData.text
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
trySave : function(immediate=false){
|
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
|
||||||
if(this.hasChanges()){
|
|
||||||
this.debounceSave();
|
|
||||||
} else {
|
|
||||||
this.debounceSave.cancel();
|
|
||||||
}
|
|
||||||
if(immediate) this.debounceSave.flush();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleGoogleClick : function(){
|
|
||||||
if(!global.account?.googleId) {
|
if(!global.account?.googleId) {
|
||||||
this.setState({
|
setAlertLoginToTransfer(true);
|
||||||
alertLoginToTransfer : true
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState((prevState)=>({
|
|
||||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
|
||||||
}));
|
|
||||||
this.setState({
|
|
||||||
error : null,
|
|
||||||
isSaving : false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
closeAlerts : function(event){
|
setConfirmGoogleTransfer((prev)=>!prev);
|
||||||
event.stopPropagation(); //Only handle click once so alert doesn't reopen
|
setError(null);
|
||||||
this.setState({
|
};
|
||||||
alertTrashedGoogleBrew : false,
|
|
||||||
alertLoginToTransfer : false,
|
|
||||||
confirmGoogleTransfer : false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleGoogleStorage : function(){
|
const closeAlerts = (e)=>{
|
||||||
this.setState((prevState)=>({
|
e.stopPropagation(); //Only handle click once so alert doesn't reopen
|
||||||
saveGoogle : !prevState.saveGoogle,
|
setAlertTrashedGoogleBrew(false);
|
||||||
isSaving : false,
|
setAlertLoginToTransfer(false);
|
||||||
error : null
|
setConfirmGoogleTransfer(false);
|
||||||
}), ()=>this.save());
|
};
|
||||||
},
|
|
||||||
|
|
||||||
save : async function(){
|
const toggleGoogleStorage = ()=>{
|
||||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
setSaveGoogle((prev)=>!prev);
|
||||||
|
setError(null);
|
||||||
|
trySave(true);
|
||||||
|
};
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
const trySave = (immediate = false, hasChanges = true)=>{
|
||||||
isSaving : true,
|
clearTimeout(saveTimeout.current);
|
||||||
error : null,
|
if(isSaving) return;
|
||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
if(!hasChanges && !immediate) return;
|
||||||
}));
|
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
|
||||||
|
|
||||||
await updateHistory(this.state.brew).catch(console.error);
|
saveTimeout.current = setTimeout(async ()=>{
|
||||||
|
setIsSaving(true);
|
||||||
|
setError(null);
|
||||||
|
await save(currentBrew, saveGoogle)
|
||||||
|
.catch((err)=>{
|
||||||
|
setError(err);
|
||||||
|
});
|
||||||
|
setIsSaving(false);
|
||||||
|
setLastSavedTime(new Date());
|
||||||
|
if(!autoSaveEnabled) resetWarnUnsavedTimer();
|
||||||
|
}, newTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async (brew, saveToGoogle)=>{
|
||||||
|
setHTMLErrors(Markdown.validate(brew.text));
|
||||||
|
|
||||||
|
await updateHistory(brew).catch(console.error);
|
||||||
await versionHistoryGarbageCollection().catch(console.error);
|
await versionHistoryGarbageCollection().catch(console.error);
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
//Prepare content to send to server
|
||||||
|
const brewToSave = {
|
||||||
|
...brew,
|
||||||
|
text : brew.text.normalize('NFC'),
|
||||||
|
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
|
||||||
|
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
|
||||||
|
hash : await md5(lastSavedBrew.current.text),
|
||||||
|
textBin : undefined,
|
||||||
|
version : lastSavedBrew.current.version
|
||||||
|
};
|
||||||
|
|
||||||
const brew = this.state.brew;
|
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave)));
|
||||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
const transfer = saveToGoogle === _.isNil(brew.googleId);
|
||||||
|
const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : '';
|
||||||
|
|
||||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.put(`/api/update/${brew.editId}${params}`)
|
.put(`/api/update/${brewToSave.editId}${params}`)
|
||||||
.send(brew)
|
.set('Content-Encoding', 'gzip')
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.send(compressedBrew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error Updating Local Brew');
|
console.error('Error Updating Local Brew');
|
||||||
this.setState({ error: err });
|
setError(err);
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
const updatedFields = {
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
googleId : res.body.googleId ?? null,
|
||||||
|
editId : res.body.editId,
|
||||||
|
shareId : res.body.shareId,
|
||||||
|
version : res.body.version
|
||||||
|
};
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
lastSavedBrew.current = {
|
||||||
brew : { ...prevState.brew,
|
...brew,
|
||||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
...updatedFields
|
||||||
editId : this.savedBrew.editId,
|
};
|
||||||
shareId : this.savedBrew.shareId,
|
|
||||||
version : this.savedBrew.version
|
setCurrentBrew((prevBrew)=>({
|
||||||
},
|
...prevBrew,
|
||||||
isPending : false,
|
...updatedFields
|
||||||
isSaving : false,
|
|
||||||
unsavedTime : new Date()
|
|
||||||
}));
|
}));
|
||||||
},
|
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
history.replaceState(null, null, `/edit/${res.body.editId}`);
|
||||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
};
|
||||||
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
|
|
||||||
|
|
||||||
{this.state.confirmGoogleTransfer &&
|
const renderGoogleDriveIcon = ()=>(
|
||||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}>
|
||||||
{ this.state.saveGoogle
|
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' />
|
||||||
? `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?`
|
{confirmGoogleTransfer && (
|
||||||
}
|
<div className='errorContainer' onClick={closeAlerts}>
|
||||||
|
{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 />
|
<br />
|
||||||
<div className='confirm' onClick={this.toggleGoogleStorage}>
|
<div className='confirm' onClick={toggleGoogleStorage}> Yes </div>
|
||||||
Yes
|
<div className='deny'> No </div>
|
||||||
</div>
|
|
||||||
<div className='deny'>
|
|
||||||
No
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{this.state.alertLoginToTransfer &&
|
{alertLoginToTransfer && (
|
||||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
<div className='errorContainer' onClick={closeAlerts}>
|
||||||
You must be signed in to a Google account to transfer
|
You must be signed in to a Google account to transfer between the homebrewery and Google Drive!
|
||||||
between the homebrewery and Google Drive!
|
<a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
<div className='confirm'> Sign In </div>
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
<div className='confirm'>
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<div className='deny'>
|
<div className='deny'> Not Now </div>
|
||||||
Not Now
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{this.state.alertTrashedGoogleBrew &&
|
{alertTrashedGoogleBrew && (
|
||||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
<div className='errorContainer' onClick={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 />
|
This brew is currently in your Trash folder on Google Drive!<br />
|
||||||
<div className='confirm'>
|
If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||||
OK
|
<div className='confirm'> OK </div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</Nav.item>;
|
</Nav.item>
|
||||||
},
|
);
|
||||||
|
|
||||||
renderSaveButton : function(){
|
const renderSaveButton = ()=>{
|
||||||
if(this.state.autoSaveWarning && this.hasChanges()){
|
// #1 - Currently saving, show SAVING
|
||||||
this.setAutosaveWarning();
|
if(isSaving)
|
||||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
|
||||||
|
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
|
||||||
|
if(unsavedChanges && warnUnsavedChanges) {
|
||||||
|
resetWarnUnsavedTimer();
|
||||||
|
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
|
||||||
|
const text = elapsedTime === 0
|
||||||
|
? 'Autosave is OFF.'
|
||||||
|
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
||||||
Reminder...
|
Reminder...
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>{text}</div>
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.state.isSaving){
|
// #3 - Unsaved changes exist, click to save, show SAVE NOW
|
||||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
if(unsavedChanges)
|
||||||
}
|
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||||
if(this.state.isPending && this.hasChanges()){
|
|
||||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
|
||||||
}
|
if(autoSaveEnabled)
|
||||||
if(!this.state.isPending && !this.state.isSaving && this.state.autoSave){
|
|
||||||
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
|
||||||
}
|
|
||||||
if(!this.state.isPending && !this.state.isSaving){
|
|
||||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleAutoSave : function(){
|
// DEFAULT - No unsaved changes, show SAVED
|
||||||
if(this.warningTimer) clearTimeout(this.warningTimer);
|
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||||
this.setState((prevState)=>({
|
};
|
||||||
autoSave : !prevState.autoSave,
|
|
||||||
autoSaveWarning : prevState.autoSave
|
|
||||||
}), ()=>{
|
|
||||||
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
setAutosaveWarning : function(){
|
const toggleAutoSave = ()=>{
|
||||||
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
|
clearTimeout(warnUnsavedTimeout.current);
|
||||||
this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
|
clearTimeout(saveTimeout.current);
|
||||||
this.warningTimer;
|
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
|
||||||
},
|
setAutoSaveEnabled(!autoSaveEnabled);
|
||||||
|
setWarnUnsavedChanges(autoSaveEnabled);
|
||||||
|
};
|
||||||
|
|
||||||
errorReported : function(error) {
|
const renderAutoSaveButton = ()=>(
|
||||||
this.setState({
|
<Nav.item onClick={toggleAutoSave}>
|
||||||
error
|
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||||
});
|
</Nav.item>
|
||||||
},
|
);
|
||||||
|
|
||||||
renderAutoSaveButton : function(){
|
const clearError = ()=>{
|
||||||
return <Nav.item onClick={this.handleAutoSave}>
|
setError(null);
|
||||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
setIsSaving(false);
|
||||||
</Nav.item>;
|
};
|
||||||
},
|
|
||||||
|
|
||||||
processShareId : function() {
|
|
||||||
return this.state.brew.googleId && !this.state.brew.stubbed ?
|
|
||||||
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](${global.config.publicUrl}/share/${shareLink})**`;
|
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderNavbar : function(){
|
|
||||||
const shareLink = this.processShareId();
|
|
||||||
|
|
||||||
|
const renderNavbar = ()=>{
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderGoogleDriveIcon()}
|
{renderGoogleDriveIcon()}
|
||||||
{this.state.error ?
|
{error
|
||||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
? <ErrorNavItem error={error} clearError={clearError} />
|
||||||
<Nav.dropdown className='save-menu'>
|
: <Nav.dropdown className='save-menu'>
|
||||||
{this.renderSaveButton()}
|
{renderSaveButton()}
|
||||||
{this.renderAutoSaveButton()}
|
{renderAutoSaveButton()}
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>}
|
||||||
}
|
<NewBrewItem />
|
||||||
<NewBrew />
|
|
||||||
<HelpNavItem/>
|
|
||||||
<Nav.dropdown>
|
|
||||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
|
||||||
share
|
|
||||||
</Nav.item>
|
|
||||||
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
|
||||||
view
|
|
||||||
</Nav.item>
|
|
||||||
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.publicUrl}/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>
|
|
||||||
<PrintNavItem />
|
<PrintNavItem />
|
||||||
|
<HelpNavItem />
|
||||||
<VaultNavItem />
|
<VaultNavItem />
|
||||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
<ShareNavItem brew={currentBrew} />
|
||||||
<Account />
|
<RecentNavItem brew={currentBrew} storageKey='edit' />
|
||||||
|
<AccountNavItem/>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
</Navbar>;
|
</Navbar>;
|
||||||
},
|
};
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
return <div className='editPage sitePage'>
|
<div className='editPage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
{this.renderNavbar()}
|
|
||||||
|
|
||||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
{renderNavbar()}
|
||||||
<div className="content">
|
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
|
||||||
<Editor
|
|
||||||
ref={this.editor}
|
<div className='content'>
|
||||||
brew={this.state.brew}
|
<SplitPane onDragFinish={handleSplitMove}>
|
||||||
onTextChange={this.handleTextChange}
|
<Editor
|
||||||
onStyleChange={this.handleStyleChange}
|
ref={editorRef}
|
||||||
onMetaChange={this.handleMetaChange}
|
brew={currentBrew}
|
||||||
reportError={this.errorReported}
|
onBrewChange={handleBrewChange}
|
||||||
renderer={this.state.brew.renderer}
|
reportError={setError}
|
||||||
userThemes={this.props.userThemes}
|
renderer={currentBrew.renderer}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
userThemes={props.userThemes}
|
||||||
updateBrew={this.updateBrew}
|
themeBundle={themeBundle}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
updateBrew={updateBrew}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
onCursorPageChange={setCurrentEditorCursorPageNum}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
onViewPageChange={setCurrentEditorViewPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||||
/>
|
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||||
<BrewRenderer
|
/>
|
||||||
text={this.state.brew.text}
|
<BrewRenderer
|
||||||
style={this.state.brew.style}
|
text={currentBrew.text}
|
||||||
renderer={this.state.brew.renderer}
|
style={currentBrew.style}
|
||||||
theme={this.state.brew.theme}
|
renderer={currentBrew.renderer}
|
||||||
themeBundle={this.state.themeBundle}
|
theme={currentBrew.theme}
|
||||||
errors={this.state.htmlErrors}
|
themeBundle={themeBundle}
|
||||||
lang={this.state.brew.lang}
|
errors={HTMLErrors}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
lang={currentBrew.lang}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
onPageChange={setCurrentBrewRendererPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||||
allowPrint={true}
|
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||||
/>
|
allowPrint={true}
|
||||||
</SplitPane>
|
/>
|
||||||
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = EditPage;
|
module.exports = EditPage;
|
||||||
|
|||||||
@@ -1,29 +1,25 @@
|
|||||||
@keyframes glideDown {
|
@keyframes glideDown {
|
||||||
0% {transform : translate(-50% + 3px, 0px);
|
0% {
|
||||||
opacity : 0;}
|
opacity : 0;transform : translate(-50% + 3px, 0px);}
|
||||||
100% {transform : translate(-50% + 3px, 10px);
|
100% {
|
||||||
opacity : 1;}
|
opacity : 1;transform : translate(-50% + 3px, 10px);}
|
||||||
}
|
}
|
||||||
.editPage{
|
.editPage {
|
||||||
.navItem.save{
|
.navItem.save {
|
||||||
|
position : relative;
|
||||||
width : 106px;
|
width : 106px;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
position : relative;
|
&.saved {
|
||||||
&.saved{
|
color : #666666;
|
||||||
cursor : initial;
|
cursor : initial;
|
||||||
color : #666;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.googleDriveStorage {
|
.googleDriveStorage { position : relative; }
|
||||||
position : relative;
|
.googleDriveStorage img {
|
||||||
}
|
height : 18px;
|
||||||
.googleDriveStorage img{
|
|
||||||
height : 18px;
|
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
|
|
||||||
&.inactive {
|
&.inactive { filter : grayscale(1); }
|
||||||
filter: grayscale(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
require('./lockNotification.less');
|
import './lockNotification.less';
|
||||||
const React = require('react');
|
import * as React from 'react';
|
||||||
|
import request from '../../../utils/request-middleware.js';
|
||||||
import Dialog from '../../../../components/dialog.jsx';
|
import Dialog from '../../../../components/dialog.jsx';
|
||||||
|
|
||||||
function LockNotification(props) {
|
function LockNotification(props) {
|
||||||
props = {
|
props = {
|
||||||
shareId : 0,
|
shareId : 0,
|
||||||
disableLock : ()=>{},
|
disableLock : ()=>{},
|
||||||
message : '',
|
lock : {},
|
||||||
|
message : 'Unable to retrieve Lock Message',
|
||||||
|
reviewRequested : false,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeLock = ()=>{
|
const [reviewState, setReviewState] = React.useState(props.reviewRequested);
|
||||||
alert(`Not yet implemented - ID ${props.shareId}`);
|
|
||||||
|
const removeLock = async ()=>{
|
||||||
|
await request.put(`/api/lock/review/request/${props.shareId}`)
|
||||||
|
.then(()=>{
|
||||||
|
setReviewState(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderReviewButton = function(){
|
||||||
|
if(reviewState){ return <button className='inactive'>REVIEW REQUESTED</button>; };
|
||||||
|
return <button onClick={removeLock}>REQUEST LOCK REMOVAL</button>;
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
|
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
|
||||||
@@ -19,11 +32,11 @@ function LockNotification(props) {
|
|||||||
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
|
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
|
||||||
<hr />
|
<hr />
|
||||||
<h3>LOCK REASON</h3>
|
<h3>LOCK REASON</h3>
|
||||||
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
|
<p>{props.message}</p>
|
||||||
<hr />
|
<hr />
|
||||||
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
|
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
|
||||||
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
|
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
|
||||||
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
|
{renderReviewButton()}
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,12 @@
|
|||||||
&::backdrop { background-color : #000000AA; }
|
&::backdrop { background-color : #000000AA; }
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
padding : 2px 15px;
|
||||||
margin : 10px;
|
margin : 10px;
|
||||||
color : white;
|
color : white;
|
||||||
background-color : #333333;
|
background-color : #333333;
|
||||||
|
|
||||||
|
&.inactive,
|
||||||
&:hover { background-color : #777777; }
|
&:hover { background-color : #777777; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ const dedent = require('dedent-tabs').default;
|
|||||||
|
|
||||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
|
// Prevent parsing text (e.g. document titles) as markdown
|
||||||
|
const escape = (text = '')=>{
|
||||||
|
return text.split('').map((char)=>`&#${char.charCodeAt(0)};`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
//001-050 : Brew errors
|
//001-050 : Brew errors
|
||||||
//050-100 : Other pages errors
|
//050-100 : Other pages errors
|
||||||
|
|
||||||
@@ -18,7 +23,18 @@ const errorIndex = (props)=>{
|
|||||||
'01' : dedent`
|
'01' : dedent`
|
||||||
## An error occurred while retrieving this brew from Google Drive!
|
## An error occurred while retrieving this brew from Google Drive!
|
||||||
|
|
||||||
Google reported an error while attempting to retrieve a brew from this link.`,
|
Google is able to see the brew at this link, but reported an error while attempting to retrieve it.
|
||||||
|
|
||||||
|
### Refreshing your Google Credentials
|
||||||
|
|
||||||
|
This issue is likely caused by an issue with your Google credentials; if you are the owner of this file, the following steps may resolve the issue:
|
||||||
|
|
||||||
|
- Go to https://www.naturalcrit.com/login and click logout if present (in small text at the bottom of the page).
|
||||||
|
- Click "Sign In with Google", which will refresh your Google credentials.
|
||||||
|
- After completing the sign in process, return to Homebrewery and refresh/reload the page so that it can pick up the updated credentials.
|
||||||
|
- If this was the source of the issue, it should now be resolved.
|
||||||
|
|
||||||
|
If following these steps does not resolve the issue, please let us know!`,
|
||||||
|
|
||||||
// Google Drive - 404 : brew deleted or access denied
|
// Google Drive - 404 : brew deleted or access denied
|
||||||
'02' : dedent`
|
'02' : dedent`
|
||||||
@@ -50,7 +66,7 @@ const errorIndex = (props)=>{
|
|||||||
- **The Google Account may be closed.** Google may have removed the account
|
- **The Google Account may be closed.** Google may have removed the account
|
||||||
due to inactivity or violating a Google policy. Make sure the owner can
|
due to inactivity or violating a Google policy. Make sure the owner can
|
||||||
still access Google Drive normally and upload/download files to it.
|
still access Google Drive normally and upload/download files to it.
|
||||||
:
|
|
||||||
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
If the file isn't found, Google Drive usually puts your file in your Trash folder for
|
||||||
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
|
||||||
You can also find the Activity tab on the right side of the Google Drive page, which
|
You can also find the Activity tab on the right side of the Google Drive page, which
|
||||||
@@ -78,7 +94,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
@@ -93,7 +109,7 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
:
|
:
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
**Brew Title:** ${escape(props.brew.brewTitle) || 'Unable to show title'}
|
||||||
|
|
||||||
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
|
||||||
|
|
||||||
@@ -152,6 +168,34 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Theme Not Valid
|
||||||
|
'10' : dedent`
|
||||||
|
## The selected theme is not tagged as a theme.
|
||||||
|
|
||||||
|
The brew selected as a theme exists, but has not been marked for use as a theme with the \`theme:meta\` tag.
|
||||||
|
|
||||||
|
If the selected brew is your document, you may designate it as a theme by adding the \`theme:meta\` tag.`,
|
||||||
|
|
||||||
|
// ID validation error
|
||||||
|
'11' : dedent`
|
||||||
|
## No Homebrewery document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Homebrewery document. The Brew ID failed the validation check.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Google ID validation error
|
||||||
|
'12' : dedent`
|
||||||
|
## No Google document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Google document. The Google ID failed the validation check.
|
||||||
|
|
||||||
|
:
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
//account page when account is not defined
|
//account page when account is not defined
|
||||||
'50' : dedent`
|
'50' : dedent`
|
||||||
## You are not signed in
|
## You are not signed in
|
||||||
@@ -170,13 +214,47 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}
|
**Brew ID:** ${props.brew.brewId}
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle}`,
|
**Brew Title:** ${escape(props.brew.brewTitle)}
|
||||||
|
|
||||||
|
**Brew Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}`,
|
||||||
|
|
||||||
// ####### Admin page error #######
|
// ####### Admin page error #######
|
||||||
'52': dedent`
|
'52' : dedent`
|
||||||
## Access Denied
|
## Access Denied
|
||||||
You need to provide correct administrator credentials to access this page.`,
|
You need to provide correct administrator credentials to access this page.`,
|
||||||
|
|
||||||
|
// ####### Lock Errors
|
||||||
|
|
||||||
|
'60' : dedent`Lock Error: General`,
|
||||||
|
|
||||||
|
'61' : dedent`Lock Get Error: Unable to get lock count`,
|
||||||
|
|
||||||
|
'62' : dedent`Lock Set Error: Cannot lock`,
|
||||||
|
|
||||||
|
'63' : dedent`Lock Set Error: Brew not found`,
|
||||||
|
|
||||||
|
'64' : dedent`Lock Set Error: Already locked`,
|
||||||
|
|
||||||
|
'65' : dedent`Lock Remove Error: Cannot unlock`,
|
||||||
|
|
||||||
|
'66' : dedent`Lock Remove Error: Brew not found`,
|
||||||
|
|
||||||
|
'67' : dedent`Lock Remove Error: Not locked`,
|
||||||
|
|
||||||
|
'68' : dedent`Lock Get Review Error: Cannot get review requests`,
|
||||||
|
|
||||||
|
'69' : dedent`Lock Set Review Error: Cannot set review request`,
|
||||||
|
|
||||||
|
'70' : dedent`Lock Set Review Error: Brew not found`,
|
||||||
|
|
||||||
|
'71' : dedent`Lock Set Review Error: Review already requested`,
|
||||||
|
|
||||||
|
'72' : dedent`Lock Remove Review Error: Cannot clear review request`,
|
||||||
|
|
||||||
|
'73' : dedent`Lock Remove Review Error: Brew not found`,
|
||||||
|
|
||||||
|
// ####### Other Errors
|
||||||
|
|
||||||
'90' : dedent` An unexpected error occurred while looking for these brews.
|
'90' : dedent` An unexpected error occurred while looking for these brews.
|
||||||
Try again in a few minutes.`,
|
Try again in a few minutes.`,
|
||||||
|
|
||||||
|
|||||||
@@ -1,141 +1,179 @@
|
|||||||
require('./homePage.less');
|
/* eslint-disable max-lines */
|
||||||
const React = require('react');
|
import './homePage.less';
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const cx = require('classnames');
|
|
||||||
import request from '../../utils/request-middleware.js';
|
|
||||||
const { Meta } = require('vitreum/headtags');
|
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
// Common imports
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
import request from '../../utils/request-middleware.js';
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
|
||||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||||
const Editor = require('../../editor/editor.jsx');
|
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|
||||||
|
|
||||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||||
|
import Editor from '../../editor/editor.jsx';
|
||||||
|
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||||
|
|
||||||
const HomePage = createClass({
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
displayName : 'HomePage',
|
import Navbar from '../../navbar/navbar.jsx';
|
||||||
getDefaultProps : function() {
|
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
||||||
return {
|
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||||
brew : DEFAULT_BREW,
|
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||||
ver : '0.0.0'
|
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||||
|
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
||||||
|
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
||||||
|
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||||
|
|
||||||
|
// Page specific imports
|
||||||
|
import { Meta } from 'vitreum/headtags';
|
||||||
|
|
||||||
|
const BREWKEY = 'homebrewery-new';
|
||||||
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
|
const SNIPKEY = 'homebrewery-new-snippets';
|
||||||
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
|
|
||||||
|
const useLocalStorage = false;
|
||||||
|
|
||||||
|
const HomePage =(props)=>{
|
||||||
|
props = {
|
||||||
|
brew : DEFAULT_BREW,
|
||||||
|
ver : '0.0.0',
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
|
||||||
|
const [currentBrew , setCurrentBrew] = useState(props.brew);
|
||||||
|
const [welcomeText , setWelcomeText] = useState(props.brew.text);
|
||||||
|
const [error , setError] = useState(undefined);
|
||||||
|
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text));
|
||||||
|
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
|
||||||
|
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||||
|
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||||
|
const [themeBundle , setThemeBundle] = useState({});
|
||||||
|
const [isSaving , setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||||
|
|
||||||
|
const handleControlKeys = (e)=>{
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
if(e.keyCode === 83) trySaveRef.current(true);
|
||||||
|
if(e.keyCode === 80) printCurrentBrew();
|
||||||
|
if([83, 80].includes(e.keyCode)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
getInitialState : function() {
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
return {
|
|
||||||
brew : this.props.brew,
|
return () => {
|
||||||
welcomeText : this.props.brew.text,
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
error : undefined,
|
|
||||||
currentEditorViewPageNum : 1,
|
|
||||||
currentEditorCursorPageNum : 1,
|
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
themeBundle : {}
|
|
||||||
};
|
};
|
||||||
},
|
}, []);
|
||||||
|
|
||||||
editor : React.createRef(null),
|
const save = ()=>{
|
||||||
|
|
||||||
componentDidMount : function() {
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSave : function(){
|
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(currentBrew)
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err) {
|
if(err) {
|
||||||
this.setState({ error: err });
|
setError(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const brew = res.body;
|
const saved = res.body;
|
||||||
window.location = `/edit/${brew.editId}`;
|
window.location = `/edit/${saved.editId}`;
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
handleSplitMove : function(){
|
|
||||||
this.editor.current.update();
|
|
||||||
},
|
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
const handleSplitMove = ()=>{
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
editorRef.current.update();
|
||||||
},
|
};
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
if (subfield == 'renderer' || subfield == 'theme')
|
||||||
},
|
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||||
},
|
setHTMLErrors(Markdown.validate(value));
|
||||||
|
|
||||||
handleTextChange : function(text){
|
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
|
||||||
this.setState((prevState)=>({
|
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
|
||||||
brew : { ...prevState.brew, text: text },
|
|
||||||
}));
|
if(useLocalStorage) {
|
||||||
},
|
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||||
renderNavbar : function(){
|
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||||
return <Navbar ver={this.props.ver}>
|
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||||
|
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||||
|
renderer : value.renderer,
|
||||||
|
theme : value.theme,
|
||||||
|
lang : value.lang
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearError = ()=>{
|
||||||
|
setError(null);
|
||||||
|
setIsSaving(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNavbar = ()=>{
|
||||||
|
return <Navbar ver={props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.state.error ?
|
{error ?
|
||||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
|
<PrintNavItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<VaultNavItem />
|
<VaultNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>;
|
</Navbar>;
|
||||||
},
|
};
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
return <div className='homePage sitePage'>
|
<div className='homePage sitePage'>
|
||||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||||
{this.renderNavbar()}
|
{renderNavbar()}
|
||||||
<div className="content">
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={editorRef}
|
||||||
brew={this.state.brew}
|
brew={currentBrew}
|
||||||
onTextChange={this.handleTextChange}
|
onBrewChange={handleBrewChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={currentBrew.renderer}
|
||||||
showEditButtons={false}
|
showEditButtons={false}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
themeBundle={themeBundle}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
onCursorPageChange={setCurrentEditorCursorPageNum}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
onViewPageChange={setCurrentEditorViewPageNum}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.state.brew.text}
|
text={currentBrew.text}
|
||||||
style={this.state.brew.style}
|
style={currentBrew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={currentBrew.renderer}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
onPageChange={setCurrentBrewRendererPageNum}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||||
themeBundle={this.state.themeBundle}
|
themeBundle={themeBundle}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
<div className={`floatingSaveButton${welcomeText !== currentBrew.text ? ' show' : ''}`} onClick={save}>
|
||||||
Save current <i className='fas 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='fas fa-magic' />
|
Create your own <i className='fas fa-magic' />
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</div>
|
||||||
}
|
)
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = HomePage;
|
module.exports = HomePage;
|
||||||
|
|||||||
@@ -1,50 +1,40 @@
|
|||||||
.homePage{
|
.homePage {
|
||||||
position : relative;
|
position : relative;
|
||||||
a.floatingNewButton{
|
a.floatingNewButton {
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
display : block;
|
|
||||||
right : 70px;
|
right : 70px;
|
||||||
bottom : 50px;
|
bottom : 50px;
|
||||||
z-index : 100;
|
|
||||||
z-index : 5001;
|
z-index : 5001;
|
||||||
|
display : block;
|
||||||
padding : 1em;
|
padding : 1em;
|
||||||
background-color : @orange;
|
|
||||||
font-size : 1.5em;
|
font-size : 1.5em;
|
||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
|
background-color : @orange;
|
||||||
box-shadow : 3px 3px 15px black;
|
box-shadow : 3px 3px 15px black;
|
||||||
&:hover{
|
&:hover { background-color : darken(@orange, 20%); }
|
||||||
background-color : darken(@orange, 20%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.floatingSaveButton{
|
.floatingSaveButton {
|
||||||
.animateAll();
|
.animateAll();
|
||||||
position : absolute;
|
position : absolute;
|
||||||
display : block;
|
|
||||||
right : 200px;
|
right : 200px;
|
||||||
bottom : 70px;
|
bottom : 70px;
|
||||||
z-index : 100;
|
|
||||||
z-index : 5000;
|
z-index : 5000;
|
||||||
|
display : block;
|
||||||
padding : 0.8em;
|
padding : 0.8em;
|
||||||
cursor : pointer;
|
|
||||||
background-color : @blue;
|
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
|
cursor : pointer;
|
||||||
|
background-color : @blue;
|
||||||
box-shadow : 3px 3px 15px black;
|
box-shadow : 3px 3px 15px black;
|
||||||
&:hover{
|
&:hover { background-color : darken(@blue, 20%); }
|
||||||
background-color : darken(@blue, 20%);
|
&.show { right : 350px; }
|
||||||
}
|
|
||||||
&.show{
|
|
||||||
right : 350px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navItem.save{
|
.navItem.save {
|
||||||
background-color: @orange;
|
background-color : @orange;
|
||||||
&:hover{
|
&:hover { background-color : @green; }
|
||||||
background-color: @green;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,263 +1,236 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/* eslint-disable max-lines */
|
||||||
require('./newPage.less');
|
import './newPage.less';
|
||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
import request from '../../utils/request-middleware.js';
|
|
||||||
|
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
// Common imports
|
||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import request from '../../utils/request-middleware.js';
|
||||||
|
import Markdown from 'naturalcrit/markdown.js';
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
|
||||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
import SplitPane from 'client/components/splitPane/splitPane.jsx';
|
||||||
const Editor = require('../../editor/editor.jsx');
|
import Editor from '../../editor/editor.jsx';
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
|
||||||
|
|
||||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
import Nav from 'naturalcrit/nav/nav.jsx';
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
import Navbar from '../../navbar/navbar.jsx';
|
||||||
|
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
|
||||||
|
import AccountNavItem from '../../navbar/account.navitem.jsx';
|
||||||
|
import ErrorNavItem from '../../navbar/error-navitem.jsx';
|
||||||
|
import HelpNavItem from '../../navbar/help.navitem.jsx';
|
||||||
|
import VaultNavItem from '../../navbar/vault.navitem.jsx';
|
||||||
|
import PrintNavItem from '../../navbar/print.navitem.jsx';
|
||||||
|
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
// Page specific imports
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
import { Meta } from 'vitreum/headtags';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
|
||||||
let SAVEKEY;
|
|
||||||
|
|
||||||
|
|
||||||
const NewPage = createClass({
|
const BREWKEY = 'HB_newPage_content';
|
||||||
displayName : 'NewPage',
|
const STYLEKEY = 'HB_newPage_style';
|
||||||
getDefaultProps : function() {
|
const METAKEY = 'HB_newPage_metadata';
|
||||||
return {
|
const SNIPKEY = 'HB_newPage_snippets';
|
||||||
brew : DEFAULT_BREW
|
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
|
||||||
|
|
||||||
|
|
||||||
|
const useLocalStorage = true;
|
||||||
|
|
||||||
|
const NewPage = (props) => {
|
||||||
|
props = {
|
||||||
|
brew: DEFAULT_BREW,
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
|
||||||
|
const [currentBrew , setCurrentBrew ] = useState(props.brew);
|
||||||
|
const [isSaving , setIsSaving ] = useState(false);
|
||||||
|
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
|
||||||
|
const [error , setError ] = useState(null);
|
||||||
|
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
|
||||||
|
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
|
||||||
|
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
|
||||||
|
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||||
|
const [themeBundle , setThemeBundle ] = useState({});
|
||||||
|
|
||||||
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadBrew();
|
||||||
|
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
|
||||||
|
|
||||||
|
const handleControlKeys = (e)=>{
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
if(e.keyCode === 83) trySaveRef.current(true);
|
||||||
|
if(e.keyCode === 80) printCurrentBrew();
|
||||||
|
if([83, 80].includes(e.keyCode)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
const brew = this.props.brew;
|
|
||||||
|
|
||||||
return {
|
return () => {
|
||||||
brew : brew,
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
isSaving : false,
|
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
|
||||||
error : null,
|
|
||||||
htmlErrors : Markdown.validate(brew.text),
|
|
||||||
currentEditorViewPageNum : 1,
|
|
||||||
currentEditorCursorPageNum : 1,
|
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
themeBundle : {}
|
|
||||||
};
|
};
|
||||||
},
|
}, []);
|
||||||
|
|
||||||
editor : React.createRef(null),
|
const loadBrew = ()=>{
|
||||||
|
const brew = { ...currentBrew };
|
||||||
componentDidMount : function() {
|
if(!brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
|
||||||
|
|
||||||
const brew = this.state.brew;
|
|
||||||
|
|
||||||
if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
|
||||||
const brewStorage = localStorage.getItem(BREWKEY);
|
const brewStorage = localStorage.getItem(BREWKEY);
|
||||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||||
|
|
||||||
brew.text = brewStorage ?? brew.text;
|
brew.text = brewStorage ?? brew.text;
|
||||||
brew.style = styleStorage ?? brew.style;
|
brew.style = styleStorage ?? brew.style;
|
||||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
|
||||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
|
||||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||||
brew.lang = metaStorage?.lang ?? brew.lang;
|
brew.lang = metaStorage?.lang ?? brew.lang;
|
||||||
}
|
}
|
||||||
|
|
||||||
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
|
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
|
||||||
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
|
||||||
|
|
||||||
this.setState({
|
setCurrentBrew(brew);
|
||||||
brew : brew,
|
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
|
||||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
|
||||||
});
|
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
if(brew.style)
|
if(brew.style)
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
localStorage.setItem(METAKEY, JSON.stringify({ renderer: brew.renderer, theme: brew.theme, lang: brew.lang }));
|
||||||
if(window.location.pathname != '/new') {
|
if(window.location.pathname !== '/new')
|
||||||
window.history.replaceState({}, window.location.title, '/new/');
|
window.history.replaceState({}, window.location.title, '/new/');
|
||||||
}
|
};
|
||||||
},
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
const handleSplitMove = ()=>{
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
editorRef.current.update();
|
||||||
const S_KEY = 83;
|
};
|
||||||
const P_KEY = 80;
|
|
||||||
if(e.keyCode == S_KEY) this.save();
|
|
||||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSplitMove : function(){
|
const handleBrewChange = (field) => (value, subfield) => { //'text', 'style', 'snippets', 'metadata'
|
||||||
this.editor.current.update();
|
if (subfield == 'renderer' || subfield == 'theme')
|
||||||
},
|
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
//If there are HTML errors, run the validator on every change to give quick feedback
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
|
||||||
},
|
setHTMLErrors(Markdown.validate(value));
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
if(field == 'metadata') setCurrentBrew(prev => ({ ...prev, ...value }));
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
else setCurrentBrew(prev => ({ ...prev, [field]: value }));
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
if(useLocalStorage) {
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
if(field == 'text') localStorage.setItem(BREWKEY, value);
|
||||||
},
|
if(field == 'style') localStorage.setItem(STYLEKEY, value);
|
||||||
|
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
|
||||||
handleTextChange : function(text){
|
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
|
||||||
//If there are errors, run the validator on every change to give quick feedback
|
renderer : value.renderer,
|
||||||
let htmlErrors = this.state.htmlErrors;
|
theme : value.theme,
|
||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
lang : value.lang
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : { ...prevState.brew, text: text },
|
|
||||||
htmlErrors : htmlErrors,
|
|
||||||
}));
|
|
||||||
localStorage.setItem(BREWKEY, text);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleStyleChange : function(style){
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : { ...prevState.brew, style: style },
|
|
||||||
}));
|
|
||||||
localStorage.setItem(STYLEKEY, style);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleMetaChange : function(metadata, field=undefined){
|
|
||||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
|
||||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : { ...prevState.brew, ...metadata },
|
|
||||||
}), ()=>{
|
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({
|
|
||||||
// 'title' : this.state.brew.title,
|
|
||||||
// 'description' : this.state.brew.description,
|
|
||||||
'renderer' : this.state.brew.renderer,
|
|
||||||
'theme' : this.state.brew.theme,
|
|
||||||
'lang' : this.state.brew.lang
|
|
||||||
}));
|
}));
|
||||||
});
|
|
||||||
;
|
|
||||||
},
|
|
||||||
|
|
||||||
save : async function(){
|
|
||||||
this.setState({
|
|
||||||
isSaving : true
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
let updatedBrew = { ...currentBrew };
|
||||||
|
splitTextStyleAndMetadata(updatedBrew);
|
||||||
|
|
||||||
|
const pageRegex = updatedBrew.renderer === 'legacy' ? /\\page/g : /^\\page$/gm;
|
||||||
|
updatedBrew.pageCount = (updatedBrew.text.match(pageRegex) || []).length + 1;
|
||||||
|
|
||||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
.post(`/api${saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||||
.send(brew)
|
.send(updatedBrew)
|
||||||
.catch((err)=>{
|
.catch((err) => {
|
||||||
this.setState({ isSaving: false, error: err });
|
setIsSaving(false);
|
||||||
|
setError(err);
|
||||||
});
|
});
|
||||||
if(!res) return;
|
|
||||||
|
|
||||||
brew = res.body;
|
setIsSaving(false)
|
||||||
|
if (!res) return;
|
||||||
|
|
||||||
|
const savedBrew = res.body;
|
||||||
|
|
||||||
localStorage.removeItem(BREWKEY);
|
localStorage.removeItem(BREWKEY);
|
||||||
localStorage.removeItem(STYLEKEY);
|
localStorage.removeItem(STYLEKEY);
|
||||||
localStorage.removeItem(METAKEY);
|
localStorage.removeItem(METAKEY);
|
||||||
window.location = `/edit/${brew.editId}`;
|
window.location = `/edit/${savedBrew.editId}`;
|
||||||
},
|
};
|
||||||
|
|
||||||
renderSaveButton : function(){
|
const renderSaveButton = ()=>{
|
||||||
if(this.state.isSaving){
|
if(isSaving){
|
||||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||||
save...
|
save...
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
} else {
|
} else {
|
||||||
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
|
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
|
||||||
save
|
save
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
renderNavbar : function(){
|
const clearError = ()=>{
|
||||||
return <Navbar>
|
setError(null);
|
||||||
|
setIsSaving(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNavbar = () => (
|
||||||
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.state.error ?
|
{error
|
||||||
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
? <ErrorNavItem error={error} clearError={clearError} />
|
||||||
this.renderSaveButton()
|
: renderSaveButton()}
|
||||||
}
|
<NewBrewItem />
|
||||||
<PrintNavItem />
|
<PrintNavItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
|
<VaultNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>;
|
</Navbar>
|
||||||
},
|
);
|
||||||
|
|
||||||
render : function(){
|
return (
|
||||||
return <div className='newPage sitePage'>
|
<div className='newPage sitePage'>
|
||||||
{this.renderNavbar()}
|
{renderNavbar()}
|
||||||
<div className="content">
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={editorRef}
|
||||||
brew={this.state.brew}
|
brew={currentBrew}
|
||||||
onTextChange={this.handleTextChange}
|
onBrewChange={handleBrewChange}
|
||||||
onStyleChange={this.handleStyleChange}
|
renderer={currentBrew.renderer}
|
||||||
onMetaChange={this.handleMetaChange}
|
userThemes={props.userThemes}
|
||||||
renderer={this.state.brew.renderer}
|
themeBundle={themeBundle}
|
||||||
userThemes={this.props.userThemes}
|
onCursorPageChange={setCurrentEditorCursorPageNum}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
onViewPageChange={setCurrentEditorViewPageNum}
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
/>
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
<BrewRenderer
|
||||||
/>
|
text={currentBrew.text}
|
||||||
<BrewRenderer
|
style={currentBrew.style}
|
||||||
text={this.state.brew.text}
|
renderer={currentBrew.renderer}
|
||||||
style={this.state.brew.style}
|
theme={currentBrew.theme}
|
||||||
renderer={this.state.brew.renderer}
|
themeBundle={themeBundle}
|
||||||
theme={this.state.brew.theme}
|
errors={HTMLErrors}
|
||||||
themeBundle={this.state.themeBundle}
|
lang={currentBrew.lang}
|
||||||
errors={this.state.htmlErrors}
|
onPageChange={setCurrentBrewRendererPageNum}
|
||||||
lang={this.state.brew.lang}
|
currentEditorViewPageNum={currentEditorViewPageNum}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
currentEditorCursorPageNum={currentEditorCursorPageNum}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
allowPrint={true}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
/>
|
||||||
allowPrint={true}
|
</SplitPane>
|
||||||
/>
|
|
||||||
</SplitPane>
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = NewPage;
|
module.exports = NewPage;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
.newPage{
|
.newPage {
|
||||||
.navItem.save{
|
.navItem.save {
|
||||||
background-color: @orange;
|
background-color : @orange;
|
||||||
&:hover{
|
&:hover { background-color : @green; }
|
||||||
background-color: @green;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require('./sharePage.less');
|
require('./sharePage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const { useState, useEffect, useCallback } = React;
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -8,130 +8,112 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = (props)=>{
|
||||||
displayName : 'SharePage',
|
const { brew = DEFAULT_BREW_LOAD, disableMeta = false } = props;
|
||||||
getDefaultProps : function() {
|
|
||||||
return {
|
|
||||||
brew : DEFAULT_BREW_LOAD,
|
|
||||||
disableMeta : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
const [themeBundle, setThemeBundle] = useState({});
|
||||||
return {
|
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
|
||||||
themeBundle : {},
|
|
||||||
currentBrewRendererPageNum : 1
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount : function() {
|
const handleBrewRendererPageChange = useCallback((pageNumber)=>{
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
setCurrentBrewRendererPageNum(pageNumber);
|
||||||
|
}, []);
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
const handleControlKeys = (e)=>{
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
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) {
|
||||||
if(e.keyCode == P_KEY) printCurrentBrew();
|
printCurrentBrew();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
|
|
||||||
processShareId : function() {
|
useEffect(()=>{
|
||||||
return this.props.brew.googleId && !this.props.brew.stubbed ?
|
document.addEventListener('keydown', handleControlKeys);
|
||||||
this.props.brew.googleId + this.props.brew.shareId :
|
fetchThemeBundle(undefined, setThemeBundle, brew.renderer, brew.theme);
|
||||||
this.props.brew.shareId;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderEditLink : function(){
|
return ()=>{
|
||||||
if(!this.props.brew.editId) return;
|
document.removeEventListener('keydown', handleControlKeys);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
let editLink = this.props.brew.editId;
|
const processShareId = ()=>{
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
return brew.googleId && !brew.stubbed ? brew.googleId + brew.shareId : brew.shareId;
|
||||||
editLink = this.props.brew.googleId + editLink;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
const renderEditLink = ()=>{
|
||||||
edit
|
if(!brew.editId) return null;
|
||||||
</Nav.item>;
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
const editLink = brew.googleId && ! brew.stubbed ? brew.googleId + brew.editId : brew.editId;
|
||||||
const titleStyle = this.props.disableMeta ? { cursor: 'default' } : {};
|
|
||||||
const titleEl = <Nav.item className='brewTitle' style={titleStyle}>{this.props.brew.title}</Nav.item>;
|
|
||||||
|
|
||||||
return <div className='sharePage sitePage'>
|
return (
|
||||||
|
<Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
|
||||||
|
edit
|
||||||
|
</Nav.item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleEl = (
|
||||||
|
<Nav.item className='brewTitle' style={disableMeta ? { cursor: 'default' } : {}}>
|
||||||
|
{brew.title}
|
||||||
|
</Nav.item>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section className='titleSection'>
|
<Nav.section className='titleSection'>
|
||||||
{
|
{disableMeta ? titleEl : <MetadataNav brew={brew}>{titleEl}</MetadataNav>}
|
||||||
this.props.disableMeta ?
|
|
||||||
titleEl
|
|
||||||
:
|
|
||||||
<MetadataNav brew={this.props.brew}>
|
|
||||||
{titleEl}
|
|
||||||
</MetadataNav>
|
|
||||||
}
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.props.brew.shareId && <>
|
{brew.shareId && (
|
||||||
<PrintNavItem/>
|
<>
|
||||||
<Nav.dropdown>
|
<PrintNavItem />
|
||||||
<Nav.item color='red' icon='fas fa-code'>
|
<Nav.dropdown>
|
||||||
source
|
<Nav.item color='red' icon='fas fa-code'>
|
||||||
</Nav.item>
|
source
|
||||||
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
|
</Nav.item>
|
||||||
view
|
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${processShareId()}`}>
|
||||||
</Nav.item>
|
view
|
||||||
{this.renderEditLink()}
|
</Nav.item>
|
||||||
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
|
{renderEditLink()}
|
||||||
download
|
<Nav.item color='blue' icon='fas fa-download' href={`/download/${processShareId()}`}>
|
||||||
</Nav.item>
|
download
|
||||||
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
|
</Nav.item>
|
||||||
clone to new
|
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${processShareId()}`}>
|
||||||
</Nav.item>
|
clone to new
|
||||||
</Nav.dropdown>
|
</Nav.item>
|
||||||
</>}
|
</Nav.dropdown>
|
||||||
<VaultNavItem/>
|
</>
|
||||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
)}
|
||||||
|
<RecentNavItem brew={brew} storageKey='view' />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.props.brew.text}
|
text={brew.text}
|
||||||
style={this.props.brew.style}
|
style={brew.style}
|
||||||
lang={this.props.brew.lang}
|
lang={brew.lang}
|
||||||
renderer={this.props.brew.renderer}
|
renderer={brew.renderer}
|
||||||
theme={this.props.brew.theme}
|
theme={brew.theme}
|
||||||
themeBundle={this.state.themeBundle}
|
themeBundle={themeBundle}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
onPageChange={handleBrewRendererPageChange}
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
currentBrewRendererPageNum={currentBrewRendererPageNum}
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>
|
||||||
}
|
);
|
||||||
});
|
};
|
||||||
|
|
||||||
module.exports = SharePage;
|
module.exports = SharePage;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
.sharePage{
|
.sharePage {
|
||||||
nav .navSection.titleSection {
|
nav .navSection.titleSection {
|
||||||
flex-grow: 1;
|
flex-grow : 1;
|
||||||
justify-content: center;
|
justify-content : center;
|
||||||
}
|
|
||||||
.content{
|
|
||||||
overflow-y : hidden;
|
|
||||||
}
|
}
|
||||||
|
.content { overflow-y : hidden; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,10 +39,14 @@ const UserPage = (props)=>{
|
|||||||
}] : [])
|
}] : [])
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const clearError = ()=>{
|
||||||
|
setError(null);
|
||||||
|
};
|
||||||
|
|
||||||
const navItems = (
|
const navItems = (
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
|
{error && (<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem>)}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<VaultNavitem />
|
<VaultNavitem />
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const Account = require('../../navbar/account.navitem.jsx');
|
|||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
||||||
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('client/components/splitPane/splitPane.jsx');
|
||||||
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
||||||
|
|
||||||
import request from '../../utils/request-middleware.js';
|
import request from '../../utils/request-middleware.js';
|
||||||
@@ -99,14 +99,14 @@ const VaultPage = (props)=>{
|
|||||||
setSearching(true);
|
setSearching(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const title = titleRef.current.value || '';
|
const title = titleRef.current.value || '';
|
||||||
const author = authorRef.current.value || '';
|
const author = authorRef.current.value || '';
|
||||||
const count = countRef.current.value || 10;
|
const count = countRef.current.value || 10;
|
||||||
const v3 = v3Ref.current.checked != false;
|
const v3 = v3Ref.current.checked != false;
|
||||||
const legacy = legacyRef.current.checked != false;
|
const legacy = legacyRef.current.checked != false;
|
||||||
const sortOption = sort || 'title';
|
const sortOption = sort || 'title';
|
||||||
const dirOption = dir || 'asc';
|
const dirOption = dir || 'asc';
|
||||||
const pageProp = page || 1;
|
const pageProp = page || 1;
|
||||||
|
|
||||||
setSort(sortOption);
|
setSort(sortOption);
|
||||||
setdir(dirOption);
|
setdir(dirOption);
|
||||||
@@ -247,7 +247,7 @@ const VaultPage = (props)=>{
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Some common words like "a", "after", "through", "itself", "here", etc.,
|
Some common words like "a", "after", "through", "itself", "here", etc.,
|
||||||
are ignored in searches. The full list can be found
|
are ignored in searches. The full list can be found
|
||||||
<a href='https://github.com/mongodb/mongo/blob/0e3b3ca8480ddddf5d0105d11a94bd4698335312/src/mongo/db/fts/stop_words_english.txt'>
|
<a href='https://github.com/mongodb/mongo/blob/0e3b3ca8480ddddf5d0105d11a94bd4698335312/src/mongo/db/fts/stop_words_english.txt'>
|
||||||
here
|
here
|
||||||
</a>
|
</a>
|
||||||
@@ -286,9 +286,9 @@ const VaultPage = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderPaginationControls = ()=>{
|
const renderPaginationControls = ()=>{
|
||||||
if(!totalBrews) return null;
|
if(!totalBrews || totalBrews < 10) return null;
|
||||||
|
|
||||||
const countInt = parseInt(props.query.count || 20);
|
const countInt = parseInt(brewCollection.length || 20);
|
||||||
const totalPages = Math.ceil(totalBrews / countInt);
|
const totalPages = Math.ceil(totalBrews / countInt);
|
||||||
|
|
||||||
let startPage, endPage;
|
let startPage, endPage;
|
||||||
@@ -355,7 +355,7 @@ const VaultPage = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderFoundBrews = ()=>{
|
const renderFoundBrews = ()=>{
|
||||||
if(searching) {
|
if(searching && !brewCollection) {
|
||||||
return (
|
return (
|
||||||
<div className='foundBrews searching'>
|
<div className='foundBrews searching'>
|
||||||
<h3 className='searchAnim'>Searching</h3>
|
<h3 className='searchAnim'>Searching</h3>
|
||||||
@@ -395,6 +395,7 @@ const VaultPage = (props)=>{
|
|||||||
{`Brews found: `}
|
{`Brews found: `}
|
||||||
<span>{totalBrews}</span>
|
<span>{totalBrews}</span>
|
||||||
</span>
|
</span>
|
||||||
|
{brewCollection.length > 10 && renderPaginationControls()}
|
||||||
{brewCollection.map((brew, index)=>{
|
{brewCollection.map((brew, index)=>{
|
||||||
return (
|
return (
|
||||||
<BrewItem
|
<BrewItem
|
||||||
@@ -415,14 +416,14 @@ const VaultPage = (props)=>{
|
|||||||
<link href='/themes/V3/Blank/style.css' rel='stylesheet' />
|
<link href='/themes/V3/Blank/style.css' rel='stylesheet' />
|
||||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
|
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
|
||||||
{renderNavItems()}
|
{renderNavItems()}
|
||||||
<div className="content">
|
<div className='content'>
|
||||||
<SplitPane showDividerButtons={false}>
|
<SplitPane showDividerButtons={false}>
|
||||||
<div className='form dataGroup'>{renderForm()}</div>
|
<div className='form dataGroup'>{renderForm()}</div>
|
||||||
<div className='resultsContainer dataGroup'>
|
<div className='resultsContainer dataGroup'>
|
||||||
{renderSortBar()}
|
{renderSortBar()}
|
||||||
{renderFoundBrews()}
|
{renderFoundBrews()}
|
||||||
</div>
|
</div>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
*:not(input) { user-select : none; }
|
*:not(input) { user-select : none; }
|
||||||
|
|
||||||
.content .dataGroup {
|
:where(.content .dataGroup) {
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
background : white;
|
background : white;
|
||||||
@@ -169,9 +169,10 @@
|
|||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
max-height : 100%;
|
max-height : 100%;
|
||||||
padding : 50px 50px 70px 50px;
|
padding : 70px 50px;
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
background-color : #2C3E50;
|
background-color : #2C3E50;
|
||||||
|
container-type : inline-size;
|
||||||
|
|
||||||
h3 { font-size : 25px; }
|
h3 { font-size : 25px; }
|
||||||
|
|
||||||
@@ -236,6 +237,7 @@
|
|||||||
margin-right : 40px;
|
margin-right : 40px;
|
||||||
color : black;
|
color : black;
|
||||||
isolation : isolate;
|
isolation : isolate;
|
||||||
|
transition : width 0.5s;
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -269,8 +271,8 @@
|
|||||||
.links { z-index : 2; }
|
.links { z-index : 2; }
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin : 0px;
|
|
||||||
visibility : hidden;
|
visibility : hidden;
|
||||||
|
margin : 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail { z-index : -1; }
|
.thumbnail { z-index : -1; }
|
||||||
@@ -278,30 +280,37 @@
|
|||||||
|
|
||||||
.paginationControls {
|
.paginationControls {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
|
top : 35px;
|
||||||
left : 50%;
|
left : 50%;
|
||||||
display : grid;
|
display : grid;
|
||||||
grid-template-areas : 'previousPage currentPage nextPage';
|
grid-template-areas : 'previousPage currentPage nextPage';
|
||||||
grid-template-columns : 50px 1fr 50px;
|
grid-template-columns : 50px 1fr 50px;
|
||||||
|
gap : 20px;
|
||||||
place-items : center;
|
place-items : center;
|
||||||
width : auto;
|
width : auto;
|
||||||
|
font-size : 15px;
|
||||||
translate : -50%;
|
translate : -50%;
|
||||||
|
|
||||||
|
&:last-child { top : unset; }
|
||||||
|
|
||||||
.pages {
|
.pages {
|
||||||
display : flex;
|
display : flex;
|
||||||
grid-area : currentPage;
|
grid-area : currentPage;
|
||||||
|
gap : 1em;
|
||||||
justify-content : space-evenly;
|
justify-content : space-evenly;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
padding : 5px 8px;
|
|
||||||
text-align : center;
|
text-align : center;
|
||||||
|
|
||||||
.pageNumber {
|
.pageNumber {
|
||||||
margin-inline : 1vw;
|
place-content : center;
|
||||||
|
width : fit-content;
|
||||||
|
min-width : 2em;
|
||||||
font-family : 'Open Sans';
|
font-family : 'Open Sans';
|
||||||
font-weight : 900;
|
font-weight : 900;
|
||||||
color : white;
|
color : white;
|
||||||
text-underline-position : under;
|
|
||||||
text-wrap : nowrap;
|
text-wrap : nowrap;
|
||||||
|
text-underline-position : under;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|
||||||
&.currentPage {
|
&.currentPage {
|
||||||
@@ -329,7 +338,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes trailingDots {
|
@keyframes trailingDots {
|
||||||
@@ -344,8 +352,7 @@
|
|||||||
100% { content : ' ...'; }
|
100% { content : ' ...'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// media query for when the page is smaller than 1079 px in width
|
@container (width < 670px) {
|
||||||
@media screen and (max-width : 1079px) {
|
|
||||||
.vaultPage {
|
.vaultPage {
|
||||||
|
|
||||||
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
|
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
|
||||||
|
|||||||
64
client/homebrew/thumbnail.svg
Normal file
64
client/homebrew/thumbnail.svg
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 94.65 94.6"
|
||||||
|
version="1.1"
|
||||||
|
id="svg11"
|
||||||
|
sodipodi:docname="thumbnail.svg"
|
||||||
|
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview13"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="8.4989431"
|
||||||
|
inkscape:cx="38.887188"
|
||||||
|
inkscape:cy="47.417661"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1043"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg11" />
|
||||||
|
<defs
|
||||||
|
id="defs4">
|
||||||
|
<style
|
||||||
|
id="style2">.cls-1{fill:#ed1f24;}</style>
|
||||||
|
</defs>
|
||||||
|
<title
|
||||||
|
id="title6">NaturalCritLogo</title>
|
||||||
|
<g
|
||||||
|
id="Layer_2"
|
||||||
|
data-name="Layer 2"
|
||||||
|
style="fill:#000000;stroke:#000000">
|
||||||
|
<g
|
||||||
|
id="base"
|
||||||
|
style="fill:#000000;stroke:#000000">
|
||||||
|
<path
|
||||||
|
id="D20"
|
||||||
|
class="cls-1"
|
||||||
|
d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<metadata
|
||||||
|
id="metadata1">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:title>NaturalCritLogo</dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
74
client/homebrew/utils/request-middleware.spec.js
Normal file
74
client/homebrew/utils/request-middleware.spec.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import requestMiddleware from './request-middleware';
|
||||||
|
|
||||||
|
jest.mock('superagent');
|
||||||
|
import request from 'superagent';
|
||||||
|
|
||||||
|
describe('request-middleware', ()=>{
|
||||||
|
let version;
|
||||||
|
|
||||||
|
let setFn;
|
||||||
|
let testFn;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
jest.resetAllMocks();
|
||||||
|
version = global.version;
|
||||||
|
|
||||||
|
global.version = '999';
|
||||||
|
|
||||||
|
setFn = jest.fn();
|
||||||
|
testFn = jest.fn(()=>{ return { set: setFn }; });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
global.version = version;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add header to get', ()=>{
|
||||||
|
// Ensure tests functions have been reset
|
||||||
|
expect(testFn).not.toHaveBeenCalled();
|
||||||
|
expect(setFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
request.get = testFn;
|
||||||
|
|
||||||
|
requestMiddleware.get('path');
|
||||||
|
|
||||||
|
expect(testFn).toHaveBeenCalledWith('path');
|
||||||
|
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add header to put', ()=>{
|
||||||
|
expect(testFn).not.toHaveBeenCalled();
|
||||||
|
expect(setFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
request.put = testFn;
|
||||||
|
|
||||||
|
requestMiddleware.put('path');
|
||||||
|
|
||||||
|
expect(testFn).toHaveBeenCalledWith('path');
|
||||||
|
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add header to post', ()=>{
|
||||||
|
expect(testFn).not.toHaveBeenCalled();
|
||||||
|
expect(setFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
request.post = testFn;
|
||||||
|
|
||||||
|
requestMiddleware.post('path');
|
||||||
|
|
||||||
|
expect(testFn).toHaveBeenCalledWith('path');
|
||||||
|
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add header to delete', ()=>{
|
||||||
|
expect(testFn).not.toHaveBeenCalled();
|
||||||
|
expect(setFn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
request.delete = testFn;
|
||||||
|
|
||||||
|
requestMiddleware.delete('path');
|
||||||
|
|
||||||
|
expect(testFn).toHaveBeenCalledWith('path');
|
||||||
|
expect(setFn).toHaveBeenCalledWith('Homebrewery-Version', '999');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
const getLocalStorageMap = function(){
|
||||||
|
const localStorageMap = {
|
||||||
|
'AUTOSAVE_ON' : 'HB_editor_autoSaveOn',
|
||||||
|
'HOMEBREWERY-EDITOR-THEME' : 'HB_editor_theme',
|
||||||
|
'liveScroll' : 'HB_editor_liveScroll',
|
||||||
|
'naturalcrit-pane-split' : 'HB_editor_splitWidth',
|
||||||
|
|
||||||
|
'HOMEBREWERY-LISTPAGE-SORTDIR' : 'HB_listPage_sortDir',
|
||||||
|
'HOMEBREWERY-LISTPAGE-SORTTYPE' : 'HB_listPage_sortType',
|
||||||
|
'HOMEBREWERY-LISTPAGE-VISIBILITY-published' : 'HB_listPage_visibility_group_published',
|
||||||
|
'HOMEBREWERY-LISTPAGE-VISIBILITY-unpublished' : 'HB_listPage_visibility_group_unpublished',
|
||||||
|
|
||||||
|
'hbAdminTab' : 'HB_adminPage_currentTab',
|
||||||
|
|
||||||
|
'homebrewery-new' : 'HB_newPage_content',
|
||||||
|
'homebrewery-new-meta' : 'HB_newPage_metadata',
|
||||||
|
'homebrewery-new-style' : 'HB_newPage_style',
|
||||||
|
|
||||||
|
'homebrewery-recently-edited' : 'HB_nav_recentlyEdited',
|
||||||
|
'homebrewery-recently-viewed' : 'HB_nav_recentlyViewed',
|
||||||
|
|
||||||
|
'hb_toolbarState' : 'HB_renderer_toolbarState',
|
||||||
|
'hb_toolbarVisibility' : 'HB_renderer_toolbarVisibility'
|
||||||
|
};
|
||||||
|
|
||||||
|
if(global?.account?.username){
|
||||||
|
const username = global.account.username;
|
||||||
|
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return localStorageMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getLocalStorageMap;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import getLocalStorageMap from './localStorageKeyMap.js';
|
||||||
|
|
||||||
|
describe('getLocalStorageMap', ()=>{
|
||||||
|
it('no username', ()=>{
|
||||||
|
const account = global.account;
|
||||||
|
|
||||||
|
delete global.account;
|
||||||
|
|
||||||
|
const map = getLocalStorageMap();
|
||||||
|
|
||||||
|
global.account = account;
|
||||||
|
|
||||||
|
expect(map).toBeInstanceOf(Object);
|
||||||
|
expect(Object.entries(map)).toHaveLength(16);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no username', ()=>{
|
||||||
|
const account = global.account;
|
||||||
|
|
||||||
|
global.account = { username: 'test' };
|
||||||
|
|
||||||
|
const map = getLocalStorageMap();
|
||||||
|
|
||||||
|
global.account = account;
|
||||||
|
|
||||||
|
expect(map).toBeInstanceOf(Object);
|
||||||
|
expect(Object.entries(map)).toHaveLength(17);
|
||||||
|
expect(map).toHaveProperty('HOMEBREWERY-DEFAULT-SAVE-LOCATION-test', 'HB_editor_defaultSave_test');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import getLocalStorageMap from './localStorageKeyMap.js';
|
||||||
|
|
||||||
|
const updateLocalStorage = function(){
|
||||||
|
// Return if no window and thus no local storage
|
||||||
|
if(typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// Return if the local storage key map has no content
|
||||||
|
const localStorageKeyMap = getLocalStorageMap();
|
||||||
|
if(Object.keys(localStorageKeyMap).length == 0) return;
|
||||||
|
|
||||||
|
const storage = window.localStorage;
|
||||||
|
|
||||||
|
Object.keys(localStorageKeyMap).forEach((key)=>{
|
||||||
|
if(storage[key]){
|
||||||
|
if(!storage[localStorageKeyMap[key]]){
|
||||||
|
const data = storage.getItem(key);
|
||||||
|
storage.setItem(localStorageKeyMap[key], data);
|
||||||
|
};
|
||||||
|
storage.removeItem(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export { updateLocalStorage };
|
||||||
@@ -42,6 +42,7 @@ function parseBrewForStorage(brew, slot = 0) {
|
|||||||
title : brew.title,
|
title : brew.title,
|
||||||
text : brew.text,
|
text : brew.text,
|
||||||
style : brew.style,
|
style : brew.style,
|
||||||
|
snippets : brew.snippets,
|
||||||
version : brew.version,
|
version : brew.version,
|
||||||
shareId : brew.shareId,
|
shareId : brew.shareId,
|
||||||
savedAt : brew?.savedAt || new Date(),
|
savedAt : brew?.savedAt || new Date(),
|
||||||
|
|||||||
@@ -1,84 +1,34 @@
|
|||||||
.fac {
|
.fac {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
background-color : currentColor;
|
|
||||||
mask-size : contain;
|
|
||||||
mask-repeat : no-repeat;
|
|
||||||
mask-position : center;
|
|
||||||
width : 1em;
|
width : 1em;
|
||||||
aspect-ratio : 1;
|
aspect-ratio : 1;
|
||||||
|
background-color : currentColor;
|
||||||
|
mask-repeat : no-repeat;
|
||||||
|
mask-position : center;
|
||||||
|
mask-size : contain;
|
||||||
}
|
}
|
||||||
.position-top-left {
|
.position-top-left { mask-image : url('../icons/position-top-left.svg'); }
|
||||||
mask-image: url('../icons/position-top-left.svg');
|
.position-top-right { mask-image : url('../icons/position-top-right.svg'); }
|
||||||
}
|
.position-bottom-left { mask-image : url('../icons/position-bottom-left.svg'); }
|
||||||
.position-top-right {
|
.position-bottom-right { mask-image : url('../icons/position-bottom-right.svg'); }
|
||||||
mask-image: url('../icons/position-top-right.svg');
|
.position-top { mask-image : url('../icons/position-top.svg'); }
|
||||||
}
|
.position-right { mask-image : url('../icons/position-right.svg'); }
|
||||||
.position-bottom-left {
|
.position-bottom { mask-image : url('../icons/position-bottom.svg'); }
|
||||||
mask-image: url('../icons/position-bottom-left.svg');
|
.position-left { mask-image : url('../icons/position-left.svg'); }
|
||||||
}
|
.mask-edge { mask-image : url('../icons/mask-edge.svg'); }
|
||||||
.position-bottom-right {
|
.mask-corner { mask-image : url('../icons/mask-corner.svg'); }
|
||||||
mask-image: url('../icons/position-bottom-right.svg');
|
.mask-center { mask-image : url('../icons/mask-center.svg'); }
|
||||||
}
|
.book-front-cover { mask-image : url('../icons/book-front-cover.svg'); }
|
||||||
.position-top {
|
.book-back-cover { mask-image : url('../icons/book-back-cover.svg'); }
|
||||||
mask-image: url('../icons/position-top.svg');
|
.book-inside-cover { mask-image : url('../icons/book-inside-cover.svg'); }
|
||||||
}
|
.book-part-cover { mask-image : url('../icons/book-part-cover.svg'); }
|
||||||
.position-right {
|
.image-wrap-left { mask-image : url('../icons/image-wrap-left.svg'); }
|
||||||
mask-image: url('../icons/position-right.svg');
|
.image-wrap-right { mask-image : url('../icons/image-wrap-right.svg'); }
|
||||||
}
|
.davek { mask-image : url('../icons/Davek.svg'); }
|
||||||
.position-bottom {
|
.rellanic { mask-image : url('../icons/Rellanic.svg'); }
|
||||||
mask-image: url('../icons/position-bottom.svg');
|
.iokharic { mask-image : url('../icons/Iokharic.svg'); }
|
||||||
}
|
.zoom-to-fit { mask-image : url('../icons/zoom-to-fit.svg'); }
|
||||||
.position-left {
|
.fit-width { mask-image : url('../icons/fit-width.svg'); }
|
||||||
mask-image: url('../icons/position-left.svg');
|
.single-spread { mask-image : url('../icons/single-spread.svg'); }
|
||||||
}
|
.facing-spread { mask-image : url('../icons/facing-spread.svg'); }
|
||||||
.mask-edge {
|
.flow-spread { mask-image : url('../icons/flow-spread.svg'); }
|
||||||
mask-image: url('../icons/mask-edge.svg');
|
|
||||||
}
|
|
||||||
.mask-corner {
|
|
||||||
mask-image: url('../icons/mask-corner.svg');
|
|
||||||
}
|
|
||||||
.mask-center {
|
|
||||||
mask-image: url('../icons/mask-center.svg');
|
|
||||||
}
|
|
||||||
.book-front-cover {
|
|
||||||
mask-image: url('../icons/book-front-cover.svg');
|
|
||||||
}
|
|
||||||
.book-back-cover {
|
|
||||||
mask-image: url('../icons/book-back-cover.svg');
|
|
||||||
}
|
|
||||||
.book-inside-cover {
|
|
||||||
mask-image: url('../icons/book-inside-cover.svg');
|
|
||||||
}
|
|
||||||
.book-part-cover {
|
|
||||||
mask-image: url('../icons/book-part-cover.svg');
|
|
||||||
}
|
|
||||||
.image-wrap-left {
|
|
||||||
mask-image: url('../icons/image-wrap-left.svg');
|
|
||||||
}
|
|
||||||
.image-wrap-right {
|
|
||||||
mask-image: url('../icons/image-wrap-right.svg');
|
|
||||||
}
|
|
||||||
.davek {
|
|
||||||
mask-image: url('../icons/Davek.svg');
|
|
||||||
}
|
|
||||||
.rellanic {
|
|
||||||
mask-image: url('../icons/Rellanic.svg');
|
|
||||||
}
|
|
||||||
.iokharic {
|
|
||||||
mask-image: url('../icons/Iokharic.svg');
|
|
||||||
}
|
|
||||||
.zoom-to-fit {
|
|
||||||
mask-image: url('../icons/zoom-to-fit.svg');
|
|
||||||
}
|
|
||||||
.fit-width {
|
|
||||||
mask-image: url('../icons/fit-width.svg');
|
|
||||||
}
|
|
||||||
.single-spread {
|
|
||||||
mask-image: url('../icons/single-spread.svg');
|
|
||||||
}
|
|
||||||
.facing-spread {
|
|
||||||
mask-image: url('../icons/facing-spread.svg');
|
|
||||||
}
|
|
||||||
.flow-spread {
|
|
||||||
mask-image: url('../icons/flow-spread.svg');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const template = async function(name, title='', props = {}){
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
|
||||||
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" 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="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
<link href=${`/${name}/bundle.css`} type="text/css" rel='stylesheet' />
|
||||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
version: '2'
|
|
||||||
services:
|
services:
|
||||||
mongodb:
|
mongodb:
|
||||||
image: mongo:latest
|
image: mongo:latest
|
||||||
|
|||||||
3
font-awesome-source/README.md
Normal file
3
font-awesome-source/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# About
|
||||||
|
|
||||||
|
Run `deploy.bash` to download, extract, and deploy the font awesome files into place for building. Should only be needed when Font Awesome version changes and we want the new version.
|
||||||
42
font-awesome-source/deploy.bash
Normal file
42
font-awesome-source/deploy.bash
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Deploys the Font Awesome files for HB self-hosting to settle various issues.
|
||||||
|
|
||||||
|
THEURL=https://use.fontawesome.com/releases/v6.7.2/fontawesome-free-6.7.2-web.zip
|
||||||
|
THEFILE=fontawesome-free-6.7.2-web.zip
|
||||||
|
if [ ! "$(which wget)" ]; then
|
||||||
|
echo "Please manually download ${THEURL}"
|
||||||
|
exit -1
|
||||||
|
fi
|
||||||
|
|
||||||
|
wget ${THEURL}
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error downloading ${THEURL}"
|
||||||
|
exit -2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! "$(which unzip)" ]; then
|
||||||
|
echo "Please unzip the file with your tool of choice."
|
||||||
|
exit -3
|
||||||
|
fi
|
||||||
|
|
||||||
|
unzip fontawesome-free-6.7.2-web.zip
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error extracting ${THEFILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Copying fonts"
|
||||||
|
cp -rv fontawesome-free-*-web/webfonts/*.woff2 ../themes/fonts/iconFonts
|
||||||
|
echo "Copying and updating css"
|
||||||
|
|
||||||
|
echo "fontawesome-free.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/fontawesome.css > ../themes/fonts/iconFonts/fontawesome-free.less
|
||||||
|
|
||||||
|
echo "fontawesome-solid.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/solid.css > ../themes/fonts/iconFonts/fontawesome-solid.less
|
||||||
|
|
||||||
|
echo "fontawesome-brands.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/brands.css > ../themes/fonts/iconFonts/fontawesome-brands.less
|
||||||
|
|
||||||
|
echo "fontawesome-regular.less"
|
||||||
|
sed 's/..\/webfonts/\/fonts\/iconFonts/g' fontawesome-free-*-web/css/regular.css > ../themes/fonts/iconFonts/fontawesome-regular.less
|
||||||
@@ -24,12 +24,16 @@ These instructions assume that you are installing to a completely new, fresh Ubu
|
|||||||
|
|
||||||
These installation instructions have been tested on the following Ubuntu releases:
|
These installation instructions have been tested on the following Ubuntu releases:
|
||||||
|
|
||||||
- *ubuntu-20.04.3-desktop-amd64*
|
- *ubuntu-24.04.1-desktop-amd64*
|
||||||
|
- *ubuntu-22.04.5-desktop-amd64*
|
||||||
|
- *ubuntu-20.04.6-desktop-amd64*
|
||||||
|
|
||||||
## Final Notes
|
## 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.
|
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.
|
||||||
|
|
||||||
|
Earlier versions of Ubuntu may requier an alternate Mongo setup, see https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-ubuntu/ for assistance.
|
||||||
|
|
||||||
Regards,
|
Regards,
|
||||||
G
|
G
|
||||||
December 19, 2021
|
December 19, 2021
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ Description=Homebrewery Web Server
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=root
|
User=root
|
||||||
After=mongodb
|
BindsTo=mongod.service
|
||||||
|
After=mongod.service
|
||||||
Environment=NODE_ENV=local
|
Environment=NODE_ENV=local
|
||||||
WorkingDirectory=/usr/local/homebrewery
|
WorkingDirectory=/usr/local/homebrewery
|
||||||
ExecStart=node server.js
|
ExecStart=node server.js
|
||||||
|
|||||||
@@ -1,14 +1,60 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Detect Ubuntu Version
|
||||||
|
export DISTRO=$(grep "^NAME=" /etc/os-release | awk -F '=' '{print $2}' | sed 's/"//g')
|
||||||
|
export DISTRO_VER=$(grep "VERSION_ID=" /etc/os-release | awk -F '=' '{print $2}' | sed 's/"//g')
|
||||||
|
export MATCHED="Yes"
|
||||||
|
|
||||||
|
if [ "${DISTRO}" != "Ubuntu" ];
|
||||||
|
then
|
||||||
|
echo :: Ubuntu not detected. Are you using an alternate spin or derivative?
|
||||||
|
echo :: Detected - ${DISTRO}
|
||||||
|
read -p [y/N] YESNO
|
||||||
|
if [ "${YESNO}" != "Y" ] && [ ]"${YESNO}" != "y" ]; then
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
MATCHED="No"
|
||||||
|
fi
|
||||||
|
|
||||||
# Install CURL and add required NodeJS source to package repo
|
# Install CURL and add required NodeJS source to package repo
|
||||||
echo ::Install CURL
|
echo ::Install CURL
|
||||||
apt install -y curl
|
apt install -y curl
|
||||||
echo ::Add NodeJS source to package repo
|
echo ::Add NodeJS source to package repo
|
||||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||||
|
|
||||||
|
# Add Mongo CE Source
|
||||||
|
if [ ${DISTRO} = "Ubuntu" ];
|
||||||
|
then
|
||||||
|
echo ::Add Mongo CE source to package repo
|
||||||
|
curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | \
|
||||||
|
sudo gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg \
|
||||||
|
--dearmor
|
||||||
|
if [ "${DISTRO_VER}" == "24.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
elif [ "${DISTRO_VER}" == "22.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
elif [ "${DISTRO_VER}" == "20.04" ]; then
|
||||||
|
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/8.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-8.0.list
|
||||||
|
else
|
||||||
|
MATCHED="No"
|
||||||
|
fi
|
||||||
|
sudo apt-get update
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ${MATCHED} == "No" ]; then
|
||||||
|
echo :: WARNING
|
||||||
|
echo :: Unable to determine Ubuntu version for Mongo installation purposes.
|
||||||
|
echo :: Please check your spin/distro documentation to install Mongo CE and enable it on startup.
|
||||||
|
fi
|
||||||
|
|
||||||
# Install required packages
|
# Install required packages
|
||||||
echo ::Install Homebrewery requirements
|
echo ::Install Homebrewery requirements
|
||||||
apt satisfy -y git nodejs npm mongodb
|
apt satisfy -y git nodejs npm mongodb-org
|
||||||
|
|
||||||
|
# Enable and start Mongo
|
||||||
|
systemctl enable mongod
|
||||||
|
systemctl start mongod
|
||||||
|
|
||||||
# Clone Homebrewery repo
|
# Clone Homebrewery repo
|
||||||
echo ::Get Homebrewery files
|
echo ::Get Homebrewery files
|
||||||
|
|||||||
7051
package-lock.json
generated
7051
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"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": "3.16.1",
|
"version": "3.19.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.8.x",
|
||||||
"node": "^20.18.x"
|
"node": "^20.18.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
|
||||||
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
||||||
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace",
|
"test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace",
|
||||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
"lines": 50
|
"lines": 50
|
||||||
},
|
},
|
||||||
"server/homebrew.api.js": {
|
"server/homebrew.api.js": {
|
||||||
"statements": 70,
|
"statements": 60,
|
||||||
"branches": 50,
|
"branches": 50,
|
||||||
"functions": 65,
|
"functions": 65,
|
||||||
"lines": 70
|
"lines": 70
|
||||||
@@ -84,62 +83,72 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.27.1",
|
||||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
"@babel/plugin-transform-runtime": "^7.28.0",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.28.0",
|
||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@googleapis/drive": "^8.14.0",
|
"@babel/runtime": "^7.27.6",
|
||||||
"body-parser": "^1.20.2",
|
"@dmsnell/diff-match-patch": "^1.1.0",
|
||||||
|
"@googleapis/drive": "^13.0.1",
|
||||||
|
"@sanity/diff-match-patch": "^3.2.0",
|
||||||
|
"body-parser": "^2.2.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"core-js": "^3.39.0",
|
"core-js": "^3.44.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"dompurify": "^3.2.3",
|
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.2",
|
"express": "^5.1.0",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.2.0",
|
"express-static-gzip": "3.0.0",
|
||||||
"fs-extra": "11.2.0",
|
"fflate": "^0.8.2",
|
||||||
"idb-keyval": "^6.2.1",
|
"fs-extra": "11.3.0",
|
||||||
|
"hash-wasm": "^4.12.0",
|
||||||
|
"idb-keyval": "^6.2.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "11.2.0",
|
"marked": "15.0.12",
|
||||||
"marked-emoji": "^1.4.3",
|
"marked-alignment-paragraphs": "^1.0.0",
|
||||||
"marked-extended-tables": "^1.0.10",
|
"marked-definition-lists": "^1.0.1",
|
||||||
"marked-gfm-heading-id": "^3.2.0",
|
"marked-emoji": "^2.0.1",
|
||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-extended-tables": "^2.0.1",
|
||||||
|
"marked-gfm-heading-id": "^4.1.2",
|
||||||
|
"marked-nonbreaking-spaces": "^1.0.1",
|
||||||
|
"marked-smartypants-lite": "^1.0.3",
|
||||||
|
"marked-subsuper-text": "^1.0.3",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.9.2",
|
"mongoose": "^8.16.3",
|
||||||
"nanoid": "5.0.9",
|
"nanoid": "5.1.5",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.13.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router": "^7.0.2",
|
"react-router": "^7.6.3",
|
||||||
|
"romans": "^3.1.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.1",
|
"superagent": "^10.2.1",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git",
|
||||||
|
"written-number": "^0.11.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"@stylistic/stylelint-plugin": "^4.0.0",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"babel-plugin-transform-import-meta": "^2.3.3",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.35.0",
|
||||||
"eslint-plugin-jest": "^28.10.0",
|
"eslint-plugin-jest": "^29.0.1",
|
||||||
"eslint-plugin-react": "^7.37.2",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.14.0",
|
"globals": "^16.3.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^30.1.3",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom-global": "^3.0.2",
|
"jsdom-global": "^3.0.2",
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.12.0",
|
"stylelint": "^16.24.0",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^7.3.0",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^17.0.0",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import babel from '@babel/core';
|
|||||||
import babelConfig from '../babel.config.json' with { type : 'json' };
|
import babelConfig from '../babel.config.json' with { type : 'json' };
|
||||||
import less from 'less';
|
import less from 'less';
|
||||||
|
|
||||||
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
const isDev = !!process.argv.find((arg)=>arg === '--dev');
|
||||||
|
|
||||||
const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
|
const babelify = async (code)=>(await babel.transformAsync(code, babelConfig)).code;
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ fs.emptyDirSync('./build');
|
|||||||
const themes = { Legacy: {}, V3: {} };
|
const themes = { Legacy: {}, V3: {} };
|
||||||
|
|
||||||
let themeFiles = fs.readdirSync('./themes/Legacy');
|
let themeFiles = fs.readdirSync('./themes/Legacy');
|
||||||
for (let dir of themeFiles) {
|
for (const dir of themeFiles) {
|
||||||
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.Legacy[dir] = (themeData);
|
themes.Legacy[dir] = (themeData);
|
||||||
@@ -70,7 +70,7 @@ fs.emptyDirSync('./build');
|
|||||||
}
|
}
|
||||||
|
|
||||||
themeFiles = fs.readdirSync('./themes/V3');
|
themeFiles = fs.readdirSync('./themes/V3');
|
||||||
for (let dir of themeFiles) {
|
for (const dir of themeFiles) {
|
||||||
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.V3[dir] = (themeData);
|
themes.V3[dir] = (themeData);
|
||||||
@@ -113,7 +113,7 @@ fs.emptyDirSync('./build');
|
|||||||
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||||
stream.write('[\n"default"');
|
stream.write('[\n"default"');
|
||||||
|
|
||||||
for (let themeFile of editorThemeFiles) {
|
for (const themeFile of editorThemeFiles) {
|
||||||
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||||
}
|
}
|
||||||
stream.write('\n]\n');
|
stream.write('\n]\n');
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"codemirror/addon/selection/active-line.js",
|
"codemirror/addon/selection/active-line.js",
|
||||||
"codemirror/addon/hint/show-hint.js",
|
"codemirror/addon/hint/show-hint.js",
|
||||||
"moment",
|
"moment",
|
||||||
"superagent"
|
"superagent",
|
||||||
|
"@sanity/diff-match-patch",
|
||||||
|
"fflate"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
import { model as HomebrewModel } from './homebrew.model.js';
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
import { model as NotificationModel } from './notifications.model.js';
|
import { model as NotificationModel } from './notifications.model.js';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
@@ -11,6 +12,7 @@ import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|
||||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
||||||
|
|
||||||
@@ -93,7 +95,7 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
|||||||
|
|
||||||
/* Cleans `<script` and `</script>` from the "text" field of a brew */
|
/* Cleans `<script` and `</script>` from the "text" field of a brew */
|
||||||
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
|
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
|
||||||
console.log(`[ADMIN] Cleaning script tags from ShareID ${req.params.id}`);
|
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
|
||||||
|
|
||||||
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
|
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
|
||||||
|
|
||||||
@@ -114,6 +116,18 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin',
|
|||||||
return await HomebrewAPI.updateBrew(req, res);
|
return await HomebrewAPI.updateBrew(req, res);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Get list of a user's documents */
|
||||||
|
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
|
||||||
|
const username = req.params.user;
|
||||||
|
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
|
||||||
|
|
||||||
|
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`);
|
||||||
|
|
||||||
|
const brews = await HomebrewModel.getByUser(username, true, fields);
|
||||||
|
|
||||||
|
return res.json(brews);
|
||||||
|
});
|
||||||
|
|
||||||
/* Compresses the "text" field of a brew to binary */
|
/* Compresses the "text" field of a brew to binary */
|
||||||
router.put('/admin/compress/:id', (req, res)=>{
|
router.put('/admin/compress/:id', (req, res)=>{
|
||||||
HomebrewModel.findOne({ _id: req.params.id })
|
HomebrewModel.findOne({ _id: req.params.id })
|
||||||
@@ -135,7 +149,6 @@ router.put('/admin/compress/:id', (req, res)=>{
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
||||||
try {
|
try {
|
||||||
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
const totalBrewsCount = await HomebrewModel.countDocuments({});
|
||||||
@@ -151,6 +164,180 @@ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ####################### LOCKS
|
||||||
|
|
||||||
|
router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||||
|
|
||||||
|
const countLocksQuery = {
|
||||||
|
lock : { $exists: true }
|
||||||
|
};
|
||||||
|
const count = await HomebrewModel.countDocuments(countLocksQuery)
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { name: 'Lock Count Error', message: 'Unable to get lock count', status: 500, HBErrorCode: '61', error };
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ count });
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||||
|
const countLocksPipeline = [
|
||||||
|
{
|
||||||
|
$match :
|
||||||
|
{
|
||||||
|
'lock' : { '$exists': 1 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project : {
|
||||||
|
shareId : 1,
|
||||||
|
editId : 1,
|
||||||
|
title : 1,
|
||||||
|
lock : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { name: 'Can Not Get Locked Brews', message: 'Unable to get locked brew collection', status: 500, HBErrorCode: '68', error };
|
||||||
|
});
|
||||||
|
return res.json({
|
||||||
|
lockedDocuments
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||||
|
|
||||||
|
const lock = req.body;
|
||||||
|
|
||||||
|
lock.applied = new Date;
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
shareId : req.params.id
|
||||||
|
};
|
||||||
|
|
||||||
|
const brew = await HomebrewModel.findOne(filter);
|
||||||
|
|
||||||
|
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' };
|
||||||
|
|
||||||
|
if(brew.lock && !lock.overwrite) {
|
||||||
|
throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' };
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.overwrite = undefined;
|
||||||
|
|
||||||
|
brew.lock = lock;
|
||||||
|
brew.markModified('lock');
|
||||||
|
|
||||||
|
await brew.save()
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { name: 'Lock Error', message: 'Unable to set lock', shareId: req.params.id, status: 500, HBErrorCode: '62', error };
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock });
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
shareId : req.params.id
|
||||||
|
};
|
||||||
|
|
||||||
|
const brew = await HomebrewModel.findOne(filter);
|
||||||
|
|
||||||
|
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' };
|
||||||
|
|
||||||
|
if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' };
|
||||||
|
|
||||||
|
brew.lock = undefined;
|
||||||
|
brew.markModified('lock');
|
||||||
|
|
||||||
|
await brew.save()
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { name: 'Cannot Unlock', message: 'Unable to clear lock', shareId: req.params.id, status: 500, HBErrorCode: '65', error };
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` });
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||||
|
const countReviewsPipeline = [
|
||||||
|
{
|
||||||
|
$match :
|
||||||
|
{
|
||||||
|
'lock.reviewRequested' : { '$exists': 1 }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project : {
|
||||||
|
shareId : 1,
|
||||||
|
editId : 1,
|
||||||
|
title : 1,
|
||||||
|
lock : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { name: 'Can Not Get Reviews', message: 'Unable to get review collection', status: 500, HBErrorCode: '68', error };
|
||||||
|
});
|
||||||
|
return res.json({
|
||||||
|
reviewDocuments
|
||||||
|
});
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
|
||||||
|
// === This route is NOT Admin only ===
|
||||||
|
// Any user can request a review of their document
|
||||||
|
const filter = {
|
||||||
|
shareId : req.params.id,
|
||||||
|
lock : { $exists: 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const brew = await HomebrewModel.findOne(filter);
|
||||||
|
if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; };
|
||||||
|
|
||||||
|
if(brew.lock.reviewRequested){
|
||||||
|
throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' };
|
||||||
|
};
|
||||||
|
|
||||||
|
brew.lock.reviewRequested = new Date();
|
||||||
|
brew.markModified('lock');
|
||||||
|
|
||||||
|
await brew.save()
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { name: 'Can Not Set Review Request', message: `Unable to set request for review on brew ID ${req.params.id}`, code: 500, HBErrorCode: '69', error };
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` });
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
shareId : req.params.id,
|
||||||
|
'lock.reviewRequested' : { $exists: 1 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const brew = await HomebrewModel.findOne(filter);
|
||||||
|
if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; };
|
||||||
|
|
||||||
|
brew.lock.reviewRequested = undefined;
|
||||||
|
brew.markModified('lock');
|
||||||
|
|
||||||
|
await brew.save()
|
||||||
|
.catch((error)=>{
|
||||||
|
throw { name: 'Can Not Clear Review Request', message: `Unable to remove request for review on brew ID ${req.params.id}`, HBErrorCode: '72', error };
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` });
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
// ####################### NOTIFICATIONS
|
// ####################### NOTIFICATIONS
|
||||||
|
|
||||||
router.get('/admin/notification/all', async (req, res, next)=>{
|
router.get('/admin/notification/all', async (req, res, next)=>{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 1000, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
import supertest from 'supertest';
|
import supertest from 'supertest';
|
||||||
import HBApp from './app.js';
|
import HBApp from './app.js';
|
||||||
import {model as NotificationModel } from './notifications.model.js';
|
import { model as NotificationModel } from './notifications.model.js';
|
||||||
|
import { model as HomebrewModel } from './homebrew.model.js';
|
||||||
|
|
||||||
|
|
||||||
// Mimic https responses to avoid being redirected all the time
|
// Mimic https responses to avoid being redirected all the time
|
||||||
@@ -16,7 +18,7 @@ describe('Tests for admin api', ()=>{
|
|||||||
const testNotifications = ['a', 'b'];
|
const testNotifications = ['a', 'b'];
|
||||||
|
|
||||||
jest.spyOn(NotificationModel, 'find')
|
jest.spyOn(NotificationModel, 'find')
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(()=>{
|
||||||
return { exec: jest.fn().mockResolvedValue(testNotifications) };
|
return { exec: jest.fn().mockResolvedValue(testNotifications) };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,7 +61,7 @@ describe('Tests for admin api', ()=>{
|
|||||||
expect(response.body).toEqual(savedNotification);
|
expect(response.body).toEqual(savedNotification);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle error adding a notification without dismissKey', async () => {
|
it('should handle error adding a notification without dismissKey', async ()=>{
|
||||||
const inputNotification = {
|
const inputNotification = {
|
||||||
title : 'Test Notification',
|
title : 'Test Notification',
|
||||||
text : 'This is a test notification',
|
text : 'This is a test notification',
|
||||||
@@ -75,7 +77,7 @@ describe('Tests for admin api', ()=>{
|
|||||||
|
|
||||||
const response = await app
|
const response = await app
|
||||||
.post('/admin/notification/add')
|
.post('/admin/notification/add')
|
||||||
.set('Authorization', 'Basic ' + Buffer.from('admin:password3').toString('base64'))
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
.send(inputNotification);
|
.send(inputNotification);
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
@@ -86,14 +88,14 @@ describe('Tests for admin api', ()=>{
|
|||||||
const dismissKey = 'testKey';
|
const dismissKey = 'testKey';
|
||||||
|
|
||||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||||
.mockImplementationOnce((key) => {
|
.mockImplementationOnce((key)=>{
|
||||||
return { exec: jest.fn().mockResolvedValue(key) };
|
return { exec: jest.fn().mockResolvedValue(key) };
|
||||||
});
|
});
|
||||||
const response = await app
|
const response = await app
|
||||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||||
|
|
||||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body).toEqual({ dismissKey: 'testKey' });
|
expect(response.body).toEqual({ dismissKey: 'testKey' });
|
||||||
});
|
});
|
||||||
@@ -102,16 +104,602 @@ describe('Tests for admin api', ()=>{
|
|||||||
const dismissKey = 'testKey';
|
const dismissKey = 'testKey';
|
||||||
|
|
||||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(()=>{
|
||||||
return { exec: jest.fn().mockResolvedValue() };
|
return { exec: jest.fn().mockResolvedValue() };
|
||||||
});
|
});
|
||||||
const response = await app
|
const response = await app
|
||||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
.delete(`/admin/notification/delete/${dismissKey}`)
|
||||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
||||||
|
|
||||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({ 'dismissKey': 'testKey' });
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body).toEqual({ message: 'Notification not found' });
|
expect(response.body).toEqual({ message: 'Notification not found' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Locks', ()=>{
|
||||||
|
describe('Count', ()=>{
|
||||||
|
it('Count of all locked documents', async ()=>{
|
||||||
|
const testNumber = 16777216; // 8^8, because why not
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'countDocuments')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testNumber);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.get('/api/lock/count');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({ count: testNumber });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handle error while fetching count of locked documents', async ()=>{
|
||||||
|
jest.spyOn(HomebrewModel, 'countDocuments')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.get('/api/lock/count');
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '61',
|
||||||
|
message : 'Unable to get lock count',
|
||||||
|
name : 'Lock Count Error',
|
||||||
|
originalUrl : '/api/lock/count',
|
||||||
|
status : 500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Lists', ()=>{
|
||||||
|
it('Get list of all locked documents', async ()=>{
|
||||||
|
const testLocks = ['a', 'b'];
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'aggregate')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testLocks);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.get('/api/locks');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({ lockedDocuments: testLocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handle error while fetching list of all locked documents', async ()=>{
|
||||||
|
jest.spyOn(HomebrewModel, 'aggregate')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.get('/api/locks');
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '68',
|
||||||
|
message : 'Unable to get locked brew collection',
|
||||||
|
name : 'Can Not Get Locked Brews',
|
||||||
|
originalUrl : '/api/locks',
|
||||||
|
status : 500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Get list of all locked documents with pending review requests', async ()=>{
|
||||||
|
const testLocks = ['a', 'b'];
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'aggregate')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testLocks);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.get('/api/lock/reviews');
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({ reviewDocuments: testLocks });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handle error while fetching list of all locked documents with pending review requests', async ()=>{
|
||||||
|
jest.spyOn(HomebrewModel, 'aggregate')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.get('/api/lock/reviews');
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '68',
|
||||||
|
message : 'Unable to get review collection',
|
||||||
|
name : 'Can Not Get Reviews',
|
||||||
|
originalUrl : '/api/lock/reviews',
|
||||||
|
status : 500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Lock', ()=>{
|
||||||
|
it('Lock a brew', async ()=>{
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const testLock = {
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share'
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.post(`/api/lock/${testBrew.shareId}`)
|
||||||
|
.send(testLock);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
applied : expect.any(String),
|
||||||
|
code : testLock.code,
|
||||||
|
editMessage : testLock.editMessage,
|
||||||
|
shareMessage : testLock.shareMessage,
|
||||||
|
name : 'LOCKED',
|
||||||
|
message : `Lock applied to brew ID ${testBrew.shareId} - ${testBrew.title}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Overwrite lock on a locked brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'newEdit',
|
||||||
|
shareMessage : 'newShare',
|
||||||
|
overwrite : true
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); },
|
||||||
|
lock : {
|
||||||
|
code : 1,
|
||||||
|
editMessage : 'oldEdit',
|
||||||
|
shareMessage : 'oldShare',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.post(`/api/lock/${testBrew.shareId}`)
|
||||||
|
.send(testLock);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toMatchObject({
|
||||||
|
applied : expect.any(String),
|
||||||
|
code : testLock.code,
|
||||||
|
editMessage : testLock.editMessage,
|
||||||
|
shareMessage : testLock.shareMessage,
|
||||||
|
name : 'LOCKED',
|
||||||
|
message : `Lock applied to brew ID ${testBrew.shareId} - ${testBrew.title}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error when locking a locked brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'newEdit',
|
||||||
|
shareMessage : 'newShare'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); },
|
||||||
|
lock : {
|
||||||
|
code : 1,
|
||||||
|
editMessage : 'oldEdit',
|
||||||
|
shareMessage : 'oldShare',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.post(`/api/lock/${testBrew.shareId}`)
|
||||||
|
.send(testLock);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '64',
|
||||||
|
message : 'Lock already exists on brew',
|
||||||
|
name : 'Already Locked',
|
||||||
|
originalUrl : `/api/lock/${testBrew.shareId}`,
|
||||||
|
shareId : testBrew.shareId,
|
||||||
|
status : 500,
|
||||||
|
title : 'title'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handle save error while locking a brew', async ()=>{
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.reject(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const testLock = {
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share'
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.post(`/api/lock/${testBrew.shareId}`)
|
||||||
|
.send(testLock);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '62',
|
||||||
|
message : 'Unable to set lock',
|
||||||
|
name : 'Lock Error',
|
||||||
|
originalUrl : `/api/lock/${testBrew.shareId}`,
|
||||||
|
shareId : testBrew.shareId,
|
||||||
|
status : 500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Unlock', ()=>{
|
||||||
|
it('Unlock a brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
applied : 'YES',
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); },
|
||||||
|
lock : testLock
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.put(`/api/unlock/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
name : 'Unlocked',
|
||||||
|
message : `Lock removed from brew ID ${testBrew.shareId}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error when unlocking a brew with no lock', async ()=>{
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); },
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.put(`/api/unlock/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '67',
|
||||||
|
message : 'Cannot unlock as brew is not locked',
|
||||||
|
name : 'Not Locked',
|
||||||
|
originalUrl : `/api/unlock/${testBrew.shareId}`,
|
||||||
|
shareId : testBrew.shareId,
|
||||||
|
status : 500,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handle error while unlocking a brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
applied : 'YES',
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.reject(); },
|
||||||
|
lock : testLock
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.put(`/api/unlock/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '65',
|
||||||
|
message : 'Unable to clear lock',
|
||||||
|
name : 'Cannot Unlock',
|
||||||
|
originalUrl : `/api/unlock/${testBrew.shareId}`,
|
||||||
|
shareId : testBrew.shareId,
|
||||||
|
status : 500
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Reviews', ()=>{
|
||||||
|
it('Add review request to a locked brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
applied : 'YES',
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); },
|
||||||
|
lock : testLock
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
message : `Review requested on brew ID ${testBrew.shareId} - ${testBrew.title}`,
|
||||||
|
name : 'Review Requested',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error when cannot find a locked brew', async ()=>{
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId'
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.put(`/api/lock/review/request/${testBrew.shareId}`)
|
||||||
|
.catch((err)=>{return err;});
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
message : `Cannot find a locked brew with ID ${testBrew.shareId}`,
|
||||||
|
name : 'Brew Not Found',
|
||||||
|
HBErrorCode : '70',
|
||||||
|
code : 500,
|
||||||
|
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error when review is already requested', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
applied : 'YES',
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share',
|
||||||
|
reviewRequested : 'YES'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); },
|
||||||
|
lock : testLock
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.put(`/api/lock/review/request/${testBrew.shareId}`)
|
||||||
|
.catch((err)=>{return err;});
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '70',
|
||||||
|
code : 500,
|
||||||
|
message : `Cannot find a locked brew with ID ${testBrew.shareId}`,
|
||||||
|
name : 'Brew Not Found',
|
||||||
|
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handle error while adding review request to a locked brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
applied : 'YES',
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.reject(); },
|
||||||
|
lock : testLock
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.put(`/api/lock/review/request/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '69',
|
||||||
|
code : 500,
|
||||||
|
message : `Unable to set request for review on brew ID ${testBrew.shareId}`,
|
||||||
|
name : 'Can Not Set Review Request',
|
||||||
|
originalUrl : `/api/lock/review/request/${testBrew.shareId}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Clear review request from a locked brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
applied : 'YES',
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share',
|
||||||
|
reviewRequested : 'YES'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.resolve(); },
|
||||||
|
lock : testLock
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
message : `Review request removed for brew ID ${testBrew.shareId} - ${testBrew.title}`,
|
||||||
|
name : 'Review Request Cleared'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error when clearing review request from a brew with no review request', async ()=>{
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '73',
|
||||||
|
message : `Brew ID ${testBrew.shareId} does not have a review pending!`,
|
||||||
|
name : 'Can Not Clear Review Request',
|
||||||
|
originalUrl : `/api/lock/review/remove/${testBrew.shareId}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Handle error while clearing review request from a locked brew', async ()=>{
|
||||||
|
const testLock = {
|
||||||
|
applied : 'YES',
|
||||||
|
code : 999,
|
||||||
|
editMessage : 'edit',
|
||||||
|
shareMessage : 'share',
|
||||||
|
reviewRequested : 'YES'
|
||||||
|
};
|
||||||
|
|
||||||
|
const testBrew = {
|
||||||
|
shareId : 'shareId',
|
||||||
|
title : 'title',
|
||||||
|
markModified : ()=>{ return true; },
|
||||||
|
save : ()=>{ return Promise.reject(); },
|
||||||
|
lock : testLock
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(HomebrewModel, 'findOne')
|
||||||
|
.mockImplementationOnce(()=>{
|
||||||
|
return Promise.resolve(testBrew);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await app
|
||||||
|
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
||||||
|
.put(`/api/lock/review/remove/${testBrew.shareId}`);
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(response.body).toEqual({
|
||||||
|
HBErrorCode : '72',
|
||||||
|
message : `Unable to remove request for review on brew ID ${testBrew.shareId}`,
|
||||||
|
name : 'Can Not Clear Review Request',
|
||||||
|
originalUrl : `/api/lock/review/remove/${testBrew.shareId}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import packageJSON from './../package.json' with { type: 'json' };
|
import packageJSON from './../package.json' with { type: 'json' };
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
@@ -11,7 +11,6 @@ const version = packageJSON.version;
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import jwt from 'jwt-simple';
|
import jwt from 'jwt-simple';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import yaml from 'js-yaml';
|
|
||||||
import config from './config.js';
|
import config from './config.js';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
@@ -70,13 +69,11 @@ const corsOptions = {
|
|||||||
'https://homebrewery-stage.herokuapp.com',
|
'https://homebrewery-stage.herokuapp.com',
|
||||||
];
|
];
|
||||||
|
|
||||||
if(isLocalEnvironment) {
|
const localNetworkRegex = /^http:\/\/(localhost|127\.0\.0\.1|10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+):\d+$/;
|
||||||
allowedOrigins.push('http://localhost:8000', 'http://localhost:8010');
|
|
||||||
}
|
|
||||||
|
|
||||||
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
const herokuRegex = /^https:\/\/(?:homebrewery-pr-\d+\.herokuapp\.com|naturalcrit-pr-\d+\.herokuapp\.com)$/; // Matches any Heroku app
|
||||||
|
|
||||||
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin)) {
|
if(!origin || allowedOrigins.includes(origin) || herokuRegex.test(origin) || (isLocalEnvironment && localNetworkRegex.test(origin))) {
|
||||||
callback(null, true);
|
callback(null, true);
|
||||||
} else {
|
} else {
|
||||||
console.log(origin, 'not allowed');
|
console.log(origin, 'not allowed');
|
||||||
@@ -352,7 +349,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
app.put('/api/user/rename', async (req, res)=>{
|
app.put('/api/user/rename', async (req, res)=>{
|
||||||
const { username, newUsername } = req.body;
|
const { username, newUsername } = req.body;
|
||||||
const ownAccount = req.account && (req.account.username == newUsername);
|
const ownAccount = req.account && (req.account.username == newUsername);
|
||||||
|
|
||||||
if(!username || !newUsername)
|
if(!username || !newUsername)
|
||||||
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
return res.status(400).json({ error: 'Username and newUsername are required.' });
|
||||||
if(!ownAccount)
|
if(!ownAccount)
|
||||||
@@ -386,6 +383,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
|
|||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
|
locale : req.brew.lang,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -407,6 +405,7 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
|
|||||||
renderer : req.brew.renderer,
|
renderer : req.brew.renderer,
|
||||||
theme : req.brew.theme,
|
theme : req.brew.theme,
|
||||||
tags : req.brew.tags,
|
tags : req.brew.tags,
|
||||||
|
snippets : req.brew.snippets
|
||||||
};
|
};
|
||||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
@@ -436,7 +435,7 @@ app.get('/new', asyncHandler(async(req, res, next)=>{
|
|||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
const { brew } = req;
|
const { brew } = req;
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : `${req.brew.title || 'Untitled Brew'} - ${req.brew.authors[0] || 'No author.'}`,
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || defaultMetaTags.image,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
@@ -488,8 +487,8 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
const mongoCount = await HomebrewModel.countDocuments(query)
|
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
mongoCount = 0;
|
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
data.accountDetails = {
|
data.accountDetails = {
|
||||||
@@ -552,6 +551,7 @@ const renderPage = async (req, res)=>{
|
|||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
publicUrl : config.get('publicUrl') ?? '',
|
publicUrl : config.get('publicUrl') ?? '',
|
||||||
|
baseUrl : `${req.protocol}://${req.get('host')}`,
|
||||||
environment : nodeEnv,
|
environment : nodeEnv,
|
||||||
deployment : config.get('heroku_app_name') ?? ''
|
deployment : config.get('heroku_app_name') ?? ''
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user