Compare commits
1155 Commits
v3.3.1
...
editor-wid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8b427ea21 | ||
|
|
379f260de5 | ||
|
|
46f413c656 | ||
|
|
77b0e93dd3 | ||
|
|
1b2d8d46a6 | ||
|
|
62aae96012 | ||
|
|
bef6b94dc4 | ||
|
|
c113b8dd1f | ||
|
|
e6055bd417 | ||
|
|
4c087e9aa5 | ||
|
|
9d6a9c4ebf | ||
|
|
18c94f95f3 | ||
|
|
23f2f1f53b | ||
|
|
d044229b49 | ||
|
|
8e40cec051 | ||
|
|
ebbf0ca88b | ||
|
|
51760e02e7 | ||
|
|
f52d42bef5 | ||
|
|
3af5d27e3e | ||
|
|
9d64740678 | ||
|
|
053aadeff4 | ||
|
|
d3763beb15 | ||
|
|
8281051797 | ||
|
|
5b0104fc10 | ||
|
|
b3fa902d85 | ||
|
|
a22d223927 | ||
|
|
5c08926576 | ||
|
|
797ca7e64e | ||
|
|
5b8f2d8e3c | ||
|
|
bd0ef5da48 | ||
|
|
fe324d6822 | ||
|
|
3140299d73 | ||
|
|
96d04ad75a | ||
|
|
62532f788e | ||
|
|
69a3d04bb7 | ||
|
|
21017e45fe | ||
|
|
48474c6f7b | ||
|
|
3d318f8863 | ||
|
|
2b798f4ecb | ||
|
|
938802e1a3 | ||
|
|
14f825f3b5 | ||
|
|
37241a70eb | ||
|
|
bcd86a7f0c | ||
|
|
cf54594a4c | ||
|
|
535fdeaf62 | ||
|
|
77bf1b5258 | ||
|
|
63da418b60 | ||
|
|
67f5e53160 | ||
|
|
c6103d51c5 | ||
|
|
a4a10783f6 | ||
|
|
5ed53f75c5 | ||
|
|
81b289923a | ||
|
|
c3d8364789 | ||
|
|
82fec9901d | ||
|
|
173d0a726b | ||
|
|
e064219ca0 | ||
|
|
ec339f2717 | ||
|
|
d9d27808a8 | ||
|
|
a4584dc78e | ||
|
|
6344eaa17d | ||
|
|
5c41110e50 | ||
|
|
085cb99562 | ||
|
|
568586541a | ||
|
|
0d44e1778f | ||
|
|
4a5269e1f3 | ||
|
|
62cf0a4483 | ||
|
|
07c7352aa2 | ||
|
|
cf6c8bce88 | ||
|
|
45d32ebfc3 | ||
|
|
314275122d | ||
|
|
5716e4fcfd | ||
|
|
b9aaee43c2 | ||
|
|
35a74b3e46 | ||
|
|
f4fe08f8fd | ||
|
|
65c0c81984 | ||
|
|
84496f51ba | ||
|
|
4cf659e711 | ||
|
|
712f0309e9 | ||
|
|
e0bfef5231 | ||
|
|
afb6962407 | ||
|
|
b7be2d6463 | ||
|
|
47c84d9f01 | ||
|
|
8d2945ee5c | ||
|
|
1dad009298 | ||
|
|
aadf663623 | ||
|
|
8685f32b49 | ||
|
|
678ac90cd0 | ||
|
|
3cb5e8ed42 | ||
|
|
273f0ca05d | ||
|
|
3c929870cb | ||
|
|
36df5a3212 | ||
|
|
cea5f2e43a | ||
|
|
046845885d | ||
|
|
9713cc4be9 | ||
|
|
8baf0fc849 | ||
|
|
a7040e554a | ||
|
|
ba43055f32 | ||
|
|
d0de7ca28c | ||
|
|
c0164dce6a | ||
|
|
9e2b8477a8 | ||
|
|
5a32ae5cd4 | ||
|
|
e88e7f852c | ||
|
|
a3b2c6987f | ||
|
|
3d47b5a0bc | ||
|
|
c5f4793c23 | ||
|
|
10e14bfcfd | ||
|
|
f3c36ffb0a | ||
|
|
cff4f8eae5 | ||
|
|
4799e8b443 | ||
|
|
fa38d5c892 | ||
|
|
04eb7d0556 | ||
|
|
f175323221 | ||
|
|
9f4de3c66e | ||
|
|
800bff611a | ||
|
|
e28b6e7a19 | ||
|
|
4c6de90d82 | ||
|
|
e5ef0aedd3 | ||
|
|
da8e7ec610 | ||
|
|
d1412abe03 | ||
|
|
9de4a82977 | ||
|
|
9ddae7bbea | ||
|
|
4fdc6b79ea | ||
|
|
0001cf16d9 | ||
|
|
438cb7f26d | ||
|
|
ffa240f78d | ||
|
|
782aa8e658 | ||
|
|
7efe8964f1 | ||
|
|
853515e09e | ||
|
|
f6c5354ce0 | ||
|
|
6353341738 | ||
|
|
66b9a792e7 | ||
|
|
2775614eab | ||
|
|
32229c6e6e | ||
|
|
37c88b83f1 | ||
|
|
2fa1b2bb8b | ||
|
|
949d763e35 | ||
|
|
46cb2e6b5b | ||
|
|
e7224e97ef | ||
|
|
e07d53aa5f | ||
|
|
1e004977be | ||
|
|
9110e7cf7e | ||
|
|
27e8b54528 | ||
|
|
aa31919563 | ||
|
|
7bf3295fc2 | ||
|
|
9fd3f47689 | ||
|
|
0ca7e43d73 | ||
|
|
b33b3cd49b | ||
|
|
71c384ee0b | ||
|
|
546b8d5725 | ||
|
|
4d6ac2b142 | ||
|
|
ce538ebbfd | ||
|
|
cf17e73dfa | ||
|
|
69ef4d7653 | ||
|
|
c98224f3e4 | ||
|
|
4f870de68f | ||
|
|
2cfee2e8ad | ||
|
|
9e1d53a30c | ||
|
|
1fe9f0c8d0 | ||
|
|
adc7233cab | ||
|
|
1b2fc746d3 | ||
|
|
b472fc1115 | ||
|
|
a7a47afaae | ||
|
|
509c7d8832 | ||
|
|
caff1d8e2b | ||
|
|
e06f5e17d9 | ||
|
|
ade61971d0 | ||
|
|
6451d79d92 | ||
|
|
9202f9c8eb | ||
|
|
097cc220f8 | ||
|
|
2e3c10c35b | ||
|
|
a5aeb7dccd | ||
|
|
b6d37dd825 | ||
|
|
92ff776270 | ||
|
|
bf1fb97789 | ||
|
|
2cddc2debe | ||
|
|
0d4a1a11c1 | ||
|
|
aa3cf1d9c1 | ||
|
|
5d0062f610 | ||
|
|
7976917bb9 | ||
|
|
023071c874 | ||
|
|
7da42d3742 | ||
|
|
3269e94757 | ||
|
|
c69f4289ed | ||
|
|
8752a32626 | ||
|
|
8735d1f222 | ||
|
|
21929e676d | ||
|
|
5ca61935a8 | ||
|
|
10143cec93 | ||
|
|
643c8503c0 | ||
|
|
e92d3ecd68 | ||
|
|
4f092828ac | ||
|
|
2d4c211483 | ||
|
|
7d30abc4d9 | ||
|
|
1d513f7a0e | ||
|
|
44922f5261 | ||
|
|
f695cc6948 | ||
|
|
3722387f1f | ||
|
|
8950cb944f | ||
|
|
66fb70a5f8 | ||
|
|
69c242425b | ||
|
|
9093f610bd | ||
|
|
d2b2e69123 | ||
|
|
052c255068 | ||
|
|
e6ad8aefde | ||
|
|
43ae80e80d | ||
|
|
e6e04ad21d | ||
|
|
7be6b913b0 | ||
|
|
94b7c89252 | ||
|
|
c2e8967ed9 | ||
|
|
942fdb8095 | ||
|
|
95873ac158 | ||
|
|
3b4b0583cf | ||
|
|
2c9e3d2f2f | ||
|
|
5fca0a77d3 | ||
|
|
2c73e59eb0 | ||
|
|
f5db5c7bf2 | ||
|
|
37abc38426 | ||
|
|
855fabb89e | ||
|
|
bc35490ba2 | ||
|
|
8b5404606e | ||
|
|
c5d3605c11 | ||
|
|
43ab292391 | ||
|
|
3ed9702ef2 | ||
|
|
755b43179b | ||
|
|
66b827ee2f | ||
|
|
483a1c44ef | ||
|
|
47680f07df | ||
|
|
9e43986d24 | ||
|
|
7a198fe8b8 | ||
|
|
e3e5cb1dff | ||
|
|
f44ea92d4f | ||
|
|
5781b9d177 | ||
|
|
817539dfda | ||
|
|
4e083aece8 | ||
|
|
27a12dfa79 | ||
|
|
3b5ebf8f60 | ||
|
|
daea4419ff | ||
|
|
3b76a12505 | ||
|
|
dda3ba8215 | ||
|
|
6ea05d8ec2 | ||
|
|
71ec9034b7 | ||
|
|
86dce0ae24 | ||
|
|
1ebdf318bf | ||
|
|
f05e0db14b | ||
|
|
43fd6c451e | ||
|
|
e621f2d19b | ||
|
|
ca34ca499d | ||
|
|
0715e365f1 | ||
|
|
55d265069c | ||
|
|
52ee7d9dbf | ||
|
|
d0346650c4 | ||
|
|
96b26d72fd | ||
|
|
d51757b8b9 | ||
|
|
beccef2685 | ||
|
|
06f74c6b64 | ||
|
|
288b407e3e | ||
|
|
57eea5c69f | ||
|
|
fbfb92735c | ||
|
|
95376db055 | ||
|
|
01d3ec9d58 | ||
|
|
a1eb09225a | ||
|
|
5c2e2edbed | ||
|
|
4bb7d143aa | ||
|
|
f5cefc4db4 | ||
|
|
efbde81853 | ||
|
|
69a18d365a | ||
|
|
34e73ee69b | ||
|
|
ee1ee801a7 | ||
|
|
99d441d9ff | ||
|
|
d2be324bb0 | ||
|
|
6ceba54631 | ||
|
|
53e77718e1 | ||
|
|
0342dfed4c | ||
|
|
0864f4ced0 | ||
|
|
ebd729b78f | ||
|
|
32454a3f12 | ||
|
|
9781c8e633 | ||
|
|
8a2aacebeb | ||
|
|
5889c2f1e0 | ||
|
|
b135ce2ae9 | ||
|
|
8f2a114e1c | ||
|
|
11c8446c9c | ||
|
|
0e1b30eced | ||
|
|
b8372ebdcc | ||
|
|
42fdb0ebb1 | ||
|
|
b2ebf724f5 | ||
|
|
a4bea1c3be | ||
|
|
c800195e95 | ||
|
|
26ec222a33 | ||
|
|
618e594acf | ||
|
|
dde500004d | ||
|
|
1cf1750887 | ||
|
|
cbf281f211 | ||
|
|
34c73c3d09 | ||
|
|
9d61fc85a0 | ||
|
|
6825bb3bac | ||
|
|
0cb96f6fe6 | ||
|
|
2b7e0c3fb8 | ||
|
|
2cce7aebfc | ||
|
|
b5508b7a24 | ||
|
|
273dfdce40 | ||
|
|
1848dc8182 | ||
|
|
6fd26a2d0b | ||
|
|
528efc8b98 | ||
|
|
ef50b1966b | ||
|
|
2397f41b52 | ||
|
|
5554ad9c26 | ||
|
|
f5a07cac44 | ||
|
|
51dfd9a38c | ||
|
|
11da8b1dac | ||
|
|
22aed68200 | ||
|
|
1da329fb78 | ||
|
|
d455e8c270 | ||
|
|
e235c705ae | ||
|
|
f771e24788 | ||
|
|
55941f0318 | ||
|
|
ea38540e3b | ||
|
|
1500ed071f | ||
|
|
a568ab3b8a | ||
|
|
3c8660442b | ||
|
|
2525fa2a53 | ||
|
|
3f7aff587c | ||
|
|
00dd030ee2 | ||
|
|
a8179cae7b | ||
|
|
0425e61be2 | ||
|
|
a2ebb025a2 | ||
|
|
a43ea5abb9 | ||
|
|
1ceb1dccca | ||
|
|
d375cdf10b | ||
|
|
24639f1c29 | ||
|
|
62a9901676 | ||
|
|
c48dccb0d3 | ||
|
|
65c738d3b2 | ||
|
|
08ee142f6e | ||
|
|
891bf528cd | ||
|
|
45b7d7da88 | ||
|
|
f52321dd4b | ||
|
|
3b55cd7d88 | ||
|
|
f33b7b21bb | ||
|
|
ed6e64af8d | ||
|
|
cadbb422a9 | ||
|
|
b756a2f026 | ||
|
|
c8b8d40863 | ||
|
|
d369cad02c | ||
|
|
d92005a3c2 | ||
|
|
a2430c8744 | ||
|
|
8febaee2a9 | ||
|
|
29fd836965 | ||
|
|
ebf9cf9364 | ||
|
|
8b8388391c | ||
|
|
ba72f1ab22 | ||
|
|
9bb1cac547 | ||
|
|
5cf6c9b8bd | ||
|
|
4ddee3c2f1 | ||
|
|
0aac08f276 | ||
|
|
9690c6dac3 | ||
|
|
78ca5f5107 | ||
|
|
eb7d558c8d | ||
|
|
0e226ca8db | ||
|
|
14ac098882 | ||
|
|
d5dbf46fc4 | ||
|
|
bc83e1f84d | ||
|
|
b8e68f9a93 | ||
|
|
ebc90c998a | ||
|
|
aa0cc1ebf6 | ||
|
|
c5bd41acbf | ||
|
|
22b6b6a473 | ||
|
|
89ba709789 | ||
|
|
0720677824 | ||
|
|
fab4bfae27 | ||
|
|
f880c961bd | ||
|
|
ec7c083f90 | ||
|
|
99984e207f | ||
|
|
b5bd28ddd1 | ||
|
|
e9bd80aa0d | ||
|
|
89f0c7e127 | ||
|
|
70295fb227 | ||
|
|
2cb6acc090 | ||
|
|
83fac6a10f | ||
|
|
121da67b7a | ||
|
|
eaf7b9c4ef | ||
|
|
9e9bf8c6fa | ||
|
|
4cfe26b4a9 | ||
|
|
0e35b99289 | ||
|
|
b32c724c89 | ||
|
|
e5377c1939 | ||
|
|
a5e84694c1 | ||
|
|
48227eaf71 | ||
|
|
f06d30e4a6 | ||
|
|
333525d9ab | ||
|
|
69c283f00f | ||
|
|
9f17f36a87 | ||
|
|
b948106500 | ||
|
|
7000b911e7 | ||
|
|
7353e6c7ac | ||
|
|
773a9b5c82 | ||
|
|
e2b0b9e5d2 | ||
|
|
1c7540edcd | ||
|
|
4dc14101bc | ||
|
|
6016a60a3a | ||
|
|
ab51a93fb2 | ||
|
|
097d9aaacd | ||
|
|
d74acd2bdc | ||
|
|
dce880610d | ||
|
|
8958238342 | ||
|
|
d1dd5e34bd | ||
|
|
7529a4380b | ||
|
|
1b5b4154ed | ||
|
|
a1476582b0 | ||
|
|
0fbda91169 | ||
|
|
4b6f81ba34 | ||
|
|
5cdc1dda64 | ||
|
|
4bf61a063c | ||
|
|
6e99636296 | ||
|
|
8902b237ce | ||
|
|
1ef18fc53c | ||
|
|
ea00c1a5d6 | ||
|
|
d49a94498a | ||
|
|
591c45f59f | ||
|
|
435eec6e74 | ||
|
|
aed29952d6 | ||
|
|
527b704ccd | ||
|
|
02bcc9bfb9 | ||
|
|
d4b624186f | ||
|
|
a58a750b94 | ||
|
|
d793b6f690 | ||
|
|
d278c52571 | ||
|
|
1c38d30665 | ||
|
|
ab058b31b1 | ||
|
|
cdcd68bc92 | ||
|
|
38a5ebf779 | ||
|
|
7926a318d8 | ||
|
|
370c6ccf73 | ||
|
|
2ed669d95e | ||
|
|
5bce76bcba | ||
|
|
a92f5d0694 | ||
|
|
5b2aa452c0 | ||
|
|
46bc34d527 | ||
|
|
6a95ed57ca | ||
|
|
97f5a17d10 | ||
|
|
a106f6f814 | ||
|
|
1c90b3c4d6 | ||
|
|
db81d347bd | ||
|
|
d9423b9d50 | ||
|
|
220b5df559 | ||
|
|
67068221bd | ||
|
|
e28605338b | ||
|
|
9870ff369e | ||
|
|
c39a95f1e1 | ||
|
|
3b5aef7d71 | ||
|
|
dc1ef3dd3e | ||
|
|
262e79c4df | ||
|
|
43b9877fa4 | ||
|
|
d25cef0c49 | ||
|
|
bd9dfeb46c | ||
|
|
348ec5fd20 | ||
|
|
038088328e | ||
|
|
5d77dea652 | ||
|
|
0436235ec3 | ||
|
|
6431964807 | ||
|
|
bda9b455d9 | ||
|
|
c41b06eee1 | ||
|
|
402811fbec | ||
|
|
c7758e02a8 | ||
|
|
92f3fc9ff8 | ||
|
|
f7bd861d9f | ||
|
|
be39a6c7cc | ||
|
|
c6210280eb | ||
|
|
801f66c483 | ||
|
|
bdd898f5b6 | ||
|
|
1a87a5543f | ||
|
|
b24c604597 | ||
|
|
ded29dc390 | ||
|
|
ab5755e94e | ||
|
|
eaad46b6bc | ||
|
|
1ec08bb1fa | ||
|
|
9dda58991f | ||
|
|
844d2b7a06 | ||
|
|
9e2824e0be | ||
|
|
4f5f34c888 | ||
|
|
1b3ed2ad70 | ||
|
|
098de2afd3 | ||
|
|
fa762cf32f | ||
|
|
a4677956f6 | ||
|
|
efdc0b072e | ||
|
|
56e7355a0e | ||
|
|
ae8e2c9889 | ||
|
|
32543f5aa3 | ||
|
|
3587511e44 | ||
|
|
a8926503b7 | ||
|
|
a52ec1c330 | ||
|
|
c2349fb464 | ||
|
|
10263cbf7c | ||
|
|
6281ed044e | ||
|
|
25b5badf90 | ||
|
|
d743b72f9c | ||
|
|
33d8d51956 | ||
|
|
285b4c3b92 | ||
|
|
0a7ccfb89e | ||
|
|
db5469699e | ||
|
|
807ab2a538 | ||
|
|
d46736b7e6 | ||
|
|
b041ef921e | ||
|
|
064a92f0da | ||
|
|
3e73ae0327 | ||
|
|
bd4c24df46 | ||
|
|
1126481d53 | ||
|
|
a3f93c2602 | ||
|
|
24564a2750 | ||
|
|
4505308b81 | ||
|
|
cf99bd9004 | ||
|
|
75d97379f8 | ||
|
|
6588863d2d | ||
|
|
98ae938b3d | ||
|
|
ccf44cbe91 | ||
|
|
54ed9a7d33 | ||
|
|
4ec0107348 | ||
|
|
8106b2b694 | ||
|
|
c4d26e7ffe | ||
|
|
f148014a93 | ||
|
|
0a09cd9c67 | ||
|
|
3105ee1eac | ||
|
|
2fc1600865 | ||
|
|
d075b09496 | ||
|
|
22e275acd8 | ||
|
|
a5513b359e | ||
|
|
30e7c73805 | ||
|
|
e080bf1bde | ||
|
|
37c60feda1 | ||
|
|
aa76252a55 | ||
|
|
6f3292e994 | ||
|
|
eaa672c4c7 | ||
|
|
93ff59f670 | ||
|
|
461487534d | ||
|
|
7bef807c41 | ||
|
|
2a8eaa654d | ||
|
|
29f8f3546c | ||
|
|
c47fae6061 | ||
|
|
6ff0999d88 | ||
|
|
bfccf833b6 | ||
|
|
2a9ac9fa47 | ||
|
|
b990af3fc3 | ||
|
|
91a31757e5 | ||
|
|
c341bc5db6 | ||
|
|
c88253901a | ||
|
|
164e0a4433 | ||
|
|
97852c3c03 | ||
|
|
4057d7bf84 | ||
|
|
33ae652222 | ||
|
|
690e797fe5 | ||
|
|
ce2298ddd0 | ||
|
|
bbcf415a30 | ||
|
|
e67fc2f775 | ||
|
|
cf9dbffe49 | ||
|
|
c006ab0901 | ||
|
|
572b92f893 | ||
|
|
810a5b295d | ||
|
|
a956f57a56 | ||
|
|
9b4577c65b | ||
|
|
a5a59ac058 | ||
|
|
cc89ad1c7d | ||
|
|
ba11aef038 | ||
|
|
33f3fb18fa | ||
|
|
240d283536 | ||
|
|
9a60fe4129 | ||
|
|
6af98cd842 | ||
|
|
b2f64d1094 | ||
|
|
820582067e | ||
|
|
dae1106e50 | ||
|
|
239cc4d99e | ||
|
|
48cd8bb6f7 | ||
|
|
480d7d2b5e | ||
|
|
9ea80c0d31 | ||
|
|
bd151efd51 | ||
|
|
0ac1d1e87a | ||
|
|
a626f394df | ||
|
|
7df2d39a6d | ||
|
|
afa3fb434e | ||
|
|
3cdb15ba79 | ||
|
|
7c09956d7d | ||
|
|
63b088762e | ||
|
|
7117d3caed | ||
|
|
3461c47a63 | ||
|
|
4f715a316b | ||
|
|
7a316060dd | ||
|
|
a96f9e2b76 | ||
|
|
cc3c429b58 | ||
|
|
35c257ed50 | ||
|
|
a6ed05214a | ||
|
|
27ea00e9ce | ||
|
|
8a6cc3c0aa | ||
|
|
23668f15f0 | ||
|
|
ac8c79ee63 | ||
|
|
9bb628da5a | ||
|
|
5ae61e2c26 | ||
|
|
2e7c8c0cab | ||
|
|
ed85e4eb0a | ||
|
|
91a2ce211d | ||
|
|
6e859aec1c | ||
|
|
4d7e273f7d | ||
|
|
c3d53cfc10 | ||
|
|
90d5ac4603 | ||
|
|
096b3d00fc | ||
|
|
2a75c702b9 | ||
|
|
c4719f0e0f | ||
|
|
fe6223c892 | ||
|
|
5f01fa1add | ||
|
|
66c80c3891 | ||
|
|
767f03fba0 | ||
|
|
62692552b1 | ||
|
|
8b3bef02e8 | ||
|
|
3a6e4ec385 | ||
|
|
d39148116b | ||
|
|
81f0670be0 | ||
|
|
4484035d75 | ||
|
|
f528b55226 | ||
|
|
9eb8653dfb | ||
|
|
a65c26e806 | ||
|
|
17525a4f41 | ||
|
|
fe5e91c377 | ||
|
|
512ef78e9e | ||
|
|
4fca81fd3f | ||
|
|
6a8a2f080c | ||
|
|
298a10bc42 | ||
|
|
b6a95a2c01 | ||
|
|
b09a4ea5fa | ||
|
|
1fb7fe2487 | ||
|
|
e751f9a25c | ||
|
|
9e2aec18ad | ||
|
|
e8cf3bee34 | ||
|
|
41db670e63 | ||
|
|
74e14ca72a | ||
|
|
860070b0c2 | ||
|
|
43d1cb12db | ||
|
|
2bab0b50d2 | ||
|
|
4b98c1ac89 | ||
|
|
d9f5e9d635 | ||
|
|
1ce3e399a8 | ||
|
|
4a12ccf2b6 | ||
|
|
da83e1a252 | ||
|
|
a971dd4713 | ||
|
|
0a7e25a749 | ||
|
|
c9ee1bc4b0 | ||
|
|
d5c21c5881 | ||
|
|
3d6842bf86 | ||
|
|
e610194ace | ||
|
|
3393e72481 | ||
|
|
f644740f60 | ||
|
|
0355b52021 | ||
|
|
c430e5a92b | ||
|
|
2ac09130a6 | ||
|
|
63346cfd7a | ||
|
|
2ea639000b | ||
|
|
051fdbbc60 | ||
|
|
d02fcc9252 | ||
|
|
f157b57928 | ||
|
|
e6318fefb6 | ||
|
|
dae97946dc | ||
|
|
a921452c22 | ||
|
|
b0954bcd2c | ||
|
|
01f90ea085 | ||
|
|
33c8291a55 | ||
|
|
270b20aaaf | ||
|
|
0689d3d0b2 | ||
|
|
877ee1cc4f | ||
|
|
5f26f857d4 | ||
|
|
f7783aba07 | ||
|
|
7ea3ef8474 | ||
|
|
a0af588d75 | ||
|
|
f7dd03c628 | ||
|
|
3380befe21 | ||
|
|
14507c388e | ||
|
|
d4d2356708 | ||
|
|
6d9a0938ab | ||
|
|
747bb3c489 | ||
|
|
4a695eda87 | ||
|
|
208a341f27 | ||
|
|
75a4cff319 | ||
|
|
72e5515830 | ||
|
|
e5996db4f7 | ||
|
|
6f70994a77 | ||
|
|
9b06380b9c | ||
|
|
d84fe3e5b4 | ||
|
|
83a62e7133 | ||
|
|
5e38ff66da | ||
|
|
5eb1d58059 | ||
|
|
096e281a6c | ||
|
|
73f141613e | ||
|
|
aee240cf9b | ||
|
|
7af2e8810a | ||
|
|
24031fbfa0 | ||
|
|
b93e9ec95e | ||
|
|
347d132cde | ||
|
|
98c7ef9d12 | ||
|
|
5cc654a908 | ||
|
|
85b3741df8 | ||
|
|
4260f5197c | ||
|
|
f628e1a5ae | ||
|
|
14bc7e45ab | ||
|
|
4668d36a52 | ||
|
|
91fc8a1a5e | ||
|
|
341b53c603 | ||
|
|
6226f3376a | ||
|
|
ed8b0c51af | ||
|
|
a19b961b0f | ||
|
|
f4326616b4 | ||
|
|
e8509dbbed | ||
|
|
541109e7b1 | ||
|
|
625d3f7034 | ||
|
|
78554603e8 | ||
|
|
1ebbd9c8ce | ||
|
|
5bb61a7c3c | ||
|
|
3c551daf16 | ||
|
|
0bf96cb9e2 | ||
|
|
bef4d9f41f | ||
|
|
9ff0789387 | ||
|
|
a457058041 | ||
|
|
6d40e63e96 | ||
|
|
964342a13d | ||
|
|
9067cbdced | ||
|
|
c9fd2c63af | ||
|
|
ec8126eb8a | ||
|
|
6aa1920061 | ||
|
|
07325a7550 | ||
|
|
7fa50099a6 | ||
|
|
4c42a9e2fc | ||
|
|
4ae751bf70 | ||
|
|
b77c70054a | ||
|
|
12b1c41716 | ||
|
|
62d193ddd1 | ||
|
|
750c650b69 | ||
|
|
70219f1bd4 | ||
|
|
c8f9e5bcf2 | ||
|
|
0a9a5909d1 | ||
|
|
8f75ea4728 | ||
|
|
91ad46b202 | ||
|
|
752806c0ef | ||
|
|
c39bb67bf3 | ||
|
|
fe6097e0d9 | ||
|
|
6ddf0bb889 | ||
|
|
8dcbad97df | ||
|
|
64b3955bc9 | ||
|
|
055773b1c3 | ||
|
|
c3936d28d9 | ||
|
|
607a66a8f0 | ||
|
|
658aee8806 | ||
|
|
c60be5d24b | ||
|
|
3d5a8b5627 | ||
|
|
d11508886b | ||
|
|
4bb64a535f | ||
|
|
c956100416 | ||
|
|
458d104866 | ||
|
|
029230e075 | ||
|
|
0f88a13635 | ||
|
|
e8c7d38608 | ||
|
|
6020657529 | ||
|
|
a8c35f3967 | ||
|
|
62fa8f511a | ||
|
|
18cf202ac0 | ||
|
|
98d9018e13 | ||
|
|
a9e14f6165 | ||
|
|
7005e9f760 | ||
|
|
163bc4e0b3 | ||
|
|
3420886192 | ||
|
|
1704ef6557 | ||
|
|
637b3311d6 | ||
|
|
4b8c34bcbd | ||
|
|
0c68e5870b | ||
|
|
82622744a1 | ||
|
|
d5a34eedb9 | ||
|
|
d0dc83cf10 | ||
|
|
c997908a49 | ||
|
|
45c7ac4b85 | ||
|
|
128b299f25 | ||
|
|
692daa88fc | ||
|
|
bd6eb816f2 | ||
|
|
2050028484 | ||
|
|
7e4018351d | ||
|
|
01b03e8683 | ||
|
|
3538aa6da0 | ||
|
|
13eb6cac17 | ||
|
|
bd594fa214 | ||
|
|
8b0203dd7c | ||
|
|
6bae21a578 | ||
|
|
79db97efdf | ||
|
|
7755affa1e | ||
|
|
6b6e05ca11 | ||
|
|
ea04069fe5 | ||
|
|
2c3a302c85 | ||
|
|
89d9bfe1f1 | ||
|
|
c9241e2162 | ||
|
|
99f7668901 | ||
|
|
187738ee11 | ||
|
|
68d3724e50 | ||
|
|
8526faa041 | ||
|
|
ac9fbb1f08 | ||
|
|
e82da3a6c1 | ||
|
|
40e36fd875 | ||
|
|
3c86984cf1 | ||
|
|
50d172bbd5 | ||
|
|
a22d59475e | ||
|
|
da676c6ec1 | ||
|
|
901615d99e | ||
|
|
6ed52f37cc | ||
|
|
15e5a31767 | ||
|
|
8cbab4d4ce | ||
|
|
a9c28e84d0 | ||
|
|
bafabb84b4 | ||
|
|
f5d592a291 | ||
|
|
aa23da6ce4 | ||
|
|
385bee964d | ||
|
|
18f277182a | ||
|
|
344a9d8334 | ||
|
|
67a76f9d86 | ||
|
|
93d6d1ac6a | ||
|
|
4b3edf053f | ||
|
|
0720ac6a15 | ||
|
|
c0b9cd951e | ||
|
|
a54ebabf53 | ||
|
|
676acb9e5a | ||
|
|
387c468d84 | ||
|
|
64e7fe3422 | ||
|
|
2b1466c51b | ||
|
|
332bcde4b2 | ||
|
|
db783179ce | ||
|
|
c78d687387 | ||
|
|
b717059a39 | ||
|
|
e02bde5eed | ||
|
|
e1765cad41 | ||
|
|
6dab5f836e | ||
|
|
e47f81698c | ||
|
|
2b824839c3 | ||
|
|
26f009a295 | ||
|
|
9b4c997b57 | ||
|
|
39143295b1 | ||
|
|
6eaf2feb40 | ||
|
|
4fe0f1a7af | ||
|
|
7aac377ce8 | ||
|
|
77542c5f06 | ||
|
|
3d365339e4 | ||
|
|
66be400f5f | ||
|
|
9880792c4d | ||
|
|
ca03a473ec | ||
|
|
c364856942 | ||
|
|
4e7a96b67c | ||
|
|
39bfc818a6 | ||
|
|
6d9982f735 | ||
|
|
4f6ba7a388 | ||
|
|
0274fb214c | ||
|
|
15519f142d | ||
|
|
6b258886a4 | ||
|
|
d23a88c997 | ||
|
|
096e17ab5a | ||
|
|
da9e20e96f | ||
|
|
53d5f9f6e0 | ||
|
|
43b4fe75e2 | ||
|
|
925db96b08 | ||
|
|
efdb8e07b0 | ||
|
|
816860dc4f | ||
|
|
314f758d62 | ||
|
|
c799aaa7cb | ||
|
|
2f5bc8db54 | ||
|
|
7c61a27084 | ||
|
|
ad3e83da22 | ||
|
|
8888704b58 | ||
|
|
a3dc5e78fd | ||
|
|
32506860dd | ||
|
|
a89b20584f | ||
|
|
8e42c09721 | ||
|
|
bb5978dfea | ||
|
|
0dc491adfc | ||
|
|
f470a6185a | ||
|
|
e5febc1fef | ||
|
|
354d01e980 | ||
|
|
63e043593a | ||
|
|
770d0c141d | ||
|
|
0a885c8581 | ||
|
|
ec9c704e71 | ||
|
|
02c0176070 | ||
|
|
f847de852b | ||
|
|
86413b5767 | ||
|
|
747c976a14 | ||
|
|
326c28a11d | ||
|
|
263471bcbb | ||
|
|
9478454063 | ||
|
|
a9a9804517 | ||
|
|
0bde44ec2f | ||
|
|
13ad179a1b | ||
|
|
b72acd9e59 | ||
|
|
d0a1ef9571 | ||
|
|
d1f049871f | ||
|
|
070184b309 | ||
|
|
cbb41676e0 | ||
|
|
81130dd514 | ||
|
|
61d3edca17 | ||
|
|
248b56a706 | ||
|
|
90f8d1d6da | ||
|
|
8f08b71475 | ||
|
|
cc6527029c | ||
|
|
8a110567fc | ||
|
|
a451e562fb | ||
|
|
4e2f6b1d26 | ||
|
|
6b8db74a2b | ||
|
|
4c629772cc | ||
|
|
208593d203 | ||
|
|
99019be152 | ||
|
|
fa73e1707d | ||
|
|
d7de2e3d21 | ||
|
|
903ff4fd09 | ||
|
|
feaabacc94 | ||
|
|
a5827f66c9 | ||
|
|
bc0846c190 | ||
|
|
ecdcaadfa9 | ||
|
|
db2478f73d | ||
|
|
5d6a7e692f | ||
|
|
0fbeca1536 | ||
|
|
f5e1869a9b | ||
|
|
10c8414ab0 | ||
|
|
6832e68ada | ||
|
|
24477327aa | ||
|
|
52b11d9b38 | ||
|
|
c7b239f362 | ||
|
|
f1b67ad9d6 | ||
|
|
6279a85d2e | ||
|
|
7a4b5cca04 | ||
|
|
c4eff2478a | ||
|
|
15c918b5d4 | ||
|
|
0a5bfe2939 | ||
|
|
05b9bbdf59 | ||
|
|
b88c89d61b | ||
|
|
31d58f9075 | ||
|
|
743d0fa689 | ||
|
|
3b6d3963f1 | ||
|
|
3a4c2d8f13 | ||
|
|
7d86a40261 | ||
|
|
5527aa7990 | ||
|
|
f48484520a | ||
|
|
5957ddd01c | ||
|
|
994b1584b7 | ||
|
|
9647fbcc74 | ||
|
|
ca6f8d085a | ||
|
|
76d73e0e02 | ||
|
|
9e289ad6d1 | ||
|
|
914a4586b9 | ||
|
|
12fe787ab4 | ||
|
|
c2637a7576 | ||
|
|
fc7c46cfec | ||
|
|
4fd1cdd7e8 | ||
|
|
ad20ff514a | ||
|
|
5cd50f7138 | ||
|
|
89e6bada56 | ||
|
|
420d703f9d | ||
|
|
090da33f33 | ||
|
|
134e260d7b | ||
|
|
82c8ca55fc | ||
|
|
e92cd3be18 | ||
|
|
8d80f699b6 | ||
|
|
e1ff34ebaa | ||
|
|
ce732778bb | ||
|
|
b54d225f11 | ||
|
|
93c2d2d860 | ||
|
|
cd79a38755 | ||
|
|
fb95039368 | ||
|
|
60ca9530a3 | ||
|
|
70d8491e7c | ||
|
|
a9ff1cded7 | ||
|
|
417f9d7291 | ||
|
|
adb261f8f1 | ||
|
|
678c1fb3d8 | ||
|
|
e4a9c73f56 | ||
|
|
84286b7942 | ||
|
|
a16ca8c897 | ||
|
|
1818ea1e3b | ||
|
|
fec1766e26 | ||
|
|
fea07429fe | ||
|
|
fa29f2281d | ||
|
|
002b88ab83 | ||
|
|
54833f9fc6 | ||
|
|
d4b803205e | ||
|
|
a16f12546a | ||
|
|
cba9286217 | ||
|
|
9ebbff49fb | ||
|
|
b3343e5981 | ||
|
|
0061040cb6 | ||
|
|
c2d79bedb5 | ||
|
|
9ce79fbe3f | ||
|
|
806dcb04eb | ||
|
|
d5ea4c4e77 | ||
|
|
f26e3d6cd1 | ||
|
|
9ee39a83c1 | ||
|
|
c09a4c4374 | ||
|
|
3e44d0ad13 | ||
|
|
b23a09b1b8 | ||
|
|
421ed8f51d | ||
|
|
14a057cf55 | ||
|
|
2e6fcafc68 | ||
|
|
13b43e8902 | ||
|
|
837708fc0c | ||
|
|
2e305d5636 | ||
|
|
f9711de634 | ||
|
|
2c6779bb1c | ||
|
|
ee05392d27 | ||
|
|
1cc84da976 | ||
|
|
5cfdd504c1 | ||
|
|
24debfc75c | ||
|
|
ffe6272299 | ||
|
|
64c3d69641 | ||
|
|
67c19b79e3 | ||
|
|
ea29106101 | ||
|
|
8016f82040 | ||
|
|
7c93e5879c | ||
|
|
dc86f89c4f | ||
|
|
d03be052aa | ||
|
|
fef79f4fc3 | ||
|
|
cc58721ccd | ||
|
|
5f2115da0e | ||
|
|
1dd1e677e4 | ||
|
|
29b89bdc00 | ||
|
|
4493d86fd5 | ||
|
|
6a600df19a | ||
|
|
e8c6e36521 | ||
|
|
3e3610a204 | ||
|
|
3e626d91f0 | ||
|
|
6a0d8d13b0 | ||
|
|
4f762b376f | ||
|
|
bda80c9984 | ||
|
|
7de60e2345 | ||
|
|
77ad2c8958 | ||
|
|
6cca821ba6 | ||
|
|
96da053717 | ||
|
|
3723006f39 | ||
|
|
6cfdc47760 | ||
|
|
00a5600768 | ||
|
|
d8674d09a2 | ||
|
|
a46630f774 | ||
|
|
38fb9d467c | ||
|
|
fee56ca8e0 | ||
|
|
de60419926 | ||
|
|
42a1410cc2 | ||
|
|
ddd4f93f01 | ||
|
|
21de3b31b6 | ||
|
|
aea25119c0 | ||
|
|
aa1100642d | ||
|
|
4121cc0e14 | ||
|
|
3b7b56c789 | ||
|
|
ebc03aee33 | ||
|
|
03c6edf31a | ||
|
|
970d03a5e4 | ||
|
|
ed4c090f21 | ||
|
|
672d582caf | ||
|
|
b6c09683be | ||
|
|
a13fd48cda | ||
|
|
aec1147aa5 | ||
|
|
78da48b08d | ||
|
|
edce191c29 | ||
|
|
8c52a253dc | ||
|
|
589ec0251a | ||
|
|
ffddc275c1 | ||
|
|
4e26bb309c | ||
|
|
7696be5d95 | ||
|
|
ef80c23034 | ||
|
|
1f7be69624 | ||
|
|
6e069dc29d | ||
|
|
e40bbf56c7 | ||
|
|
65beb8d65e | ||
|
|
ebf4f614c4 | ||
|
|
26942e276a | ||
|
|
0bcce67e39 | ||
|
|
3318ba6277 | ||
|
|
c5eb7db432 | ||
|
|
f35345f385 | ||
|
|
a13ac2e0c5 | ||
|
|
27eb95ece8 | ||
|
|
7958bb4cda | ||
|
|
8fc01ebb12 | ||
|
|
13eedc9f82 | ||
|
|
6f919bc214 | ||
|
|
496328d511 | ||
|
|
d6af56c51c | ||
|
|
079d59695f | ||
|
|
0867b142da | ||
|
|
ac7b2bce9f | ||
|
|
4664d88aad | ||
|
|
8ca44653e9 | ||
|
|
0b5d9714c0 | ||
|
|
b0110f20d2 | ||
|
|
9e227dadd2 | ||
|
|
a214f27e9c | ||
|
|
2d3b03a9c3 | ||
|
|
af094474b8 | ||
|
|
d92d00581a | ||
|
|
5c3f7b1b82 | ||
|
|
bdf1bd1e8b | ||
|
|
df6d372243 | ||
|
|
b237456420 | ||
|
|
52a79b4f75 | ||
|
|
3f0c950e72 | ||
|
|
2b71ff2dec | ||
|
|
9ccf9d0a83 | ||
|
|
1801691f49 | ||
|
|
04eebae3ec | ||
|
|
23a0a89ead | ||
|
|
a7a67621a1 | ||
|
|
bebb06a36d | ||
|
|
9ad915c14a | ||
|
|
9fd5fea50c | ||
|
|
41fa0f2c77 | ||
|
|
c30eb9056a | ||
|
|
ee3cd21486 | ||
|
|
5de63799c2 | ||
|
|
dfa8aea1ec | ||
|
|
1d8781da90 | ||
|
|
93918bc26c | ||
|
|
8e26161244 | ||
|
|
89c091c630 | ||
|
|
7ae939623c | ||
|
|
bc0ee8138e | ||
|
|
91b2911bb0 | ||
|
|
9848dc54ba | ||
|
|
9a3bd4db4b | ||
|
|
891bde6990 | ||
|
|
d9228b8c4b | ||
|
|
5a3daf8ffd | ||
|
|
b8d7d1a8e4 | ||
|
|
5469ec6683 | ||
|
|
764621f762 | ||
|
|
248687684a | ||
|
|
d7b1f89152 | ||
|
|
9a844dae39 | ||
|
|
5193271796 | ||
|
|
ed5bef27e0 | ||
|
|
be25e90009 | ||
|
|
ab98bf5d6c | ||
|
|
ffb1c77697 | ||
|
|
6bddba6762 | ||
|
|
57b0af54df | ||
|
|
0ac50017c4 | ||
|
|
9135ca1e43 | ||
|
|
45aa8bdfae | ||
|
|
4dc1a60934 | ||
|
|
bce7cf41af | ||
|
|
8c6fd3086c | ||
|
|
d079985e6c | ||
|
|
aba2746f89 | ||
|
|
4f154922c0 | ||
|
|
2b1fe5d3fe | ||
|
|
03402e4342 | ||
|
|
1d85eede43 | ||
|
|
61f4456842 | ||
|
|
c925e04f3c | ||
|
|
00412a70e9 | ||
|
|
0923c50218 | ||
|
|
fe536bc9df | ||
|
|
e8937a285c | ||
|
|
2019d91711 |
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- node/install-npm
|
- run: sudo npm install -g npm@8.10.0
|
||||||
- node/install-packages:
|
- node/install-packages:
|
||||||
app-dir: ~/homebrewery
|
app-dir: ~/homebrewery
|
||||||
cache-path: node_modules
|
cache-path: node_modules
|
||||||
@@ -48,22 +48,28 @@ jobs:
|
|||||||
- image: cimg/node:16.11.0
|
- image: cimg/node:16.11.0
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
parallelism: 4
|
parallelism: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: .
|
at: .
|
||||||
|
|
||||||
# run tests!
|
# run tests!
|
||||||
- run:
|
- run:
|
||||||
|
name: Test - API Unit Tests
|
||||||
|
command: npm run test:api-unit
|
||||||
|
- run:
|
||||||
name: Test - Basic
|
name: Test - Basic
|
||||||
command: npm run test:basic
|
command: npm run test:basic
|
||||||
- run:
|
- run:
|
||||||
name: Test - Mustache Spans
|
name: Test - Mustache Spans
|
||||||
command: npm run test:mustache-span
|
command: npm run test:mustache-syntax
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
|
- run:
|
||||||
|
name: Test - Coverage
|
||||||
|
command: npm run test:coverage
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
build_and_test:
|
build_and_test:
|
||||||
@@ -71,4 +77,4 @@ workflows:
|
|||||||
- build
|
- build
|
||||||
- test:
|
- test:
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ module.exports = {
|
|||||||
browser : true,
|
browser : true,
|
||||||
node : true
|
node : true
|
||||||
},
|
},
|
||||||
plugins : ['react'],
|
plugins : ['react', 'jest'],
|
||||||
rules : {
|
rules : {
|
||||||
/** Errors **/
|
/** Errors **/
|
||||||
'camelcase' : ['error', { properties: 'never' }],
|
'camelcase' : ['error', { properties: 'never' }],
|
||||||
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||||
'no-array-constructor' : 'error',
|
'no-array-constructor' : 'error',
|
||||||
'no-iterator' : 'error',
|
'no-iterator' : 'error',
|
||||||
'no-nested-ternary' : 'error',
|
'no-nested-ternary' : 'error',
|
||||||
@@ -24,6 +24,7 @@ module.exports = {
|
|||||||
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
||||||
'react/jsx-uses-react' : 'error',
|
'react/jsx-uses-react' : 'error',
|
||||||
'react/prefer-es6-class' : ['error', 'never'],
|
'react/prefer-es6-class' : ['error', 'never'],
|
||||||
|
'jest/valid-expect' : ['error', { maxArgs: 3 }],
|
||||||
|
|
||||||
/** Warnings **/
|
/** Warnings **/
|
||||||
'max-lines' : ['warn', {
|
'max-lines' : ['warn', {
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -12,10 +12,6 @@ body:
|
|||||||
description: The best feature requests provide an explanation of the current issue and then an explanation of how it could be improved. Screenshots/images can be pasted right in as well!
|
description: The best feature requests provide an explanation of the current issue and then an explanation of how it could be improved. Screenshots/images can be pasted right in as well!
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
id: terms
|
|
||||||
attributes:
|
attributes:
|
||||||
label: "Please confirm:"
|
value: "Please be sure to search for any close matches to your request in the GitHub Issues tracker before opening a new request, thanks!"
|
||||||
options:
|
|
||||||
- label: I have searched the Issues tracker for any duplicate requests and found none.
|
|
||||||
required: true
|
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
@@ -4,14 +4,15 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: Please include as much information as possible.
|
value: Please include as much information as possible.
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: renderer
|
id: renderer
|
||||||
attributes:
|
attributes:
|
||||||
label: Renderer
|
label: Renderer
|
||||||
description: Which renderer does this issue occur on? If you are unsure, you can check the renderer in the Properties Editor (click the "i" in the Snippet Menu bar above the editor).
|
description: Which renderer does this issue occur on? If you are unsure, you can check the renderer in the Properties Editor (click the "i" in the Snippet Menu bar above the editor).
|
||||||
options:
|
options:
|
||||||
- label: Legacy
|
- v3
|
||||||
- label: v3
|
- Legacy
|
||||||
|
- Both
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -12,3 +12,5 @@ todo.md
|
|||||||
startDB.bat
|
startDB.bat
|
||||||
startMViewer.bat
|
startMViewer.bat
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
coverage
|
||||||
|
|||||||
48
.stylelintrc.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"stylelint-config-recess-order",
|
||||||
|
"stylelint-config-recommended"],
|
||||||
|
"plugins": [
|
||||||
|
"stylelint-stylistic",
|
||||||
|
"./stylelint_plugins/declaration-colon-align.js",
|
||||||
|
"./stylelint_plugins/declaration-colon-min-space-before",
|
||||||
|
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
||||||
|
],
|
||||||
|
"customSyntax": "postcss-less",
|
||||||
|
"rules": {
|
||||||
|
"no-descending-specificity" : null,
|
||||||
|
"at-rule-no-unknown" : null,
|
||||||
|
"function-no-unknown" : null,
|
||||||
|
"font-family-no-missing-generic-family-keyword" : null,
|
||||||
|
"font-weight-notation" : "named-where-possible",
|
||||||
|
"font-family-name-quotes" : "always-unless-keyword",
|
||||||
|
"stylistic/indentation" : "tab",
|
||||||
|
"no-duplicate-selectors" : true,
|
||||||
|
"stylistic/color-hex-case" : "upper",
|
||||||
|
"color-hex-length" : "long",
|
||||||
|
"stylistic/selector-combinator-space-after" : "always",
|
||||||
|
"stylistic/selector-combinator-space-before" : "always",
|
||||||
|
"stylistic/selector-attribute-operator-space-before" : "never",
|
||||||
|
"stylistic/selector-attribute-operator-space-after" : "never",
|
||||||
|
"stylistic/selector-attribute-brackets-space-inside" : "never",
|
||||||
|
"selector-attribute-quotes" : "always",
|
||||||
|
"selector-pseudo-element-colon-notation" : "double",
|
||||||
|
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
||||||
|
"stylistic/block-opening-brace-space-before" : "always",
|
||||||
|
"naturalcrit/declaration-colon-min-space-before" : 1,
|
||||||
|
"stylistic/declaration-block-trailing-semicolon" : "always",
|
||||||
|
"stylistic/declaration-colon-space-after" : "always",
|
||||||
|
"stylistic/number-leading-zero" : "always",
|
||||||
|
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
||||||
|
"function-url-scheme-disallowed-list" : ["data","http"],
|
||||||
|
"comment-whitespace-inside" : "always",
|
||||||
|
"stylistic/string-quotes" : "single",
|
||||||
|
"stylistic/media-feature-range-operator-space-before" : "always",
|
||||||
|
"stylistic/media-feature-range-operator-space-after" : "always",
|
||||||
|
"stylistic/media-feature-parentheses-space-inside" : "never",
|
||||||
|
"stylistic/media-feature-colon-space-before" : "always",
|
||||||
|
"stylistic/media-feature-colon-space-after" : "always",
|
||||||
|
"naturalcrit/declaration-colon-align" : true,
|
||||||
|
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:16.11-alpine
|
FROM node:18-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
@@ -10,11 +10,11 @@ WORKDIR /usr/src/app
|
|||||||
# 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 ./
|
||||||
# --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 yarn install --ignore-scripts
|
RUN npm install --ignore-scripts
|
||||||
|
|
||||||
# Bundle app source and build application
|
# Bundle app source and build application
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN yarn build
|
RUN npm run build
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
CMD [ "yarn", "start" ]
|
CMD [ "npm", "start" ]
|
||||||
|
|||||||
32
README.md
@@ -21,24 +21,29 @@ below.
|
|||||||
First, install three programs that The Homebrewery requires to run and retrieve
|
First, install three programs that The Homebrewery requires to run and retrieve
|
||||||
updates:
|
updates:
|
||||||
|
|
||||||
1. install [node](https://nodejs.org/en/)
|
1. install [node](https://nodejs.org/en/), version v16 or higher.
|
||||||
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
||||||
|
|
||||||
For the easiest installation, follow these steps:
|
For the easiest installation, follow these steps:
|
||||||
1. In the installer, uncheck the option to run as a service.
|
1. In the installer, uncheck the option to run as a service.
|
||||||
1. You can install MongoDB Compass if you want a GUI to view your database documents.
|
1. You can install MongoDB Compass if you want a GUI to view your database documents.
|
||||||
|
1. If you install any version over 6.0, you will have to install [MongoDB Shell](https://www.mongodb.com/try/download/shell).
|
||||||
1. Go to the C:\ drive and create a folder called "data".
|
1. Go to the C:\ drive and create a folder called "data".
|
||||||
1. Inside the "data" folder, create a new folder called "db".
|
1. Inside the "data" folder, create a new folder called "db".
|
||||||
1. Open a command prompt or other terminal and navigate to your MongoDB install folder (C:\Program Files\Mongo\Server\4.4\bin).
|
1. Open a command prompt or other terminal and navigate to your MongoDB install folder (C:\Program Files\Mongo\Server\6.0\bin).
|
||||||
1. In the command prompt, run "mongod", which will start up your local database server.
|
1. In the command prompt, run "mongod", which will start up your local database server.
|
||||||
1. While MongoD is running, open a second command prompt and navigate to the MongoDB install folder.
|
1. While MongoD is running, open a second command prompt and navigate to the MongoDB install folder.
|
||||||
1. In the second command prompt, run "mongo", which allows you to edit the database.
|
|
||||||
1. Type `use homebrewery` to create The Homebrewery database. You should see `switched to db homebrewery`.
|
|
||||||
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`.
|
|
||||||
1. Search in Windows for "Advanced system settings" and open it.
|
1. Search in Windows for "Advanced system settings" and open it.
|
||||||
1. Click "Environment variables", find the "path" variable, and double-click to open it.
|
1. Click "Environment variables", find the "path" variable, and double-click to open it.
|
||||||
1. Click "New" and paste in the path to the MongoDB "bin" folder.
|
1. Click "New" and paste in the path to the MongoDB "bin" folder.
|
||||||
1. Click "OK" three times to close all the windows.
|
1. Click "OK" three times to close all the windows.
|
||||||
|
1. In the second command prompt, run "mongo", which allows you to edit the database.
|
||||||
|
1. Type `use homebrewery` to create The Homebrewery database. You should see `switched to db homebrewery`.
|
||||||
|
1. Type `db.brews.insertOne({"title":"test"})` to create a blank document. You should see `{
|
||||||
|
acknowledged: true,
|
||||||
|
insertedId: ObjectId("63c2fce9e5ac5a94fe2410cf")
|
||||||
|
}`
|
||||||
|
|
||||||
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt).
|
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt).
|
||||||
|
|
||||||
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
||||||
@@ -51,11 +56,19 @@ git clone https://github.com/naturalcrit/homebrewery.git
|
|||||||
Second, you will need to add the environment variable `NODE_ENV=local` to allow
|
Second, you will need to add the environment variable `NODE_ENV=local` to allow
|
||||||
the project to run locally.
|
the project to run locally.
|
||||||
|
|
||||||
You can set this temporarily in your shell of choice:
|
You can set this **temporarily** (until you close the terminal) in your shell of choice with admin privileges:
|
||||||
* Windows Powershell: `$env:NODE_ENV="local"`
|
* Windows Powershell: `$env:NODE_ENV="local"`
|
||||||
* Windows CMD: `set NODE_ENV=local`
|
* Windows CMD: `set NODE_ENV=local`
|
||||||
* Linux / macOS: `export NODE_ENV=local`
|
* Linux / macOS: `export NODE_ENV=local`
|
||||||
|
|
||||||
|
If you want to add this variable **permanently** the steps are as follows:
|
||||||
|
1. Search in Windows for "Advanced system settings" and open it.
|
||||||
|
1. Click "Environment variables".
|
||||||
|
1. In System Variables, click "New"
|
||||||
|
1. Click "New" and write `NODE_ENV` as a name and `local` as the value.
|
||||||
|
1. Click "OK" three times to close all the windows.
|
||||||
|
This can be undone at any time if needed.
|
||||||
|
|
||||||
Third, you will need to install the Node dependencies, compile the app, and run
|
Third, you will need to install the Node dependencies, compile the app, and run
|
||||||
it using the two commands:
|
it using the two commands:
|
||||||
|
|
||||||
@@ -65,6 +78,13 @@ it using the two commands:
|
|||||||
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
||||||
in your browser and use The Homebrewery offline.
|
in your browser and use The Homebrewery offline.
|
||||||
|
|
||||||
|
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
|
||||||
|
- [Mongo community forums](https://www.mongodb.com/community/forums/)
|
||||||
|
- Useful Stack Overflow links for your most probable errors: [1](https://stackoverflow.com/questions/44962540/mongod-and-mongo-commands-not-working-on-windows-10), [2](https://stackoverflow.com/questions/15053893/mongo-command-not-recognized-when-trying-to-connect-to-a-mongodb-server/41507803#41507803), [3](https://stackoverflow.com/questions/51224959/mongo-is-not-recognized-as-an-internal-or-external-command-operable-program-o)
|
||||||
|
|
||||||
|
If you still have problems, post in [Our Subreddit](https://www.reddit.com/r/homebrewery/) and we will help you.
|
||||||
|
|
||||||
### Running the application via Docker
|
### Running the application via Docker
|
||||||
|
|
||||||
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
||||||
|
|||||||
331
changelog.md
@@ -1,13 +1,36 @@
|
|||||||
```css
|
```css
|
||||||
|
.beta {
|
||||||
|
color : white;
|
||||||
|
padding : 4px 6px;
|
||||||
|
line-height : 1em;
|
||||||
|
background : grey;
|
||||||
|
border-radius : 12px;
|
||||||
|
font-family : monospace;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
margin-top : -5px;
|
||||||
|
margin-bottom : -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fac {
|
||||||
|
height: 1em;
|
||||||
|
line-height: 2em;
|
||||||
|
margin-bottom: -0.05cm
|
||||||
|
}
|
||||||
|
|
||||||
h5 {
|
h5 {
|
||||||
font-size: .35cm !important;
|
font-size: .35cm !important;
|
||||||
margin-top: 0.3cm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page ul ul {
|
.page ul ul {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page .taskList {
|
||||||
|
display:block;
|
||||||
|
break-inside:auto;
|
||||||
|
}
|
||||||
|
|
||||||
.taskList li input {
|
.taskList li input {
|
||||||
list-style-type : none;
|
list-style-type : none;
|
||||||
margin-left : -0.52cm;
|
margin-left : -0.52cm;
|
||||||
@@ -36,15 +59,310 @@ pre {
|
|||||||
margin-top : 0.1cm;
|
margin-top : 0.1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page ul + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
.page .openSans {
|
.page .openSans {
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding-bottom: 1.5cm;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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 28/06/2023 - v3.9.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Better error pages with more useful information
|
||||||
|
|
||||||
|
Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Friday 02/06/2023 - v3.9.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
|
||||||
|
|
||||||
|
Fixes issue [#2408](https://github.com/naturalcrit/homebrewery/issues/2408)
|
||||||
|
|
||||||
|
* [x] Pressing tab now indents with spaces instead of tab character; fixes several issues with Markdown lists
|
||||||
|
|
||||||
|
Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#1556](https://github.com/naturalcrit/homebrewery/issues/1556)
|
||||||
|
|
||||||
|
* [x] Rename `naturalCritLogo.svg` to `naturalCritLogoRed.svg`. Those using the {{beta BETA}} coverPage snippet may need to update that text to make the NaturalCrit logo appear again.
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix strange animation of image masks
|
||||||
|
|
||||||
|
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
|
||||||
|
|
||||||
|
* [x] New {{openSans **PHB → {{fac,book-back-cover}} BACK COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
|
||||||
|
|
||||||
|
* [x] New {{openSans **TEXT EDITOR → {{fas,fa-bars}} INDEX** }} snippet for V3!
|
||||||
|
|
||||||
|
* [x] Fix highlighting of curly braces inside comments
|
||||||
|
|
||||||
|
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 12/04/2023 - v3.8.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Rename `{{coverPage}}` to `{{frontCover}}`. Those using this {{beta BETA}} feature will need to update that text to make the cover page appear again.
|
||||||
|
|
||||||
|
* [x] Several background fixes to test scripts
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [X] Add content negotiation to exclude image requests from our API calls
|
||||||
|
|
||||||
|
Fixes issue [#2595](https://github.com/naturalcrit/homebrewery/issues/2595)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Update server build scripts to fix Admin page
|
||||||
|
|
||||||
|
Fixes issues [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
|
||||||
|
|
||||||
|
* [x] Fix internal links inside `<\div>` blocks not receiving the `target=_self` attribute
|
||||||
|
|
||||||
|
Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680)
|
||||||
|
|
||||||
|
* [x] See brew details on `/share` pages by clicking the brew title (author, last update, tags, etc.)
|
||||||
|
|
||||||
|
Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
|
||||||
|
|
||||||
|
* [x] Add local Windows install script via Chocolatey
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
|
||||||
|
|
||||||
|
* [x] New {{openSans **IMAGES → {{fac,mask-center}} WATERCOLOR CENTER** }} snippets for V3, which adds a stylish watercolor texture to the center of your images!
|
||||||
|
|
||||||
|
* [x] New {{openSans **PHB → {{fac,book-inside-cover}} INSIDE COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
|
||||||
|
|
||||||
|
* [x] Add some missing characters {{font-family:scalySansRemake Ñ ñ ç Ç Ý ý # ^ ¿ ' " ¡ ·}} to the "scalySansRemake" font in V3.
|
||||||
|
|
||||||
|
Fixes issues [#2280](https://github.com/naturalcrit/homebrewery/issues/2280)
|
||||||
|
|
||||||
|
##### Gazook89
|
||||||
|
|
||||||
|
* [x] Add "Language" selector in {{fa,fa-info-circle}} **Properties** menu. Sets the HTML Lang attribute for your brew to better handle hyphenation or spellcheck.
|
||||||
|
|
||||||
|
Fixes issues [#1343](https://github.com/naturalcrit/homebrewery/issues/1343)
|
||||||
|
|
||||||
|
* [x] Fix a crash when multiple `{injection}` tags appear in sequence
|
||||||
|
|
||||||
|
Fixes issues [#2712](https://github.com/naturalcrit/homebrewery/issues/2712)
|
||||||
|
|
||||||
|
##### MichielDeMey
|
||||||
|
|
||||||
|
* [x] Remove all-caps display on Account button since usernames are case-sensitive.
|
||||||
|
|
||||||
|
Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Monday 13/03/2023 - v3.7.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Thursday 09/03/2023 - v3.7.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Lucastucious (new contributor!)
|
||||||
|
|
||||||
|
* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making PDF text selectable
|
||||||
|
|
||||||
|
Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
||||||
|
|
||||||
|
{{note
|
||||||
|
**NOTE:** If you create your PDF on a computer with an old version of Mac Preview (v10 or older) you may see shadows appear as solid gray.
|
||||||
|
}}
|
||||||
|
|
||||||
|
##### MichielDeMey
|
||||||
|
|
||||||
|
* [x] Updated the Google Drive icon
|
||||||
|
* [x] Backend fix to unit tests failing intermittently
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Fix PDF pixelation on CoverPage text outlines
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Tuesday 28/02/2023 - v3.7.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
{{note
|
||||||
|
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
|
||||||
|
}}
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
|
||||||
|
|
||||||
|
* [x] Fix site not displaying on iOS devices
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] New {{openSans **PHB → COVER PAGE** {{fac,book-front-cover}} }} snippet for V3, which adds a stylish coverpage to your brew! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
|
||||||
|
|
||||||
|
##### MichielDeMey (new contribuor!)
|
||||||
|
|
||||||
|
* [x] Fix typo in testing scripts
|
||||||
|
* [x] Fix "mug" image not using HTTPS
|
||||||
|
|
||||||
|
Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Saturday 18/02/2023 - v3.6.1
|
||||||
|
{{taskList
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix users not being removed from Authors list
|
||||||
|
|
||||||
|
Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 23/01/2023 - v3.6.0
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Fix Google Drive brews sometimes duplicating
|
||||||
|
|
||||||
|
Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Add unit tests with full coverage for the Homebrewery API
|
||||||
|
|
||||||
|
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
||||||
|
|
||||||
|
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Auto-compile Themes CSS on development server
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix cloned brews inheriting the parent view count
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Friday 23/12/2022 - v3.5.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Only brew owners or invited authors can edit a brew
|
||||||
|
|
||||||
|
- Visiting an `/edit` page of a brew that does not list you as an author will result in an error page. Authors can be added to any brew by opening its {{fa,fa-info-circle}} **Properties** menu and typing the author's username (case-sensitive) into the **Invited Authors** bubble.
|
||||||
|
- Warn user if a newer brew version has been saved on another device
|
||||||
|
|
||||||
|
Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Saturday 10/12/2022 - v3.4.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Fix broken tags editor
|
||||||
|
|
||||||
|
* [x] Reduce server load to fix some saving issues
|
||||||
|
|
||||||
|
Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Account page help link for Google Drive errors
|
||||||
|
|
||||||
|
Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 05/12/2022 - v3.4.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix Account page incorrect last login time
|
||||||
|
|
||||||
|
Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Fix crashing on iOS and Safari browsers
|
||||||
|
|
||||||
|
Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 28/11/2022 - v3.4.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix for Chrome v108 handling of page size
|
||||||
|
|
||||||
|
Fixes issues [#2445](https://github.com/naturalcrit/homebrewery/issues/2445), [#2516](https://github.com/naturalcrit/homebrewery/issues/2516)
|
||||||
|
|
||||||
|
* [x] New account page with some user info, at {{openSans **USERNAME {{fa,fa-user}} → ACCOUNT {{fa,fa-user}}**}}
|
||||||
|
|
||||||
|
Fixes issues [#2049](https://github.com/naturalcrit/homebrewery/issues/2049), [#2043](https://github.com/naturalcrit/homebrewery/issues/2043)
|
||||||
|
|
||||||
|
* [x] Fix "Published/Private Brews" buttons on userpage
|
||||||
|
|
||||||
|
Fixes issues [#2449](https://github.com/naturalcrit/homebrewery/issues/2449)
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Make autosave default on for new users
|
||||||
|
|
||||||
|
* [x] Added link to our FAQ at {{openSans **NEED HELP? {{fa,fa-question-circle}} → FAQ {{fa,fa-question-circle}}**}}
|
||||||
|
|
||||||
|
* [x] Fix curly blocks freezing with long property lists
|
||||||
|
|
||||||
|
Fixes issues [#2393](https://github.com/naturalcrit/homebrewery/issues/2393)
|
||||||
|
|
||||||
|
* [x] Items can now be removed from {{openSans **RECENT BREWS** {{fas,fa-history}} }}
|
||||||
|
|
||||||
|
Fixes issues [#1918](https://github.com/naturalcrit/homebrewery/issues/1918)
|
||||||
|
|
||||||
|
* [x] Curly injector syntax `{blue}` highlighting in editor
|
||||||
|
|
||||||
|
Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
### Thursday 28/10/2022 - v3.3.1
|
### Thursday 28/10/2022 - v3.3.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -54,6 +372,10 @@ For a full record of development, visit our [Github Page](https://github.com/nat
|
|||||||
|
|
||||||
Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Reduce size of thumbnails on social media links
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
### Friday 19/10/2022 - v3.3.0
|
### Friday 19/10/2022 - v3.3.0
|
||||||
@@ -86,7 +408,6 @@ Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
|
|||||||
|
|
||||||
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
||||||
|
|
||||||
|
|
||||||
##### Gazook:
|
##### Gazook:
|
||||||
|
|
||||||
* [x] Several updates to bug reporting and error popups
|
* [x] Several updates to bug reporting and error popups
|
||||||
@@ -136,6 +457,10 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 31/08/2022 - v3.2.1
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -162,8 +487,6 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Saturday 27/08/2022 - v3.2.0
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
|
|||||||
129
client/components/combobox.jsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const cx = require('classnames');
|
||||||
|
require('./combobox.less');
|
||||||
|
|
||||||
|
const Combobox = createClass({
|
||||||
|
displayName : 'Combobox',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
className : '',
|
||||||
|
trigger : 'hover',
|
||||||
|
default : '',
|
||||||
|
placeholder : '',
|
||||||
|
autoSuggest : {
|
||||||
|
clearAutoSuggestOnClick : true,
|
||||||
|
suggestMethod : 'includes',
|
||||||
|
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
showDropdown : false,
|
||||||
|
value : '',
|
||||||
|
options : [...this.props.options],
|
||||||
|
inputFocused : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
componentDidMount : function() {
|
||||||
|
if(this.props.trigger == 'click')
|
||||||
|
document.addEventListener('click', this.handleClickOutside);
|
||||||
|
this.setState({
|
||||||
|
value : this.props.default
|
||||||
|
});
|
||||||
|
},
|
||||||
|
componentWillUnmount : function() {
|
||||||
|
if(this.props.trigger == 'click')
|
||||||
|
document.removeEventListener('click', this.handleClickOutside);
|
||||||
|
},
|
||||||
|
handleClickOutside : function(e){
|
||||||
|
// Close dropdown when clicked outside
|
||||||
|
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
|
||||||
|
this.handleDropdown(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleDropdown : function(show){
|
||||||
|
this.setState({
|
||||||
|
showDropdown : show,
|
||||||
|
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleInput : function(e){
|
||||||
|
e.persist();
|
||||||
|
this.setState({
|
||||||
|
value : e.target.value,
|
||||||
|
inputFocused : false
|
||||||
|
}, ()=>{
|
||||||
|
this.props.onEntry(e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSelect : function(e){
|
||||||
|
this.setState({
|
||||||
|
value : e.currentTarget.getAttribute('data-value')
|
||||||
|
}, ()=>{this.props.onSelect(this.state.value);});
|
||||||
|
;
|
||||||
|
},
|
||||||
|
renderTextInput : function(){
|
||||||
|
return (
|
||||||
|
<div className='dropdown-input item'
|
||||||
|
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
|
||||||
|
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
onChange={(e)=>this.handleInput(e)}
|
||||||
|
value={this.state.value || ''}
|
||||||
|
placeholder={this.props.placeholder}
|
||||||
|
onBlur={(e)=>{
|
||||||
|
if(!e.target.checkValidity()){
|
||||||
|
this.setState({
|
||||||
|
value : this.props.default
|
||||||
|
}, ()=>this.props.onEntry(e));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
renderDropdown : function(dropdownChildren){
|
||||||
|
if(!this.state.showDropdown) return null;
|
||||||
|
if(this.props.autoSuggest && !this.state.inputFocused){
|
||||||
|
const suggestMethod = this.props.autoSuggest.suggestMethod;
|
||||||
|
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
|
||||||
|
const filteredArrays = filterOn.map((attr)=>{
|
||||||
|
const children = dropdownChildren.filter((item)=>{
|
||||||
|
if(suggestMethod === 'includes'){
|
||||||
|
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
|
||||||
|
} else if(suggestMethod === 'startsWith'){
|
||||||
|
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return children;
|
||||||
|
});
|
||||||
|
dropdownChildren = _.uniq(filteredArrays.flat(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='dropdown-options'>
|
||||||
|
{dropdownChildren}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
render : function () {
|
||||||
|
const dropdownChildren = this.state.options.map((child, i)=>{
|
||||||
|
const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
|
||||||
|
return clone;
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={`dropdown-container ${this.props.className}`}
|
||||||
|
ref='dropdown'
|
||||||
|
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
|
||||||
|
{this.renderTextInput()}
|
||||||
|
{this.renderDropdown(dropdownChildren)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Combobox;
|
||||||
50
client/components/combobox.less
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
.dropdown-container {
|
||||||
|
position:relative;
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.dropdown-options {
|
||||||
|
position:absolute;
|
||||||
|
background-color: white;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid gray;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 200px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
&::-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ const BrewRenderer = createClass({
|
|||||||
style : '',
|
style : '',
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
|
lang : '',
|
||||||
errors : []
|
errors : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -107,6 +108,12 @@ const BrewRenderer = createClass({
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sanitizeScriptTags : function(content) {
|
||||||
|
return content
|
||||||
|
.replace(/<script/ig, '<script')
|
||||||
|
.replace(/<\/script>/ig, '</script>');
|
||||||
|
},
|
||||||
|
|
||||||
renderPageInfo : function(){
|
renderPageInfo : function(){
|
||||||
return <div className='pageInfo' ref='main'>
|
return <div className='pageInfo' ref='main'>
|
||||||
<div>
|
<div>
|
||||||
@@ -134,17 +141,20 @@ const BrewRenderer = createClass({
|
|||||||
|
|
||||||
renderStyle : function() {
|
renderStyle : function() {
|
||||||
if(!this.props.style) return;
|
if(!this.props.style) return;
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />;
|
const cleanStyle = this.sanitizeScriptTags(this.props.style);
|
||||||
|
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.sanitizeScriptTags(this.props.style)}\n} </style>` }} />;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
renderPage : function(pageText, index){
|
||||||
|
const cleanPageText = this.sanitizeScriptTags(pageText);
|
||||||
if(this.props.renderer == 'legacy')
|
if(this.props.renderer == 'legacy')
|
||||||
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
|
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(cleanPageText) }} key={index} />;
|
||||||
else {
|
else {
|
||||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
return (
|
return (
|
||||||
<div className='page' id={`p${index + 1}`} key={index} >
|
<div className='page' id={`p${index + 1}`} key={index} >
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(cleanPageText) }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -183,13 +193,18 @@ const BrewRenderer = createClass({
|
|||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
emitClick : function(){
|
||||||
|
// console.log('iFrame clicked');
|
||||||
|
if(!window || !document) return;
|
||||||
|
document.dispatchEvent(new MouseEvent('click'));
|
||||||
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
//render in iFrame so broken code doesn't crash the site.
|
//render in iFrame so broken code doesn't crash the site.
|
||||||
//Also render dummy page while iframe is mounting.
|
//Also render dummy page while iframe is mounting.
|
||||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||||
const themePath = this.props.theme ?? '5ePHB';
|
const themePath = this.props.theme ?? '5ePHB';
|
||||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!this.state.isMounted
|
{!this.state.isMounted
|
||||||
@@ -202,7 +217,9 @@ const BrewRenderer = createClass({
|
|||||||
|
|
||||||
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
|
<Frame id='BrewRenderer' initialContent={this.state.initialContent}
|
||||||
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
|
||||||
contentDidMount={this.frameDidMount}>
|
contentDidMount={this.frameDidMount}
|
||||||
|
onClick={()=>{this.emitClick();}}
|
||||||
|
>
|
||||||
<div className={'brewRenderer'}
|
<div className={'brewRenderer'}
|
||||||
onScroll={this.handleScroll}
|
onScroll={this.handleScroll}
|
||||||
style={{ height: this.state.height }}>
|
style={{ height: this.state.height }}>
|
||||||
@@ -222,7 +239,7 @@ const BrewRenderer = createClass({
|
|||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{this.renderStyle()}
|
{this.renderStyle()}
|
||||||
<div className='pages' ref='pages'>
|
<div className='pages' ref='pages' lang={`${this.props.lang || 'en'}`}>
|
||||||
{this.renderPages()}
|
{this.renderPages()}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames'); //Unused variable
|
const cx = require('classnames'); //Unused variable
|
||||||
|
|
||||||
const DISMISS_KEY = 'dismiss_notification08-27-22';
|
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
||||||
|
|
||||||
const NotificationPopup = createClass({
|
const NotificationPopup = createClass({
|
||||||
displayName : 'NotificationPopup',
|
displayName : 'NotificationPopup',
|
||||||
@@ -25,21 +25,13 @@ const NotificationPopup = createClass({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<li key='psa'>
|
<li key='psa'>
|
||||||
<em>V3.2.0 Released!</em> <br />
|
<em>Broken default logo on <b>CoverPage</b> </em> <br />
|
||||||
We are happy to announce that after nearly a year of use by our many users,
|
If you have used the Cover Page snippet and notice the Naturalcrit
|
||||||
we are making the V3 render mode the default setting for all new brews.
|
logo is showing as a broken image, this is due to some small tweaks
|
||||||
This mode has become quite popular, and has proven to be stable and powerful.
|
of this BETA feature. To fix the logo in your cover page, rename
|
||||||
Of course, we will always keep the option to use the Legacy renderer for any
|
the image link <b>"/assets/naturalCritLogoRed.svg"</b>. Remember
|
||||||
brew, which can still be accessed from the Properties menu.
|
that any snippet marked "BETA" may have a similar change in the
|
||||||
</li>
|
future as we encounter any bugs or reworks.
|
||||||
|
|
||||||
<li key='stubs'>
|
|
||||||
<em>Change to Google Drive Storage!</em> <br />
|
|
||||||
We have made a change to the process of tranferring brews between Google
|
|
||||||
Drive and the Homebrewery storage. Starting now, any time a brew is
|
|
||||||
transferred, it will keep the same links instead of generating new ones!
|
|
||||||
We hope this change will help reduce issues where people "lost" their work
|
|
||||||
by trying to visit old links.
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li key='googleDriveFolder'>
|
<li key='googleDriveFolder'>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const Editor = createClass({
|
|||||||
onTextChange : ()=>{},
|
onTextChange : ()=>{},
|
||||||
onStyleChange : ()=>{},
|
onStyleChange : ()=>{},
|
||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
|
reportError : ()=>{},
|
||||||
|
|
||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
@@ -137,9 +138,17 @@ const Editor = createClass({
|
|||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highlight injectors {style}
|
||||||
|
if(line.includes('{') && line.includes('}')){
|
||||||
|
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) != null) {
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||||
|
}
|
||||||
|
}
|
||||||
// Highlight inline spans {{content}}
|
// Highlight inline spans {{content}}
|
||||||
if(line.includes('{{') && line.includes('}}')){
|
if(line.includes('{{') && line.includes('}}')){
|
||||||
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
||||||
let match;
|
let match;
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
@@ -158,7 +167,7 @@ const Editor = createClass({
|
|||||||
// Highlight block divs {{\n Content \n}}
|
// Highlight block divs {{\n Content \n}}
|
||||||
let endCh = line.length+1;
|
let endCh = line.length+1;
|
||||||
|
|
||||||
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
|
const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
|
||||||
if(match)
|
if(match)
|
||||||
endCh = match.index+match[0].length;
|
endCh = match.index+match[0].length;
|
||||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||||
@@ -260,7 +269,7 @@ const Editor = createClass({
|
|||||||
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.onTextChange}
|
||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
if(this.isStyle()){
|
if(this.isStyle()){
|
||||||
@@ -283,7 +292,8 @@ const Editor = createClass({
|
|||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
onChange={this.props.onMetaChange} />
|
onChange={this.props.onMetaChange}
|
||||||
|
reportError={this.props.reportError}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -313,7 +323,8 @@ const Editor = createClass({
|
|||||||
theme={this.props.brew.theme}
|
theme={this.props.brew.theme}
|
||||||
undo={this.undo}
|
undo={this.undo}
|
||||||
redo={this.redo}
|
redo={this.redo}
|
||||||
historySize={this.historySize()} />
|
historySize={this.historySize()}
|
||||||
|
cursorPos={this.refs.codeEditor?.getCursorPosition() || {}} />
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,16 +19,20 @@
|
|||||||
background-color : fade(#299, 15%);
|
background-color : fade(#299, 15%);
|
||||||
border-bottom : #299 solid 1px;
|
border-bottom : #299 solid 1px;
|
||||||
}
|
}
|
||||||
.block{
|
.block:not(.cm-comment){
|
||||||
color : purple;
|
color : purple;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
.inline-block{
|
.inline-block:not(.cm-comment){
|
||||||
color : red;
|
color : red;
|
||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
|
.injection:not(.cm-comment){
|
||||||
|
color : green;
|
||||||
|
font-weight : bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brewJump{
|
.brewJump{
|
||||||
|
|||||||
@@ -4,16 +4,24 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Combobox = require('client/components/combobox.jsx');
|
||||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
|
const validations = require('./validations.js');
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
const homebreweryThumbnail = require('../../thumbnail.png');
|
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||||
|
|
||||||
|
const callIfExists = (val, fn, ...args)=>{
|
||||||
|
if(val[fn]) {
|
||||||
|
val[fn](...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MetadataEditor = createClass({
|
const MetadataEditor = createClass({
|
||||||
displayName : 'MetadataEditor',
|
displayName : 'MetadataEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -22,14 +30,17 @@ const MetadataEditor = createClass({
|
|||||||
editId : null,
|
editId : null,
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
|
thumbnail : '',
|
||||||
tags : [],
|
tags : [],
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : [],
|
systems : [],
|
||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB'
|
theme : '5ePHB',
|
||||||
|
lang : 'en'
|
||||||
},
|
},
|
||||||
onChange : ()=>{}
|
onChange : ()=>{},
|
||||||
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -51,11 +62,28 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleFieldChange : function(name, e){
|
handleFieldChange : function(name, e){
|
||||||
this.props.onChange({
|
// load validation rules, and check input value against them
|
||||||
...this.props.metadata,
|
const inputRules = validations[name] ?? [];
|
||||||
[name] : e.target.value
|
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||||
});
|
|
||||||
|
// if no validation rules, save to props
|
||||||
|
if(validationErr.length === 0){
|
||||||
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
|
this.props.onChange({
|
||||||
|
...this.props.metadata,
|
||||||
|
[name] : e.target.value
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// if validation issues, display built-in browser error popup with each error.
|
||||||
|
const errMessage = validationErr.map((err)=>{
|
||||||
|
return `- ${err}`;
|
||||||
|
}).join('\n');
|
||||||
|
|
||||||
|
callIfExists(e.target, 'setCustomValidity', errMessage);
|
||||||
|
callIfExists(e.target, 'reportValidity');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSystem : function(system, e){
|
handleSystem : function(system, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.systems.push(system);
|
this.props.metadata.systems.push(system);
|
||||||
@@ -64,6 +92,7 @@ const MetadataEditor = createClass({
|
|||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleRenderer : function(renderer, e){
|
handleRenderer : function(renderer, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.renderer = renderer;
|
this.props.metadata.renderer = renderer;
|
||||||
@@ -85,6 +114,11 @@ const MetadataEditor = createClass({
|
|||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleLanguage : function(languageCode){
|
||||||
|
this.props.metadata.lang = languageCode;
|
||||||
|
this.props.onChange(this.props.metadata);
|
||||||
|
},
|
||||||
|
|
||||||
handleDelete : function(){
|
handleDelete : function(){
|
||||||
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
|
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
|
||||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||||
@@ -96,8 +130,12 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end((err, res)=>{
|
||||||
window.location.href = '/';
|
if(err) {
|
||||||
|
this.props.reportError(err);
|
||||||
|
} else {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -159,6 +197,10 @@ const MetadataEditor = createClass({
|
|||||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||||
{`${theme.renderer} : ${theme.name}`}
|
{`${theme.renderer} : ${theme.name}`}
|
||||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
||||||
|
<div className='preview'>
|
||||||
|
<h6>{`${theme.name}`} preview</h6>
|
||||||
|
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -168,14 +210,14 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
if(this.props.metadata.renderer == 'legacy') {
|
if(this.props.metadata.renderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled' trigger='disabled'>
|
<Nav.dropdown className='disabled value' trigger='disabled'>
|
||||||
<div>
|
<div>
|
||||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
||||||
</div>
|
</div>
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown trigger='click'>
|
<Nav.dropdown className='value' trigger='click'>
|
||||||
<div>
|
<div>
|
||||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +232,47 @@ const MetadataEditor = createClass({
|
|||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderLanguageDropdown : function(){
|
||||||
|
const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant'];
|
||||||
|
const listLanguages = ()=>{
|
||||||
|
return _.map(langCodes.sort(), (code, index)=>{
|
||||||
|
const localName = new Intl.DisplayNames([code], { 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)}`}>
|
||||||
|
{`${code}`}
|
||||||
|
<div className='detail'>{`${localName.of(code)}`}</div>
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
|
||||||
|
|
||||||
|
return <div className='field language'>
|
||||||
|
<label>language</label>
|
||||||
|
<div className='value'>
|
||||||
|
<Combobox trigger='click'
|
||||||
|
className='language-dropdown'
|
||||||
|
default={this.props.metadata.lang || ''}
|
||||||
|
placeholder='en'
|
||||||
|
onSelect={(value)=>this.handleLanguage(value)}
|
||||||
|
onEntry={(e)=>{
|
||||||
|
e.target.setCustomValidity(''); //Clear the validation popup while typing
|
||||||
|
debouncedHandleFieldChange('lang', e);
|
||||||
|
}}
|
||||||
|
options={listLanguages()}
|
||||||
|
autoSuggest={{
|
||||||
|
suggestMethod : 'startsWith',
|
||||||
|
clearAutoSuggestOnClick : true,
|
||||||
|
filterOn : ['data-value', 'data-detail', 'title']
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
</Combobox>
|
||||||
|
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
renderRenderOptions : function(){
|
renderRenderOptions : function(){
|
||||||
if(!global.enable_v3) return;
|
if(!global.enable_v3) return;
|
||||||
|
|
||||||
@@ -225,24 +308,26 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='metadataEditor'>
|
return <div className='metadataEditor'>
|
||||||
|
<h1 className='sectionHead'>Brew</h1>
|
||||||
|
|
||||||
<div className='field title'>
|
<div className='field title'>
|
||||||
<label>title</label>
|
<label>title</label>
|
||||||
<input type='text' className='value'
|
<input type='text' className='value'
|
||||||
value={this.props.metadata.title}
|
defaultValue={this.props.metadata.title}
|
||||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||||
</div>
|
</div>
|
||||||
<div className='field-group'>
|
<div className='field-group'>
|
||||||
<div className='field-column'>
|
<div className='field-column'>
|
||||||
<div className='field description'>
|
<div className='field description'>
|
||||||
<label>description</label>
|
<label>description</label>
|
||||||
<textarea value={this.props.metadata.description} className='value'
|
<textarea defaultValue={this.props.metadata.description} className='value'
|
||||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||||
</div>
|
</div>
|
||||||
<div className='field thumbnail'>
|
<div className='field thumbnail'>
|
||||||
<label>thumbnail</label>
|
<label>thumbnail</label>
|
||||||
<input type='text'
|
<input type='text'
|
||||||
value={this.props.metadata.thumbnail}
|
defaultValue={this.props.metadata.thumbnail}
|
||||||
placeholder='my.thumbnail.url'
|
placeholder='https://my.thumbnail.url'
|
||||||
className='value'
|
className='value'
|
||||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||||
@@ -258,8 +343,6 @@ const MetadataEditor = createClass({
|
|||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -267,10 +350,29 @@ const MetadataEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this.renderLanguageDropdown()}
|
||||||
|
|
||||||
{this.renderThemeDropdown()}
|
{this.renderThemeDropdown()}
|
||||||
|
|
||||||
{this.renderRenderOptions()}
|
{this.renderRenderOptions()}
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Authors</h1>
|
||||||
|
|
||||||
|
{this.renderAuthors()}
|
||||||
|
|
||||||
|
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
|
||||||
|
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||||
|
placeholder='invite author' unique={true}
|
||||||
|
values={this.props.metadata.invitedAuthors}
|
||||||
|
notes={['Invited 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)}/>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Privacy</h1>
|
||||||
|
|
||||||
<div className='field publish'>
|
<div className='field publish'>
|
||||||
<label>publish</label>
|
<label>publish</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
|
|||||||
@@ -10,6 +10,15 @@
|
|||||||
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.
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
|
|
||||||
|
.sectionHead {
|
||||||
|
font-weight: 1000;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -26,12 +35,16 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 5 0 200px;
|
flex: 5 0 200px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.field{
|
.field{
|
||||||
display : flex;
|
display : flex;
|
||||||
|
flex-wrap : wrap;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
|
position : relative;
|
||||||
&>label{
|
&>label{
|
||||||
width : 80px;
|
width : 80px;
|
||||||
font-size : 11px;
|
font-size : 11px;
|
||||||
@@ -42,6 +55,15 @@
|
|||||||
&>.value{
|
&>.value{
|
||||||
flex : 1 1 auto;
|
flex : 1 1 auto;
|
||||||
width : 50px;
|
width : 50px;
|
||||||
|
&:invalid {
|
||||||
|
background : #ffb9b9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type='text'], textarea {
|
||||||
|
border : 1px solid gray;
|
||||||
|
&:focus {
|
||||||
|
outline: 1px solid #444;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.thumbnail{
|
&.thumbnail{
|
||||||
height : 1.4em;
|
height : 1.4em;
|
||||||
@@ -72,6 +94,17 @@
|
|||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.language .language-dropdown {
|
||||||
|
max-width : 150px;
|
||||||
|
z-index : 200;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size : 0.6em;
|
||||||
|
font-style : italic;
|
||||||
|
line-height : 1.4em;
|
||||||
|
display : inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -122,10 +155,6 @@
|
|||||||
button.unpublish{
|
button.unpublish{
|
||||||
.button(@silver);
|
.button(@silver);
|
||||||
}
|
}
|
||||||
small{
|
|
||||||
font-size : 0.6em;
|
|
||||||
font-style : italic;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete.field .value{
|
.delete.field .value{
|
||||||
@@ -142,9 +171,8 @@
|
|||||||
font-size : 13.33px;
|
font-size : 13.33px;
|
||||||
.navDropdownContainer {
|
.navDropdownContainer {
|
||||||
background-color : white;
|
background-color : white;
|
||||||
width : 100%;
|
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 500;
|
z-index : 100;
|
||||||
&.disabled {
|
&.disabled {
|
||||||
font-style :italic;
|
font-style :italic;
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
@@ -165,24 +193,51 @@
|
|||||||
}
|
}
|
||||||
.navDropdown {
|
.navDropdown {
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
.item {
|
.item {
|
||||||
padding : 3px 3px;
|
padding : 3px 3px;
|
||||||
border-top : 1px solid rgb(118, 118, 118);
|
border-top : 1px solid rgb(118, 118, 118);
|
||||||
position : relative;
|
position : relative;
|
||||||
overflow : hidden;
|
overflow : visible;
|
||||||
background-color : white;
|
background-color : white;
|
||||||
|
.preview {
|
||||||
|
display : flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background : #ccc;
|
||||||
|
border-radius : 5px;
|
||||||
|
box-shadow : 0 0 5px black;
|
||||||
|
width : 200px;
|
||||||
|
color :black;
|
||||||
|
position : absolute;
|
||||||
|
top : 0;
|
||||||
|
right : 0;
|
||||||
|
opacity : 0;
|
||||||
|
transition : opacity 250ms ease;
|
||||||
|
z-index : 1;
|
||||||
|
overflow :hidden;
|
||||||
|
h6 {
|
||||||
|
font-weight : 900;
|
||||||
|
padding-inline:1em;
|
||||||
|
padding-block :.5em;
|
||||||
|
border-bottom :2px solid hsl(0,0%,40%);
|
||||||
|
}
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color : @blue;
|
background-color : @blue;
|
||||||
color : white;
|
color : white;
|
||||||
}
|
}
|
||||||
img {
|
&:hover > .preview {
|
||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
opacity: 1;
|
||||||
|
}
|
||||||
|
>img {
|
||||||
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : ~"max(100px, 100% - 300px)";
|
right : 0;
|
||||||
top : 0px;
|
top : 0px;
|
||||||
|
width : 50%;
|
||||||
|
height : 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,6 +245,7 @@
|
|||||||
}
|
}
|
||||||
.field .list {
|
.field .list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
|||||||
34
client/homebrew/editor/metadataEditor/validations.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = {
|
||||||
|
title : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 100 ? 'Max title length of 100 characters' : null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
description : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 500 ? 'Max description length of 500 characters.' : null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
thumbnail : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 256 ? 'Max URL length of 256 characters.' : null;
|
||||||
|
},
|
||||||
|
(value)=>{
|
||||||
|
if(value?.length == 0){return null;}
|
||||||
|
try {
|
||||||
|
Boolean(new URL(value));
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Must be a valid URL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
lang : [
|
||||||
|
(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;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
|||||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/snippets.js');
|
||||||
|
|
||||||
const execute = function(val, brew){
|
const execute = function(val, props){
|
||||||
if(_.isFunction(val)) return val(brew);
|
if(_.isFunction(val)) return val(props);
|
||||||
return val;
|
return val;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,7 +33,8 @@ const Snippetbar = createClass({
|
|||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
undo : ()=>{},
|
undo : ()=>{},
|
||||||
redo : ()=>{},
|
redo : ()=>{},
|
||||||
historySize : ()=>{}
|
historySize : ()=>{},
|
||||||
|
cursorPos : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ const Snippetbar = createClass({
|
|||||||
snippets={snippetGroup.snippets}
|
snippets={snippetGroup.snippets}
|
||||||
key={snippetGroup.groupName}
|
key={snippetGroup.groupName}
|
||||||
onSnippetClick={this.handleSnippetClick}
|
onSnippetClick={this.handleSnippetClick}
|
||||||
|
cursorPos={this.props.cursorPos}
|
||||||
/>;
|
/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -163,15 +165,23 @@ const SnippetGroup = createClass({
|
|||||||
onSnippetClick : function(){},
|
onSnippetClick : function(){},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSnippetClick : function(snippet){
|
handleSnippetClick : function(e, snippet){
|
||||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
e.stopPropagation();
|
||||||
|
this.props.onSnippetClick(execute(snippet.gen, this.props));
|
||||||
},
|
},
|
||||||
renderSnippets : function(){
|
renderSnippets : function(snippets){
|
||||||
return _.map(this.props.snippets, (snippet)=>{
|
return _.map(snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
|
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
||||||
<i className={snippet.icon} />
|
<i className={snippet.icon} />
|
||||||
{snippet.name}
|
<span className='name'>{snippet.name}</span>
|
||||||
|
{snippet.experimental && <span className='beta'>beta</span>}
|
||||||
|
{snippet.subsnippets && <>
|
||||||
|
<i className='fas fa-caret-right'></i>
|
||||||
|
<div className='dropdown side'>
|
||||||
|
{this.renderSnippets(snippet.subsnippets)}
|
||||||
|
</div></>}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -182,7 +192,7 @@ const SnippetGroup = createClass({
|
|||||||
<span className='groupName'>{this.props.groupName}</span>
|
<span className='groupName'>{this.props.groupName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='dropdown'>
|
<div className='dropdown'>
|
||||||
{this.renderSnippets()}
|
{this.renderSnippets(this.props.snippets)}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
@import (less) './client/icons/customIcons.less';
|
||||||
.snippetBar{
|
.snippetBar{
|
||||||
@menuHeight : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
.snippetGroup{
|
.snippetGroup{
|
||||||
border-right : 1px solid black;
|
border-right : 1px solid black;
|
||||||
&:hover{
|
&:hover{
|
||||||
.dropdown{
|
&>.dropdown{
|
||||||
visibility : visible;
|
visibility : visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,16 +96,47 @@
|
|||||||
padding : 0px;
|
padding : 0px;
|
||||||
background-color : #ddd;
|
background-color : #ddd;
|
||||||
.snippet{
|
.snippet{
|
||||||
|
position: relative;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
padding : 5px;
|
display : flex;
|
||||||
cursor : pointer;
|
align-items : center;
|
||||||
font-size : 10px;
|
min-width : max-content;
|
||||||
|
padding : 5px;
|
||||||
|
cursor : pointer;
|
||||||
|
font-size : 10px;
|
||||||
i{
|
i{
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
|
height : 1.2em;
|
||||||
|
&~i{
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
margin-right : auto;
|
||||||
|
}
|
||||||
|
.beta {
|
||||||
|
color : white;
|
||||||
|
padding : 4px 6px;
|
||||||
|
line-height : 1em;
|
||||||
|
margin-left : 5px;
|
||||||
|
align-self : center;
|
||||||
|
background : grey;
|
||||||
|
border-radius : 12px;
|
||||||
|
font-family : monospace;
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color : #999;
|
background-color : #999;
|
||||||
|
&>.dropdown{
|
||||||
|
visibility : visible;
|
||||||
|
&.side {
|
||||||
|
left: 100%;
|
||||||
|
top: 0%;
|
||||||
|
margin-left:0;
|
||||||
|
box-shadow: -1px 1px 2px 0px #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const StringArrayEditor = createClass({
|
|||||||
label : '',
|
label : '',
|
||||||
values : [],
|
values : [],
|
||||||
valuePatterns : null,
|
valuePatterns : null,
|
||||||
|
validators : [],
|
||||||
placeholder : '',
|
placeholder : '',
|
||||||
|
notes : [],
|
||||||
unique : false,
|
unique : false,
|
||||||
cannotEdit : [],
|
cannotEdit : [],
|
||||||
onChange : ()=>{}
|
onChange : ()=>{}
|
||||||
@@ -83,7 +85,8 @@ const StringArrayEditor = createClass({
|
|||||||
}
|
}
|
||||||
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||||
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||||
return matchesPatterns && uniqueIfSet;
|
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
|
||||||
|
return matchesPatterns && uniqueIfSet && passesValidators;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleValueInputKeyDown : function(event, index) {
|
handleValueInputKeyDown : function(event, index) {
|
||||||
@@ -123,17 +126,21 @@ const StringArrayEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <div className='field values'>
|
return <div className='field'>
|
||||||
<label>{this.props.label}</label>
|
<label>{this.props.label}</label>
|
||||||
<div className='list'>
|
<div style={{ flex: '1 0' }}>
|
||||||
{valueElements}
|
<div className='list'>
|
||||||
<div className='input-group'>
|
{valueElements}
|
||||||
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
<div className='input-group'>
|
||||||
value={this.state.temporaryValue}
|
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
value={this.state.temporaryValue}
|
||||||
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||||
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||||
|
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 305 KiB |
8
client/homebrew/googleDrive.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/>
|
||||||
|
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
|
||||||
|
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
|
||||||
|
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
|
||||||
|
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
|
||||||
|
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 17 KiB |
@@ -9,8 +9,9 @@ const EditPage = require('./pages/editPage/editPage.jsx');
|
|||||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||||
|
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = (props)=>{
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -46,6 +47,7 @@ const Homebrew = createClass({
|
|||||||
editId : null,
|
editId : null,
|
||||||
createdAt : null,
|
createdAt : null,
|
||||||
updatedAt : null,
|
updatedAt : null,
|
||||||
|
lang : ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -61,24 +63,28 @@ const Homebrew = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function (){
|
render : function (){
|
||||||
return <Router location={this.props.url}>
|
return (
|
||||||
<div className='homebrew'>
|
<Router location={this.props.url}>
|
||||||
<Routes>
|
<div className='homebrew'>
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
<Routes>
|
||||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
||||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
</Routes>
|
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||||
</div>
|
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
</Router>;
|
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const Account = createClass({
|
|||||||
if(global.account){
|
if(global.account){
|
||||||
return <Nav.dropdown>
|
return <Nav.dropdown>
|
||||||
<Nav.item
|
<Nav.item
|
||||||
className='account'
|
className='account username'
|
||||||
color='orange'
|
color='orange'
|
||||||
icon='fas fa-user'
|
icon='fas fa-user'
|
||||||
>
|
>
|
||||||
@@ -76,6 +76,14 @@ const Account = createClass({
|
|||||||
>
|
>
|
||||||
brews
|
brews
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
className='account'
|
||||||
|
color='orange'
|
||||||
|
icon='fas fa-user'
|
||||||
|
href='/account'
|
||||||
|
>
|
||||||
|
account
|
||||||
|
</Nav.item>
|
||||||
<Nav.item
|
<Nav.item
|
||||||
className='logout'
|
className='logout'
|
||||||
color='red'
|
color='red'
|
||||||
|
|||||||
85
client/homebrew/navbar/error-navitem.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
require('./error-navitem.less');
|
||||||
|
const React = require('react');
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
|
const ErrorNavItem = createClass({
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
error : '',
|
||||||
|
parent : null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render : function() {
|
||||||
|
const clearError = ()=>{
|
||||||
|
const state = {
|
||||||
|
error : null
|
||||||
|
};
|
||||||
|
if(this.props.parent.state.isSaving) {
|
||||||
|
state.isSaving = false;
|
||||||
|
}
|
||||||
|
this.props.parent.setState(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = this.props.error;
|
||||||
|
const response = error.response;
|
||||||
|
const status = response.status;
|
||||||
|
const message = response.body?.message;
|
||||||
|
let errMsg = '';
|
||||||
|
try {
|
||||||
|
errMsg += `${error.toString()}\n\n`;
|
||||||
|
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||||
|
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
||||||
|
console.log(errMsg);
|
||||||
|
} catch (e){}
|
||||||
|
|
||||||
|
if(status === 409) {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
} else if(status === 412) {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like your Google credentials have
|
||||||
|
expired! Visit our log in page to sign out
|
||||||
|
and sign back in with Google,
|
||||||
|
then try saving again!
|
||||||
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
|
<div className='confirm'>
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className='deny'>
|
||||||
|
Not Now
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer'>
|
||||||
|
Looks like there was a problem saving. <br />
|
||||||
|
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||||
|
here
|
||||||
|
</a>.
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = ErrorNavItem;
|
||||||
75
client/homebrew/navbar/error-navitem.less
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
.navItem.error {
|
||||||
|
position : relative;
|
||||||
|
background-color : @red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorContainer{
|
||||||
|
animation-name: glideDown;
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
position : absolute;
|
||||||
|
top : 100%;
|
||||||
|
left : 50%;
|
||||||
|
z-index : 1000;
|
||||||
|
width : 140px;
|
||||||
|
padding : 3px;
|
||||||
|
color : white;
|
||||||
|
background-color : #333;
|
||||||
|
border : 3px solid #444;
|
||||||
|
border-radius : 5px;
|
||||||
|
transform : translate(-50% + 3px, 10px);
|
||||||
|
text-align : center;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
text-transform : uppercase;
|
||||||
|
a{
|
||||||
|
color : @teal;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-top: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid #444;
|
||||||
|
left: 53px;
|
||||||
|
top: -23px;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-top: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid #333;
|
||||||
|
left: 53px;
|
||||||
|
top: -19px;
|
||||||
|
}
|
||||||
|
.deny {
|
||||||
|
width : 48%;
|
||||||
|
margin : 1px;
|
||||||
|
padding : 5px;
|
||||||
|
background-color : #333;
|
||||||
|
display : inline-block;
|
||||||
|
border-left : 1px solid #666;
|
||||||
|
.animate(background-color);
|
||||||
|
&:hover{
|
||||||
|
background-color : red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirm {
|
||||||
|
width : 48%;
|
||||||
|
margin : 1px;
|
||||||
|
padding : 5px;
|
||||||
|
background-color : #333;
|
||||||
|
display : inline-block;
|
||||||
|
color : white;
|
||||||
|
.animate(background-color);
|
||||||
|
&:hover{
|
||||||
|
background-color : teal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,12 @@ module.exports = function(props){
|
|||||||
rel='noopener noreferrer'>
|
rel='noopener noreferrer'>
|
||||||
report issue
|
report issue
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
|
<Nav.item color='green' icon='fas fa-question-circle'
|
||||||
|
href='/faq'
|
||||||
|
newTab={true}
|
||||||
|
rel='noopener noreferrer'>
|
||||||
|
FAQ
|
||||||
|
</Nav.item>
|
||||||
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
||||||
href='/migrate'
|
href='/migrate'
|
||||||
newTab={true}
|
newTab={true}
|
||||||
|
|||||||
90
client/homebrew/navbar/metadata.navitem.jsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const Moment = require('moment');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
|
||||||
|
const MetadataNav = createClass({
|
||||||
|
displayName : 'MetadataNav',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
showMetaWindow : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount : function() {
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMetaWindow : function(){
|
||||||
|
this.setState((prevProps)=>({
|
||||||
|
showMetaWindow : !prevProps.showMetaWindow
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
getAuthors : function(){
|
||||||
|
if(!this.props.brew.authors || this.props.brew.authors.length == 0) return 'No authors';
|
||||||
|
return <>
|
||||||
|
{this.props.brew.authors.map((author, idx, arr)=>{
|
||||||
|
const spacer = arr.length - 1 == idx ? <></> : <span>, </span>;
|
||||||
|
return <span key={idx}><a className='userPageLink' href={`/user/${author}`}>{author}</a>{spacer}</span>;
|
||||||
|
})}
|
||||||
|
</>;
|
||||||
|
},
|
||||||
|
|
||||||
|
getTags : function(){
|
||||||
|
if(!this.props.brew.tags || this.props.brew.tags.length == 0) return 'No tags';
|
||||||
|
return <>
|
||||||
|
{this.props.brew.tags.map((tag, idx)=>{
|
||||||
|
return <span className='tag' key={idx}>{tag}</span>;
|
||||||
|
})}
|
||||||
|
</>;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSystems : function(){
|
||||||
|
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
|
||||||
|
return this.props.brew.systems.join(', ');
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMetaWindow : function(){
|
||||||
|
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
|
||||||
|
<div className='row'>
|
||||||
|
<h4>Description</h4>
|
||||||
|
<p>{this.props.brew.description || 'No description.'}</p>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<h4>Authors</h4>
|
||||||
|
<p>{this.getAuthors()}</p>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<h4>Tags</h4>
|
||||||
|
<p>{this.getTags()}</p>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<h4>Systems</h4>
|
||||||
|
<p>{this.getSystems()}</p>
|
||||||
|
</div>
|
||||||
|
<div className='row'>
|
||||||
|
<h4>Updated</h4>
|
||||||
|
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <Nav.item icon='fas fa-info-circle' color='grey' className='metadata'
|
||||||
|
onClick={()=>this.toggleMetaWindow()}>
|
||||||
|
{this.props.children}
|
||||||
|
{this.renderMetaWindow()}
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = MetadataNav;
|
||||||
@@ -1,162 +1,272 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import "naturalcrit/styles/colors.less";
|
||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
@keyframes pinkColoring {
|
@keyframes pinkColoring {
|
||||||
//from {color: white;}
|
0% {color : pink;}
|
||||||
//to {color: red;}
|
50% {color : pink;}
|
||||||
0% {color: pink;}
|
75% {color : red;}
|
||||||
50% {color: pink;}
|
100% {color : pink;}
|
||||||
75% {color: red;}
|
}
|
||||||
100% {color: pink;}
|
.homebrew nav {
|
||||||
}
|
.homebrewLogo {
|
||||||
.homebrew nav{
|
.animate(color);
|
||||||
.homebrewLogo{
|
font-family : CodeBold;
|
||||||
.animate(color);
|
font-size : 12px;
|
||||||
font-family : CodeBold;
|
color : white;
|
||||||
font-size : 12px;
|
div {
|
||||||
color : white;
|
margin-top : 2px;
|
||||||
div{
|
margin-bottom : -2px;
|
||||||
margin-top : 2px;
|
}
|
||||||
margin-bottom : -2px;
|
&:hover {
|
||||||
}
|
color : @blue;
|
||||||
&:hover{
|
}
|
||||||
color : @blue;
|
}
|
||||||
}
|
.editTitle.navItem {
|
||||||
}
|
padding : 2px 12px;
|
||||||
.editTitle.navItem{
|
input {
|
||||||
padding : 2px 12px;
|
font-family : "Open Sans", sans-serif;
|
||||||
input{
|
font-size : 12px;
|
||||||
width : 250px;
|
font-weight : 800;
|
||||||
margin : 0;
|
width : 250px;
|
||||||
padding : 2px;
|
margin : 0;
|
||||||
background-color : #444;
|
padding : 2px;
|
||||||
font-family : 'Open Sans', sans-serif;
|
text-align : center;
|
||||||
font-size : 12px;
|
color : white;
|
||||||
font-weight : 800;
|
border : 1px solid @blue;
|
||||||
color : white;
|
outline : none;
|
||||||
text-align : center;
|
background-color : transparent;
|
||||||
border : 1px solid @blue;
|
}
|
||||||
outline : none;
|
.charCount {
|
||||||
}
|
display : inline-block;
|
||||||
.charCount{
|
margin-left : 8px;
|
||||||
display : inline-block;
|
text-align : right;
|
||||||
vertical-align : bottom;
|
vertical-align : bottom;
|
||||||
margin-left : 8px;
|
color : #666;
|
||||||
color : #666;
|
&.max {
|
||||||
text-align : right;
|
color : @red;
|
||||||
&.max{
|
}
|
||||||
color : @red;
|
}
|
||||||
}
|
}
|
||||||
}
|
.brewTitle.navItem {
|
||||||
}
|
font-size : 12px;
|
||||||
.brewTitle.navItem{
|
font-weight : 800;
|
||||||
font-size : 12px;
|
height : 100%;
|
||||||
font-weight : 800;
|
text-align : center;
|
||||||
color : white;
|
text-transform : initial;
|
||||||
text-align : center;
|
color : white;
|
||||||
text-transform : initial;
|
background-color : transparent;
|
||||||
}
|
flex-grow : 1;
|
||||||
.save-menu {
|
}
|
||||||
.dropdown {
|
.save-menu {
|
||||||
z-index: 1000;
|
.dropdown {
|
||||||
}
|
z-index : 1000;
|
||||||
.navItem i.fa-power-off {
|
}
|
||||||
color : red;
|
.navItem i.fa-power-off {
|
||||||
&.active {
|
color : red;
|
||||||
color : rgb(0, 182, 52);
|
&.active {
|
||||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765))
|
color : rgb(0, 182, 52);
|
||||||
}
|
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.patreon.navItem{
|
}
|
||||||
border-left : 1px solid #666;
|
.patreon.navItem {
|
||||||
border-right : 1px solid #666;
|
border-right : 1px solid #666;
|
||||||
&:hover i {
|
border-left : 1px solid #666;
|
||||||
color: red;
|
&:hover i {
|
||||||
}
|
color : red;
|
||||||
i{
|
}
|
||||||
.animate(color);
|
i {
|
||||||
animation-name: pinkColoring;
|
.animate(color);
|
||||||
animation-duration: 2s;
|
animation-name : pinkColoring;
|
||||||
color: pink;
|
animation-duration : 2s;
|
||||||
}
|
color : pink;
|
||||||
}
|
}
|
||||||
.recent.navItem {
|
}
|
||||||
position : relative;
|
.recent.navDropdownContainer {
|
||||||
.dropdown{
|
position : relative;
|
||||||
position : absolute;
|
.navDropdown .navItem {
|
||||||
top : 28px;
|
overflow : hidden auto;
|
||||||
left : 0px;
|
max-height : ~"calc(100vh - 28px)";
|
||||||
z-index : 10000;
|
scrollbar-color : #666 #333;
|
||||||
width : 100%;
|
scrollbar-width : thin;
|
||||||
overflow : hidden auto;
|
|
||||||
max-height : ~"calc(100vh - 28px)";
|
|
||||||
scrollbar-color : #666 #333;
|
#backgroundColorsHover;
|
||||||
scrollbar-width : thin;
|
.animate(background-color);
|
||||||
h4{
|
position : relative;
|
||||||
display : block;
|
display : block;
|
||||||
box-sizing : border-box;
|
overflow : clip;
|
||||||
padding : 5px 0px;
|
box-sizing : border-box;
|
||||||
background-color : #333;
|
padding : 8px 5px 13px;
|
||||||
font-size : 0.8em;
|
text-decoration : none;
|
||||||
color : #bbb;
|
color : white;
|
||||||
text-align : center;
|
border-top : 1px solid #888;
|
||||||
border-top : 1px solid #888;
|
background-color : #333;
|
||||||
&:nth-of-type(1){ background-color: darken(@teal, 20%); }
|
.clear {
|
||||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
position : absolute;
|
||||||
}
|
top : 50%;
|
||||||
.item{
|
right : 0;
|
||||||
#backgroundColorsHover;
|
display : none;
|
||||||
.animate(background-color);
|
width : 20px;
|
||||||
position : relative;
|
height : 100%;
|
||||||
display : block;
|
transform : translateY(-50%);
|
||||||
box-sizing : border-box;
|
opacity : 70%;
|
||||||
padding : 8px 5px 13px;
|
border-radius : 3px;
|
||||||
background-color : #333;
|
background-color : #333;
|
||||||
color : white;
|
&:hover {
|
||||||
text-decoration : none;
|
opacity : 100%;
|
||||||
border-top : 1px solid #888;
|
}
|
||||||
&:hover{
|
i {
|
||||||
background-color : @blue;
|
font-size : 10px;
|
||||||
}
|
width : 100%;
|
||||||
.title{
|
height : 100%;
|
||||||
display : inline-block;
|
margin : 0;
|
||||||
overflow : hidden;
|
text-align : center;
|
||||||
width : 100%;
|
}
|
||||||
text-overflow : ellipsis;
|
}
|
||||||
white-space : nowrap;
|
&:hover {
|
||||||
}
|
background-color : @blue;
|
||||||
.time{
|
.clear {
|
||||||
position : absolute;
|
display : grid;
|
||||||
right : 2px;
|
place-content : center;
|
||||||
bottom : 2px;
|
}
|
||||||
font-size : 0.7em;
|
}
|
||||||
color : #888;
|
.title {
|
||||||
}
|
display : inline-block;
|
||||||
}
|
overflow : hidden;
|
||||||
}
|
width : 100%;
|
||||||
}
|
white-space : nowrap;
|
||||||
.warning.navItem{
|
text-overflow : ellipsis;
|
||||||
position : relative;
|
}
|
||||||
background-color : @orange;
|
.time {
|
||||||
color : white;
|
font-size : 0.7em;
|
||||||
&:hover>.dropdown{
|
position : absolute;
|
||||||
visibility : visible;
|
right : 2px;
|
||||||
}
|
bottom : 2px;
|
||||||
.dropdown{
|
color : #888;
|
||||||
position : absolute;
|
}
|
||||||
display : block;
|
&.header {
|
||||||
top : 28px;
|
display : block;
|
||||||
left : 0px;
|
box-sizing : border-box;
|
||||||
visibility : hidden;
|
padding : 5px 0;
|
||||||
z-index : 10000;
|
text-align : center;
|
||||||
box-sizing : border-box;
|
color : #BBB;
|
||||||
width : 100%;
|
border-top : 1px solid #888;
|
||||||
padding : 13px 5px;
|
background-color : #333;
|
||||||
background-color : #333;
|
&:nth-of-type(1) {
|
||||||
text-align : center;
|
background-color : darken(@teal, 20%);
|
||||||
}
|
}
|
||||||
}
|
&:nth-of-type(2) {
|
||||||
.account.navItem{
|
background-color : darken(@purple, 30%);
|
||||||
min-width: 100px;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.metadata.navItem {
|
||||||
|
position : relative;
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
height : 100%;
|
||||||
|
padding : 0;
|
||||||
|
flex-grow : 1;
|
||||||
|
i {
|
||||||
|
margin-right : 10px;
|
||||||
|
}
|
||||||
|
.window {
|
||||||
|
position : absolute;
|
||||||
|
z-index : -1;
|
||||||
|
bottom : 0;
|
||||||
|
left : 50%;
|
||||||
|
display : flex;
|
||||||
|
justify-content : flex-start;
|
||||||
|
width : 440px;
|
||||||
|
max-height : ~"calc(100vh - 28px)";
|
||||||
|
margin : 0 auto;
|
||||||
|
padding : 0 10px 5px;
|
||||||
|
transition : transform 0.4s, opacity 0.4s;
|
||||||
|
border : 3px solid #444;
|
||||||
|
border-top : unset;
|
||||||
|
border-radius : 0 0 5px 5px;
|
||||||
|
background-color : #333;
|
||||||
|
box-shadow : inset 0 7px 9px -7px #111;
|
||||||
|
flex-flow : row wrap;
|
||||||
|
align-content : baseline;
|
||||||
|
&.active {
|
||||||
|
transform : translateX(-50%) translateY(100%);
|
||||||
|
opacity : 1;
|
||||||
|
}
|
||||||
|
&.inactive {
|
||||||
|
transform : translateX(-50%) translateY(0%);
|
||||||
|
opacity : 0;
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display : flex;
|
||||||
|
width : 100%;
|
||||||
|
flex-flow : row wrap;
|
||||||
|
h4 {
|
||||||
|
display : block;
|
||||||
|
box-sizing : border-box;
|
||||||
|
min-width : 76px;
|
||||||
|
padding : 5px 0;
|
||||||
|
text-align : center;
|
||||||
|
color : #BBB;
|
||||||
|
flex-basis : 20%;
|
||||||
|
flex-grow : 1;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-family : "Open Sans", sans-serif;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : normal;
|
||||||
|
padding : 5px 0;
|
||||||
|
text-transform : initial;
|
||||||
|
flex-basis : 80%;
|
||||||
|
flex-grow : 1;
|
||||||
|
.tag {
|
||||||
|
display : inline-block;
|
||||||
|
margin : 2px 2px;
|
||||||
|
padding : 2px;
|
||||||
|
border : 2px solid grey;
|
||||||
|
border-radius : 5px;
|
||||||
|
background-color : #444;
|
||||||
|
}
|
||||||
|
a.userPageLink {
|
||||||
|
text-decoration : none;
|
||||||
|
color : white;
|
||||||
|
&:hover {
|
||||||
|
text-decoration : underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:nth-of-type(even) {
|
||||||
|
background-color : #555;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.warning.navItem {
|
||||||
|
position : relative;
|
||||||
|
color : white;
|
||||||
|
background-color : @orange;
|
||||||
|
&:hover > .dropdown {
|
||||||
|
visibility : visible;
|
||||||
|
}
|
||||||
|
.dropdown {
|
||||||
|
position : absolute;
|
||||||
|
z-index : 10000;
|
||||||
|
top : 28px;
|
||||||
|
left : 0;
|
||||||
|
display : block;
|
||||||
|
visibility : hidden;
|
||||||
|
box-sizing : border-box;
|
||||||
|
width : 100%;
|
||||||
|
padding : 13px 5px;
|
||||||
|
text-align : center;
|
||||||
|
background-color : #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.account.navItem {
|
||||||
|
min-width : 100px;
|
||||||
|
}
|
||||||
|
.account.username.navItem {
|
||||||
|
text-transform : none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,37 +119,58 @@ const RecentItems = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeItem : function(url, evt){
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||||
|
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||||
|
|
||||||
|
edited = edited.filter((item)=>{ return (item.url !== url);});
|
||||||
|
viewed = viewed.filter((item)=>{ return (item.url !== url);});
|
||||||
|
|
||||||
|
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||||
|
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
edit : edited,
|
||||||
|
view : viewed
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
renderDropdown : function(){
|
renderDropdown : function(){
|
||||||
if(!this.state.showDropdown) return null;
|
// if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
const makeItems = (brews)=>{
|
const makeItems = (brews)=>{
|
||||||
return _.map(brews, (brew, i)=>{
|
return _.map(brews, (brew, i)=>{
|
||||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
return <a className='navItem' href={brew.url} key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||||
|
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||||
</a>;
|
</a>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className='dropdown'>
|
return <>
|
||||||
{(this.props.showEdit && this.props.showView) ?
|
{(this.props.showEdit && this.props.showView) ?
|
||||||
<h4>edited</h4> : null }
|
<Nav.item className='header'>edited</Nav.item> : null }
|
||||||
{this.props.showEdit ?
|
{this.props.showEdit ?
|
||||||
makeItems(this.state.edit) : null }
|
makeItems(this.state.edit) : null }
|
||||||
{(this.props.showEdit && this.props.showView) ?
|
{(this.props.showEdit && this.props.showView) ?
|
||||||
<h4>viewed</h4> : null }
|
<Nav.item className='header'>viewed</Nav.item> : null }
|
||||||
{this.props.showView ?
|
{this.props.showView ?
|
||||||
makeItems(this.state.view) : null }
|
makeItems(this.state.view) : null }
|
||||||
</div>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <Nav.item icon='fas fa-history' color='grey' className='recent'
|
return <Nav.dropdown className='recent'>
|
||||||
onMouseEnter={()=>this.handleDropdown(true)}
|
<Nav.item icon='fas fa-history' color='grey' >
|
||||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
{this.props.text}
|
||||||
{this.props.text}
|
</Nav.item>
|
||||||
{this.renderDropdown()}
|
{this.renderDropdown()}
|
||||||
</Nav.item>;
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
74
client/homebrew/pages/accountPage/accountPage.jsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const cx = require('classnames');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
|
||||||
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
|
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||||
|
|
||||||
|
const AccountPage = createClass({
|
||||||
|
displayName : 'AccountPage',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
brew : {},
|
||||||
|
uiItems : {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
uiItems : this.props.uiItems
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderNavItems : function() {
|
||||||
|
return <Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUiItems : function() {
|
||||||
|
return <>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||||
|
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
|
||||||
|
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||||
|
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
||||||
|
</div>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||||
|
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
||||||
|
{this.props.uiItems.googleId &&
|
||||||
|
<p>
|
||||||
|
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <UIPage brew={this.props.brew}>
|
||||||
|
{this.renderUiItems()}
|
||||||
|
</UIPage>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = AccountPage;
|
||||||
@@ -4,9 +4,9 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const request = require('superagent');
|
const request = require('../../../../utils/request-middleware.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.png');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
@@ -18,7 +18,8 @@ const BrewItem = createClass({
|
|||||||
description : '',
|
description : '',
|
||||||
authors : [],
|
authors : [],
|
||||||
stubbed : true
|
stubbed : true
|
||||||
}
|
},
|
||||||
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -33,8 +34,12 @@ const BrewItem = createClass({
|
|||||||
|
|
||||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end((err, res)=>{
|
||||||
location.reload();
|
if(err) {
|
||||||
|
this.props.reportError(err);
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.googleDriveIcon {
|
.googleDriveIcon {
|
||||||
height : 20px;
|
height : 18px;
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const ListPage = createClass({
|
|||||||
brews : []
|
brews : []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
navItems : <></>
|
navItems : <></>,
|
||||||
|
reportError : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -81,7 +82,7 @@ const ListPage = createClass({
|
|||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
return _.map(brews, (brew, idx)=>{
|
return _.map(brews, (brew, idx)=>{
|
||||||
return <BrewItem brew={brew} key={idx}/>;
|
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -218,12 +219,13 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='listPage sitePage'>
|
return <div className='listPage sitePage'>
|
||||||
|
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
||||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
||||||
{this.props.navItems}
|
{this.props.navItems}
|
||||||
{this.renderSortOptions()}
|
{this.renderSortOptions()}
|
||||||
|
|
||||||
<div className='content V3'>
|
<div className='content V3'>
|
||||||
<div className='phb page'>
|
<div className='page'>
|
||||||
{this.renderBrewCollection(this.state.brewCollection)}
|
{this.renderBrewCollection(this.state.brewCollection)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,14 +10,15 @@
|
|||||||
-moz-column-width : auto;
|
-moz-column-width : auto;
|
||||||
-webkit-column-gap : auto;
|
-webkit-column-gap : auto;
|
||||||
-moz-column-gap : auto;
|
-moz-column-gap : auto;
|
||||||
|
height : auto;
|
||||||
|
min-height : 279.4mm;
|
||||||
|
margin : 20px auto;
|
||||||
}
|
}
|
||||||
.listPage{
|
.listPage{
|
||||||
.content{
|
.content{
|
||||||
.phb{
|
z-index : 1;
|
||||||
.noColumns();
|
.page{
|
||||||
height : auto;
|
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
|
||||||
min-height : 279.4mm;
|
|
||||||
margin : 20px auto;
|
|
||||||
&::after{
|
&::after{
|
||||||
display : none;
|
display : none;
|
||||||
}
|
}
|
||||||
@@ -26,7 +27,29 @@
|
|||||||
font-size : 1.3em;
|
font-size : 1.3em;
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
}
|
}
|
||||||
|
.brewCollection {
|
||||||
|
h1:hover{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.active::before, .inactive::before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 0.6cm;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
color: var(--HB_Color_HeaderText);
|
||||||
|
}
|
||||||
|
.active::before {
|
||||||
|
content: '\f107';
|
||||||
|
}
|
||||||
|
.inactive {
|
||||||
|
color: #707070;
|
||||||
|
}
|
||||||
|
.inactive::before {
|
||||||
|
content: '\f105';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sort-container{
|
.sort-container{
|
||||||
@@ -41,7 +64,7 @@
|
|||||||
border-bottom : 1px solid #666;
|
border-bottom : 1px solid #666;
|
||||||
color : white;
|
color : white;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
z-index : 500;
|
z-index : 1;
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : center;
|
justify-content : center;
|
||||||
align-items : baseline;
|
align-items : baseline;
|
||||||
|
|||||||
38
client/homebrew/pages/basePages/uiPage/uiPage.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
require('./uiPage.less');
|
||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Navbar = require('../../../navbar/navbar.jsx');
|
||||||
|
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
|
||||||
|
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
|
||||||
|
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
|
||||||
|
const Account = require('../../../navbar/account.navitem.jsx');
|
||||||
|
|
||||||
|
|
||||||
|
const UIPage = createClass({
|
||||||
|
displayName : 'UIPage',
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <div className='uiPage sitePage'>
|
||||||
|
<Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||||
|
</Nav.section>
|
||||||
|
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrewItem />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>
|
||||||
|
|
||||||
|
<div className='content'>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = UIPage;
|
||||||
52
client/homebrew/pages/basePages/uiPage/uiPage.less
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
.homebrew {
|
||||||
|
.uiPage.sitePage {
|
||||||
|
.content {
|
||||||
|
width : ~"min(90vw, 1000px)";
|
||||||
|
padding : 2% 4%;
|
||||||
|
margin-top : 25px;
|
||||||
|
margin-right : auto;
|
||||||
|
margin-left : auto;
|
||||||
|
overflow-y : scroll;
|
||||||
|
font-family : 'Open Sans';
|
||||||
|
font-size : 0.8em;
|
||||||
|
line-height : 1.8em;
|
||||||
|
background-color : #F0F0F0;
|
||||||
|
.dataGroup {
|
||||||
|
padding : 6px 20px 15px;
|
||||||
|
margin : 5px 0px;
|
||||||
|
border : 2px solid black;
|
||||||
|
border-radius : 5px;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
width : 100%;
|
||||||
|
margin : 0.5em 30% 0.25em 0;
|
||||||
|
font-weight : 900;
|
||||||
|
text-transform : uppercase;
|
||||||
|
border-bottom : 2px solid slategrey;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-right : 0;
|
||||||
|
margin-bottom : 0.5em;
|
||||||
|
font-size : 2em;
|
||||||
|
border-bottom : 2px solid darkslategrey;
|
||||||
|
}
|
||||||
|
h2 { font-size : 1.75em; }
|
||||||
|
h3 {
|
||||||
|
font-size : 1.5em;
|
||||||
|
svg { width : 19px; }
|
||||||
|
}
|
||||||
|
h4 { font-size : 1.25em; }
|
||||||
|
strong { font-weight : bold; }
|
||||||
|
em { font-style : italic; }
|
||||||
|
ul {
|
||||||
|
padding-left : 1.25em;
|
||||||
|
list-style : square;
|
||||||
|
}
|
||||||
|
.blank {
|
||||||
|
height : 1em;
|
||||||
|
margin-top : 0;
|
||||||
|
& + * { margin-top : 0; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ require('./editPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -12,6 +12,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
|
||||||
@@ -21,8 +22,9 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const googleDriveActive = require('../../googleDrive.png');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const googleDriveInactive = require('../../googleDriveMono.png');
|
|
||||||
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 3000;
|
const SAVE_TIMEOUT = 3000;
|
||||||
|
|
||||||
@@ -30,24 +32,7 @@ const EditPage = createClass({
|
|||||||
displayName : 'EditPage',
|
displayName : 'EditPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW_LOAD
|
||||||
text : '',
|
|
||||||
style : '',
|
|
||||||
shareId : null,
|
|
||||||
editId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
gDrive : false,
|
|
||||||
trashed : false,
|
|
||||||
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
tags : '',
|
|
||||||
published : false,
|
|
||||||
authors : [],
|
|
||||||
systems : [],
|
|
||||||
renderer : 'legacy'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,7 +45,7 @@ const EditPage = createClass({
|
|||||||
alertLoginToTransfer : false,
|
alertLoginToTransfer : false,
|
||||||
saveGoogle : this.props.brew.googleId ? true : false,
|
saveGoogle : this.props.brew.googleId ? true : false,
|
||||||
confirmGoogleTransfer : false,
|
confirmGoogleTransfer : false,
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||||
url : '',
|
url : '',
|
||||||
autoSave : true,
|
autoSave : true,
|
||||||
@@ -75,10 +60,9 @@ const EditPage = createClass({
|
|||||||
url : window.location.href
|
url : window.location.href
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||||
|
|
||||||
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) }, ()=>{
|
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
||||||
if(this.state.autoSave){
|
if(this.state.autoSave){
|
||||||
this.trySave();
|
this.trySave();
|
||||||
} else {
|
} else {
|
||||||
@@ -172,7 +156,10 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||||
}));
|
}));
|
||||||
this.clearErrors();
|
this.setState({
|
||||||
|
error : null,
|
||||||
|
isSaving : false
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
closeAlerts : function(event){
|
closeAlerts : function(event){
|
||||||
@@ -188,24 +175,16 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
saveGoogle : !prevState.saveGoogle,
|
saveGoogle : !prevState.saveGoogle,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
errors : null
|
error : null
|
||||||
}), ()=>this.save());
|
}), ()=>this.save());
|
||||||
},
|
},
|
||||||
|
|
||||||
clearErrors : function(){
|
|
||||||
this.setState({
|
|
||||||
errors : null,
|
|
||||||
isSaving : false
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
isSaving : true,
|
isSaving : true,
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -220,8 +199,9 @@ const EditPage = createClass({
|
|||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error Updating Local Brew');
|
console.log('Error Updating Local Brew');
|
||||||
this.setState({ errors: err });
|
this.setState({ error: err });
|
||||||
});
|
});
|
||||||
|
if(!res) return;
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
this.savedBrew = res.body;
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
@@ -230,7 +210,8 @@ const EditPage = createClass({
|
|||||||
brew : { ...prevState.brew,
|
brew : { ...prevState.brew,
|
||||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||||
editId : this.savedBrew.editId,
|
editId : this.savedBrew.editId,
|
||||||
shareId : this.savedBrew.shareId
|
shareId : this.savedBrew.shareId,
|
||||||
|
version : this.savedBrew.version
|
||||||
},
|
},
|
||||||
isPending : false,
|
isPending : false,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
@@ -240,10 +221,7 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderGoogleDriveIcon : function(){
|
||||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||||
{this.state.saveGoogle
|
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
|
||||||
? <img src={googleDriveActive} alt='googleDriveActive'/>
|
|
||||||
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{this.state.confirmGoogleTransfer &&
|
{this.state.confirmGoogleTransfer &&
|
||||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
@@ -276,71 +254,19 @@ const EditPage = createClass({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{this.state.alertTrashedGoogleBrew &&
|
||||||
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
|
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
||||||
|
<div className='confirm'>
|
||||||
|
OK
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
if(this.state.errors){
|
|
||||||
let errMsg = '';
|
|
||||||
try {
|
|
||||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
|
||||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
|
||||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
|
||||||
console.log(errMsg);
|
|
||||||
} catch (e){}
|
|
||||||
|
|
||||||
// if(this.state.errors.status == '401'){
|
|
||||||
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
// Oops!
|
|
||||||
// <div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
// You must be signed in to a Google account
|
|
||||||
// to save this to<br />Google Drive!<br />
|
|
||||||
// <a target='_blank' rel='noopener noreferrer'
|
|
||||||
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
// <div className='confirm'>
|
|
||||||
// Sign In
|
|
||||||
// </div>
|
|
||||||
// </a>
|
|
||||||
// <div className='deny'>
|
|
||||||
// Not Now
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </Nav.item>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
Looks like your Google credentials have
|
|
||||||
expired! Visit our log in page to sign out
|
|
||||||
and sign back in with Google,
|
|
||||||
then try saving again!
|
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
<div className='confirm'>
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div className='deny'>
|
|
||||||
Not Now
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer'>
|
|
||||||
Looks like there was a problem saving. <br />
|
|
||||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
|
||||||
here
|
|
||||||
</a>.
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.autoSaveWarning && this.hasChanges()){
|
if(this.state.autoSaveWarning && this.hasChanges()){
|
||||||
this.setAutosaveWarning();
|
this.setAutosaveWarning();
|
||||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||||
@@ -384,6 +310,12 @@ const EditPage = createClass({
|
|||||||
this.warningTimer;
|
this.warningTimer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errorReported : function(error) {
|
||||||
|
this.setState({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
renderAutoSaveButton : function(){
|
renderAutoSaveButton : function(){
|
||||||
return <Nav.item onClick={this.handleAutoSave}>
|
return <Nav.item onClick={this.handleAutoSave}>
|
||||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||||
@@ -412,26 +344,19 @@ const EditPage = createClass({
|
|||||||
const shareLink = this.processShareId();
|
const shareLink = this.processShareId();
|
||||||
|
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
|
|
||||||
{this.state.alertTrashedGoogleBrew &&
|
|
||||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
|
||||||
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
|
|
||||||
<div className='confirm'>
|
|
||||||
OK
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
<Nav.dropdown className='save-menu'>
|
{this.state.error ?
|
||||||
{this.renderSaveButton()}
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
{this.renderAutoSaveButton()}
|
<Nav.dropdown className='save-menu'>
|
||||||
</Nav.dropdown>
|
{this.renderSaveButton()}
|
||||||
|
{this.renderAutoSaveButton()}
|
||||||
|
</Nav.dropdown>
|
||||||
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem/>
|
<HelpNavItem/>
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
@@ -469,9 +394,17 @@ const EditPage = createClass({
|
|||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors} />
|
<BrewRenderer
|
||||||
|
text={this.state.brew.text}
|
||||||
|
style={this.state.brew.style}
|
||||||
|
renderer={this.state.brew.renderer}
|
||||||
|
theme={this.state.brew.theme}
|
||||||
|
errors={this.state.htmlErrors}
|
||||||
|
lang={this.state.brew.lang}
|
||||||
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -13,87 +13,17 @@
|
|||||||
cursor : initial;
|
cursor : initial;
|
||||||
color : #666;
|
color : #666;
|
||||||
}
|
}
|
||||||
&.error{
|
|
||||||
position : relative;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.googleDriveStorage {
|
.googleDriveStorage {
|
||||||
position : relative;
|
position : relative;
|
||||||
}
|
}
|
||||||
.googleDriveStorage img{
|
.googleDriveStorage img{
|
||||||
height : 20px;
|
height : 18px;
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
|
||||||
.errorContainer{
|
&.inactive {
|
||||||
animation-name: glideDown;
|
filter: grayscale(1);
|
||||||
animation-duration: 0.4s;
|
|
||||||
position : absolute;
|
|
||||||
top : 100%;
|
|
||||||
left : 50%;
|
|
||||||
z-index : 500;
|
|
||||||
width : 140px;
|
|
||||||
padding : 3px;
|
|
||||||
color : white;
|
|
||||||
background-color : #333;
|
|
||||||
border : 3px solid #444;
|
|
||||||
border-radius : 5px;
|
|
||||||
transform : translate(-50% + 3px, 10px);
|
|
||||||
text-align : center;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
text-transform : uppercase;
|
|
||||||
a{
|
|
||||||
color : @teal;
|
|
||||||
}
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #444;
|
|
||||||
left: 53px;
|
|
||||||
top: -23px;
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #333;
|
|
||||||
left: 53px;
|
|
||||||
top: -19px;
|
|
||||||
}
|
|
||||||
.deny {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
border-left : 1px solid #666;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.confirm {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
color : white;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : teal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,44 +4,37 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
const ErrorIndex = require('./errors/errorIndex.js');
|
||||||
|
|
||||||
const ErrorPage = createClass({
|
const ErrorPage = createClass({
|
||||||
|
displayName : 'ErrorPage',
|
||||||
|
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
ver : '0.0.0',
|
ver : '0.0.0',
|
||||||
errorId : ''
|
errorId : '',
|
||||||
|
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
||||||
|
error : {}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
|
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='errorPage sitePage'>
|
const errorText = ErrorIndex(this.props)[this.props.brew.HBErrorCode.toString()] || '';
|
||||||
<Navbar ver={this.props.ver}>
|
|
||||||
<Nav.section>
|
|
||||||
<Nav.item className='errorTitle'>
|
|
||||||
Crit Fail!
|
|
||||||
</Nav.item>
|
|
||||||
</Nav.section>
|
|
||||||
|
|
||||||
<Nav.section>
|
return <UIPage brew={{ title: 'Crit Fail!' }}>
|
||||||
<PatreonNavItem />
|
<div className='dataGroup'>
|
||||||
<HelpNavItem />
|
<div className='errorTitle'>
|
||||||
<RecentNavItem />
|
<h1>{`Error ${this.props.brew.status || '000'}`}</h1>
|
||||||
</Nav.section>
|
<h4>{this.props.brew.text || 'No error text'}</h4>
|
||||||
</Navbar>
|
</div>
|
||||||
|
<hr />
|
||||||
<div className='content'>
|
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
|
||||||
<BrewRenderer text={this.text} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</UIPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
.errorPage{
|
.homebrew {
|
||||||
.errorTitle{
|
.uiPage.sitePage {
|
||||||
background-color: @orange;
|
.errorTitle {
|
||||||
|
//background-color: @orange;
|
||||||
|
color : #D02727;
|
||||||
|
text-align : center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
h1, h2, h3, h4 { border-bottom : none; }
|
||||||
|
hr { border-bottom : 2px solid slategrey; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
126
client/homebrew/pages/errorPage/errors/errorIndex.js
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
|
const errorIndex = (props)=>{
|
||||||
|
return {
|
||||||
|
// Default catch all
|
||||||
|
'00' : dedent`
|
||||||
|
## An unknown error occurred!
|
||||||
|
|
||||||
|
We aren't sure what happened, but our server wasn't able to find what you
|
||||||
|
were looking for.`,
|
||||||
|
|
||||||
|
// General Google load error
|
||||||
|
'01' : dedent`
|
||||||
|
## An error occurred while retrieving this brew from Google Drive!
|
||||||
|
|
||||||
|
Google reported an error while attempting to retrieve a brew from this link.`,
|
||||||
|
|
||||||
|
// Google Drive - 404 : brew deleted or access denied
|
||||||
|
'02' : dedent`
|
||||||
|
## We can't find this brew in Google Drive!
|
||||||
|
|
||||||
|
This file was saved on Google Drive, but this link doesn't work anymore.
|
||||||
|
${ props.brew.authors?.length > 0
|
||||||
|
? `Note that this brew belongs to the Homebrewery account **${ props.brew.authors[0] }**,
|
||||||
|
${ props.brew.account
|
||||||
|
? `which is
|
||||||
|
${props.brew.authors[0] == props.brew.account
|
||||||
|
? `your account.`
|
||||||
|
: `not your account (you are currently signed in as **${props.brew.account}**).`
|
||||||
|
}`
|
||||||
|
: 'and you are not currently signed in to any account.'
|
||||||
|
}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
The Homebrewery cannot delete files from Google Drive on its own, so there
|
||||||
|
are three most likely possibilities:
|
||||||
|
:
|
||||||
|
- **The Google Drive files may have been accidentally deleted.** Look in
|
||||||
|
the Google Drive account that owns this brew (or ask the owner to do so),
|
||||||
|
and make sure the Homebrewery folder is still there, and that it holds your brews
|
||||||
|
as text files.
|
||||||
|
- **You may have changed the sharing settings for your files.** If the files
|
||||||
|
are still on Google Drive, change all of them to be shared *with everyone who has
|
||||||
|
the link* so the Homebrewery can access them.
|
||||||
|
- **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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
shows the recent activity on Google Drive. This can help you pin down the exact date
|
||||||
|
the brew was deleted or moved, and by whom.
|
||||||
|
:
|
||||||
|
If the brew still isn't found, some people have had success asking Google to recover
|
||||||
|
accidentally deleted files at this link:
|
||||||
|
https://support.google.com/drive/answer/1716222?hl=en&ref_topic=7000946.
|
||||||
|
At the bottom of the page there is a button that says *Send yourself an Email*
|
||||||
|
and you will receive instructions on how to request the files be restored.
|
||||||
|
:
|
||||||
|
Also note, if you prefer not to use your Google Drive for storage, you can always
|
||||||
|
change the storage location of a brew by clicking the Google drive icon by the
|
||||||
|
brew title and choosing *transfer my brew to/from Google Drive*.`,
|
||||||
|
|
||||||
|
// User is not Authors list
|
||||||
|
'03' : dedent`
|
||||||
|
## Current signed-in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask one of its authors to invite you
|
||||||
|
as an author by opening the Edit page for the brew, viewing the {{fa,fa-info-circle}}
|
||||||
|
**Properties** tab, and adding your username to the "invited authors" list. You can
|
||||||
|
then try to access this document again.
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
||||||
|
|
||||||
|
// User is not signed in; must be a user on the Authors List
|
||||||
|
'04' : dedent`
|
||||||
|
## Sign-in required to edit this brew.
|
||||||
|
|
||||||
|
You must be logged in to one of the accounts listed as an author of this brew.
|
||||||
|
User is not logged in. Please log in [here](${loginUrl}).
|
||||||
|
|
||||||
|
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
|
||||||
|
|
||||||
|
**Current Authors:** ${props.brew.authors?.map((author)=>{return `${author}`;}).join(', ') || 'Unable to list authors'}`,
|
||||||
|
|
||||||
|
// Brew load error
|
||||||
|
'05' : dedent`
|
||||||
|
## No Homebrewery document could be found.
|
||||||
|
|
||||||
|
The server could not locate the Homebrewery document. It was likely deleted by
|
||||||
|
its owner.
|
||||||
|
|
||||||
|
**Requested access:** ${props.brew.accessType}
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Brew save error
|
||||||
|
'06' : dedent`
|
||||||
|
## Unable to save Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred wil attempting to save the Homebrewery document.`,
|
||||||
|
|
||||||
|
// Brew delete error
|
||||||
|
'07' : dedent`
|
||||||
|
## Unable to delete Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred while attempting to remove the Homebrewery document.
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
|
// Author delete error
|
||||||
|
'08' : dedent`
|
||||||
|
## Unable to remove user from Homebrewery document.
|
||||||
|
|
||||||
|
An error occurred while attempting to remove the user from the Homebrewery document author list!
|
||||||
|
|
||||||
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = errorIndex;
|
||||||
@@ -3,7 +3,7 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -12,35 +12,38 @@ const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
|||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const HomePage = createClass({
|
const HomePage = createClass({
|
||||||
displayName : 'HomePage',
|
displayName : 'HomePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW,
|
||||||
text : '',
|
ver : '0.0.0'
|
||||||
},
|
|
||||||
ver : '0.0.0'
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text
|
welcomeText : this.props.brew.text,
|
||||||
|
error : undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(this.state.brew)
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err) return;
|
if(err) {
|
||||||
|
this.setState({ error: err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const brew = res.body;
|
const brew = res.body;
|
||||||
window.location = `/edit/${brew.editId}`;
|
window.location = `/edit/${brew.editId}`;
|
||||||
});
|
});
|
||||||
@@ -56,6 +59,10 @@ const HomePage = createClass({
|
|||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar ver={this.props.ver}>
|
return <Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
|
|||||||
@@ -40,4 +40,11 @@
|
|||||||
right : 350px;
|
right : 350px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navItem.save{
|
||||||
|
background-color: @orange;
|
||||||
|
&:hover{
|
||||||
|
background-color: @green;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
|
|||||||
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
||||||
}}
|
}}
|
||||||
|
|
||||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||||
|
|
||||||
{{artist,bottom:160px,left:100px
|
{{artist,bottom:160px,left:100px
|
||||||
##### Homebrew Mug
|
##### Homebrew Mug
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ require('./newPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.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';
|
||||||
@@ -26,36 +29,18 @@ const NewPage = createClass({
|
|||||||
displayName : 'NewPage',
|
displayName : 'NewPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW
|
||||||
text : '',
|
|
||||||
style : undefined,
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
renderer : 'V3',
|
|
||||||
theme : '5ePHB'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
let brew = this.props.brew;
|
const brew = this.props.brew;
|
||||||
|
|
||||||
if(this.props.brew.shareId) {
|
|
||||||
brew = {
|
|
||||||
text : brew.text ?? '',
|
|
||||||
style : brew.style ?? undefined,
|
|
||||||
title : brew.title ?? '',
|
|
||||||
description : brew.description ?? '',
|
|
||||||
renderer : brew.renderer ?? 'legacy',
|
|
||||||
theme : brew.theme ?? '5ePHB'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brew : brew,
|
brew : brew,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text)
|
htmlErrors : Markdown.validate(brew.text)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -76,6 +61,7 @@ const NewPage = createClass({
|
|||||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
// 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;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
brew : brew
|
brew : brew
|
||||||
@@ -83,8 +69,9 @@ const NewPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
if(brew.style)
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
|
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
@@ -128,21 +115,16 @@ const NewPage = createClass({
|
|||||||
handleMetaChange : function(metadata){
|
handleMetaChange : function(metadata){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, ...metadata },
|
brew : { ...prevState.brew, ...metadata },
|
||||||
}));
|
}), ()=>{
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({
|
localStorage.setItem(METAKEY, JSON.stringify({
|
||||||
// 'title' : this.state.brew.title,
|
// 'title' : this.state.brew.title,
|
||||||
// 'description' : this.state.brew.description,
|
// 'description' : this.state.brew.description,
|
||||||
'renderer' : this.state.brew.renderer,
|
'renderer' : this.state.brew.renderer,
|
||||||
'theme' : this.state.brew.theme
|
'theme' : this.state.brew.theme,
|
||||||
}));
|
'lang' : this.state.brew.lang
|
||||||
},
|
}));
|
||||||
|
|
||||||
clearErrors : function(){
|
|
||||||
this.setState({
|
|
||||||
errors : null,
|
|
||||||
isSaving : false
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
;
|
||||||
},
|
},
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
@@ -167,7 +149,7 @@ const NewPage = createClass({
|
|||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
this.setState({ isSaving: false, errors: err });
|
this.setState({ isSaving: false, error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
@@ -179,67 +161,6 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
if(this.state.errors){
|
|
||||||
let errMsg = '';
|
|
||||||
try {
|
|
||||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
|
||||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
|
||||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
|
||||||
console.log(errMsg);
|
|
||||||
} catch (e){}
|
|
||||||
|
|
||||||
// if(this.state.errors.status == '401'){
|
|
||||||
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
// Oops!
|
|
||||||
// <div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
// You must be signed in to a Google account
|
|
||||||
// to save this to<br />Google Drive!<br />
|
|
||||||
// <a target='_blank' rel='noopener noreferrer'
|
|
||||||
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
// <div className='confirm'>
|
|
||||||
// Sign In
|
|
||||||
// </div>
|
|
||||||
// </a>
|
|
||||||
// <div className='deny'>
|
|
||||||
// Not Now
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </Nav.item>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
Looks like your Google credentials have
|
|
||||||
expired! Visit our log in page to sign out
|
|
||||||
and sign back in with Google,
|
|
||||||
then try saving again!
|
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
<div className='confirm'>
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div className='deny'>
|
|
||||||
Not Now
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer'>
|
|
||||||
Looks like there was a problem saving. <br />
|
|
||||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
|
||||||
here
|
|
||||||
</a>.
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.isSaving){
|
if(this.state.isSaving){
|
||||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||||
save...
|
save...
|
||||||
@@ -269,7 +190,10 @@ const NewPage = createClass({
|
|||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderSaveButton()}
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
this.renderSaveButton()
|
||||||
|
}
|
||||||
{this.renderLocalPrintButton()}
|
{this.renderLocalPrintButton()}
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
@@ -291,7 +215,7 @@ const NewPage = createClass({
|
|||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors}/>
|
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} lang={this.state.brew.lang} errors={this.state.htmlErrors}/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -4,79 +4,5 @@
|
|||||||
&:hover{
|
&:hover{
|
||||||
background-color: @green;
|
background-color: @green;
|
||||||
}
|
}
|
||||||
&.error{
|
|
||||||
position : relative;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.errorContainer{
|
|
||||||
animation-name: glideDown;
|
|
||||||
animation-duration: 0.4s;
|
|
||||||
position : absolute;
|
|
||||||
top : 100%;
|
|
||||||
left : 50%;
|
|
||||||
z-index : 100000;
|
|
||||||
width : 140px;
|
|
||||||
padding : 3px;
|
|
||||||
color : white;
|
|
||||||
background-color : #333;
|
|
||||||
border : 3px solid #444;
|
|
||||||
border-radius : 5px;
|
|
||||||
transform : translate(-50% + 3px, 10px);
|
|
||||||
text-align : center;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
text-transform : uppercase;
|
|
||||||
a{
|
|
||||||
color : @teal;
|
|
||||||
}
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #444;
|
|
||||||
left: 53px;
|
|
||||||
top: -23px;
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #333;
|
|
||||||
left: 53px;
|
|
||||||
top: -19px;
|
|
||||||
}
|
|
||||||
.deny {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
border-left : 1px solid #666;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.confirm {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
color : white;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : teal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ const PrintPage = createClass({
|
|||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
text : this.props.brew.text || '',
|
text : this.props.brew.text || '',
|
||||||
style : this.props.brew.style || undefined,
|
style : this.props.brew.style || undefined,
|
||||||
renderer : this.props.brew.renderer || 'legacy'
|
renderer : this.props.brew.renderer || 'legacy',
|
||||||
|
theme : this.props.brew.theme || '5ePHB'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -48,7 +49,7 @@ const PrintPage = createClass({
|
|||||||
text : brewStorage,
|
text : brewStorage,
|
||||||
style : styleStorage,
|
style : styleStorage,
|
||||||
renderer : metaStorage?.renderer || 'legacy',
|
renderer : metaStorage?.renderer || 'legacy',
|
||||||
theme : metaStorage?.theme || '5ePHB'
|
theme : metaStorage?.theme || '5ePHB'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -59,7 +60,8 @@ const PrintPage = createClass({
|
|||||||
|
|
||||||
renderStyle : function() {
|
renderStyle : function() {
|
||||||
if(!this.state.brew.style) return;
|
if(!this.state.brew.style) return;
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.state.brew.style} </style>` }} />;
|
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.state.brew.style}\n} </style>` }} />;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.state.brew.style}\n</style>` }} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPages : function(){
|
renderPages : function(){
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
@@ -12,21 +13,13 @@ 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 SharePage = createClass({
|
const SharePage = createClass({
|
||||||
displayName : 'SharePage',
|
displayName : 'SharePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW_LOAD
|
||||||
title : '',
|
|
||||||
text : '',
|
|
||||||
style : '',
|
|
||||||
shareId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
views : 0,
|
|
||||||
renderer : ''
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -58,8 +51,10 @@ const SharePage = createClass({
|
|||||||
return <div className='sharePage sitePage'>
|
return <div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section>
|
<Nav.section className='titleSection'>
|
||||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
<MetadataNav brew={this.props.brew}>
|
||||||
|
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||||
|
</MetadataNav>
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
.sharePage{
|
.sharePage{
|
||||||
|
.navContent .navSection.titleSection {
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
.content{
|
.content{
|
||||||
overflow-y : hidden;
|
overflow-y : hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
|
||||||
const UserPage = createClass({
|
const UserPage = createClass({
|
||||||
displayName : 'UserPage',
|
displayName : 'UserPage',
|
||||||
@@ -19,7 +20,8 @@ const UserPage = createClass({
|
|||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : [],
|
brews : [],
|
||||||
query : ''
|
query : '',
|
||||||
|
error : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -50,10 +52,19 @@ const UserPage = createClass({
|
|||||||
brewCollection : brewCollection
|
brewCollection : brewCollection
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
errorReported : function(error) {
|
||||||
|
this.setState({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
navItems : function() {
|
navItems : function() {
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
@@ -63,7 +74,7 @@ const UserPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
|
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
12
client/homebrew/utils/request-middleware.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const request = require('superagent');
|
||||||
|
|
||||||
|
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
|
||||||
|
|
||||||
|
const requestMiddleware = {
|
||||||
|
get : (path)=>addHeader(request.get(path)),
|
||||||
|
put : (path)=>addHeader(request.put(path)),
|
||||||
|
post : (path)=>addHeader(request.post(path)),
|
||||||
|
delete : (path)=>addHeader(request.delete(path)),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = requestMiddleware;
|
||||||
1
client/icons/Davek.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 791.04 953.29"><title>Davek</title><g id="Layer_2" data-name="Layer 2"><g id="Davek"><path d="M178.41,13.46a19.33,19.33,0,0,0-4.71,5.38q8.07,6.07,13.46,6.07a8.27,8.27,0,0,0,4.71-1.35,130.23,130.23,0,0,0,16.83-7.07,74.55,74.55,0,0,1,18.85-6.39h2.7q8.07,0,14.81,8.74a944.19,944.19,0,0,0,95.6,4.72q19.5,0,38.37-.67,69.33-2,139.68-5.72t139.7-5.06q16.82-.64,34.34-.66,50.49,0,98.29,3.36-17.5,12.12-22.55,31.64t-5,33.66q.64,22.89.66,45.1,0,47.13-3.36,97-6.07,74.05-9.78,148.11t-5,146.09v17.51a766.1,766.1,0,0,0,8.75,118.48,38.57,38.57,0,0,0-4,17.51,30.94,30.94,0,0,0,.67,6.06q2,12.12,3.36,23.22c.9,7.42,1.57,14.92,2,22.55v3.37a57.93,57.93,0,0,1-3.36,19.52c.43,4.5.67,8.77.67,12.8a260.65,260.65,0,0,1-2.7,37,344.26,344.26,0,0,0-4,52.52,133.5,133.5,0,0,0,8.09,45.44q8.07,22.57,33,36.68-6.06,8.78-20.19,8.77H762.1c-4.5-.45-8.53-.69-12.12-.69a78.11,78.11,0,0,0-21.54,2.7,579.1,579.1,0,0,0-63.64,3.71q-33.31,3.71-67.65,6.39t-68.66,3.37h-4a188.05,188.05,0,0,1-59.92-9.43q20.19-4,39.06-23.22t20.19-47.46q11.44-22.21,11.45-49.82a320.44,320.44,0,0,1,3.36-49.15q-9.45-4.69-10.09-8.75v-2.7a73,73,0,0,1,.66-8.74,105.81,105.81,0,0,0,3.37-12.8,7.49,7.49,0,0,0,.68-3.37q0-4.7-4.05-10.09c.45-4.93.69-10.1.69-15.48a311.71,311.71,0,0,0-3.37-46.45,207.31,207.31,0,0,1-1.35-24.25,274.58,274.58,0,0,1,4-45.1l15.5,6.73q-3.37-17.49-3.37-41.07,0-24.89,8.75-44.44a27.73,27.73,0,0,0,2-9.43,15.32,15.32,0,0,0-3.36-10.09,60.75,60.75,0,0,1-10.1-15.48l-7.39,6.73q2.67-47.79,8.74-99,3.35-33.63,3.37-65.29,0-14.81-.69-29a205.09,205.09,0,0,1-4-41.74,190.26,190.26,0,0,1,2-26.92q4-37,14.81-67.33a25.14,25.14,0,0,1-2.68-11.43,31.13,31.13,0,0,1,.66-6.07V140q0-6.72-8.74-10.09-3.37-16.83-5.73-31.3T521.07,77.41q-55.2,2.7-115.78,4.71-19.55.7-39.72.69-38.38,0-74.06-2.7c-5.4,4.5-8.08,9.21-8.08,14.14v1.34a41.5,41.5,0,0,0,4.37,15.49q3.7,7.4,7.4,15.16a35,35,0,0,1,3.71,15.13q32.31,34.35,64,68.68a335.89,335.89,0,0,1,51.83,73.38q13.46,7.4,18.51,17.49t10.11,19.87q5.06,9.78,10.1,18.85t16.5,11.78v12.12a194.5,194.5,0,0,1-37.38-4q-20.52-4-40.73-6.73a114.48,114.48,0,0,0-17.49-1.35,97.2,97.2,0,0,0-20.2,2q-17.52,4.05-31,20.19-16.84-1.35-27.27-9.75a76.13,76.13,0,0,1-17.51-20.2q-7.06-11.76-14.47-24.9a79.77,79.77,0,0,0-18.84-22.57A305.87,305.87,0,0,1,177.73,237q-28.29-33.67-54.54-69T68,99.31A381.16,381.16,0,0,0,0,38.37q12.79,0,22.89-9.75A190.69,190.69,0,0,1,44.76,10.44Q56.54,2,68.66,0H72Q82.8,0,97,10.76Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
1
client/icons/Iokharic.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 428.05 941.17"><title>Iokharic</title><g id="Layer_2" data-name="Layer 2"><g id="Iokharic"><path d="M334.76,909.61V259.3l2.74-89.18c3.43,0,6.18-8.23,7.55-24.69,3.43,0,7.55-8.92,13.72-27.44,13-11,19.89-21.27,19.89-31.56,0-13-5.48-20.58-17.15-23.32l-30.87,2.74H320.36c-21.27,13-39.79,22.64-56.94,27.44h-37c-11.67,0-26.76,7.55-46,22q-12.34,0-30.86,16.46c-10.29,0-40.48,26.75-91.93,80.95,0,8.23-6.17,21.26-18.52,38.41l-3.43,15.78v41.84L67.23,343c2.74,0,9.6,6.86,19.89,19.9,24,18.52,36.36,30.86,36.36,38.41l-12.35,10.29H105c-24.7-15.78-45.28-32.93-62.43-52.13L15.78,316.92,0,266.85c3.43-17.84,7.55-29.5,13.72-35v-11c0-18.52,7.55-39.79,22-63.8,0-9.6,8.23-21.27,24.7-34.3,0-9.6,15.77-26.07,46.64-50.08,19.9-16.46,46-28.12,76.83-35,5.49-6.86,21.27-14.41,46.65-21.95C238,5.49,251.07,0,270.28,0h137.2c8.91,0,15.77,8.23,20.57,24V40.47l-5.48,8.23V166c0,17.15-7.55,31.55-21.95,43.22v41.15l-2.75,24.7q0,9.26,24.7,30.87v38.41c0,10.29-4.81,19.9-15.09,28.82h-6.86V558.39c0,55.57-4.81,97.41-15.1,124.16-4.8,2.75-7.54,19.21-9.6,48.71l2.74,17.15-2.74,76.14v30.19q0,32.93-32.93,86.43C337.5,937.74,334.76,926.76,334.76,909.61Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
1
client/icons/Rellanic.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527.7 940.25"><title>Rellanic</title><g id="Layer_2" data-name="Layer 2"><g id="Rellanic"><path d="M527.7,5.45q-3.83,19.65-15,30.56a129.61,129.61,0,0,1-26.46,19.64q-9.84,6.56-31.66,15.28-19.63,7.65-31.64,16.38Q380.33,103.69,342.16,108a468.46,468.46,0,0,1-54,3.28q-15.83,0-30.56-1.1a53.19,53.19,0,0,0-20.19-6.55H217.74q-7.12,1.11-21.29,1.1a51.67,51.67,0,0,1-20.18-4.36q8.72,19.65,25.63,29.46,14.19,8.74,28.38,29.47a634.05,634.05,0,0,1,98.78,90.58l91.12,103.69a65.1,65.1,0,0,0-.54,8.19,42.47,42.47,0,0,0,.54,7.09c.73,1.82,1.27,3.29,1.64,4.37q7.08,8.75,10.92,12,1.62,1.1,12.55,14.19a14,14,0,0,1,3.27,6.55,9.75,9.75,0,0,1,1.1,4.37,9.62,9.62,0,0,1-1.1,4.36q35.46,43.66,51.3,89.5,3.25,9.82,5.45,19.64a288.59,288.59,0,0,1,10.37,68.75v8.19a296,296,0,0,1-9.81,76.94q-7.12,27.3-24,77.5L418,831.65Q383,872,344.88,899.31a243.27,243.27,0,0,1-90.59,38.19,179.84,179.84,0,0,1-31.64,2.75q-38.78,0-81.87-15.84A293.78,293.78,0,0,1,78,886.22a312.61,312.61,0,0,1-51.85-48,300.52,300.52,0,0,0-18-46.94,60.18,60.18,0,0,1-4.92-13.64,82.36,82.36,0,0,1-2.19-19.11,104.89,104.89,0,0,1,.56-10.91,176.12,176.12,0,0,1-1.64-24,199.79,199.79,0,0,1,2.72-32.74Q5.45,663,5.45,645a103.71,103.71,0,0,0-.54-10.92,242.44,242.44,0,0,1,50.74-67.66,646.83,646.83,0,0,0,57.86-61.12q11.44-10.89,25.09-13.1A88.3,88.3,0,0,1,163.71,489q14.17-1.11,29.46-1.1a108.11,108.11,0,0,0,28.38-7.63q17.44,8.75,27.29,12a124.47,124.47,0,0,1,28.38,13.1q8.71,4.38,23.46,17.46,9.29,9.86,17.47,28.38,7.07,12,9.27,21.83a35.16,35.16,0,0,1,1.64,9.83V585a80.23,80.23,0,0,1-8.73,27.28q-8.2,14.19-18,22.93a166.18,166.18,0,0,1-19.65,19.64q-13.1,8.74-20.72,13.1l-7.65-4.37v-1.64q0-12,6.55-18-8.17-6.55-10.36-10.92l-2.18-8.73c0-2.18-.74-5.81-2.19-10.91v-3.29a38,38,0,0,0-3.82-7.63,196.53,196.53,0,0,0-33.84-40.39Q185.53,542.43,162.61,537a163.71,163.71,0,0,0-50.75,9.81q-25.08,8.76-32.2,36Q67.12,615.56,67.13,654.3a256,256,0,0,0,3.26,39.83,176.75,176.75,0,0,0,5.47,28.38Q88.37,770,122.78,812a452.22,452.22,0,0,0,103.13,58.94,153.57,153.57,0,0,0,107,5.45q25.63-12,37.66-27.28,13.62-14.21,23.46-34.93,10.36-18.57,20.2-39.29Q426.72,753.05,437.1,740q3.27-44.76,5.47-61.12a228.17,228.17,0,0,0,3.26-38.21,213.15,213.15,0,0,0-1.64-26.19,245.3,245.3,0,0,0-8.17-48q-2.2-8.17-4.93-16.36-9.27-30.55-34.92-61.12a70,70,0,0,0-2.18-18,29.12,29.12,0,0,0-4.37-10.37,175.28,175.28,0,0,0-17.46-29.48l-18.55-27.27q-12-16.38-16.38-28.38a282.35,282.35,0,0,1-27.81-28.37q-20.22-26.2-24-31.66Q269,295.76,260.29,286q-10.92-12-31.1-25.11-36.56-31.65-79.12-70.94-45.31-39.28-88.41-66.58-14.74-8.17-17.46-16.9a16.93,16.93,0,0,0-.54-3.83V99.87q0-8.73,6.54-19.11A102.47,102.47,0,0,1,63.3,61.12q9.27-9.82,12.56-18.56a223.6,223.6,0,0,1,38.73-3.27,271,271,0,0,1,40.93,3.27A367.15,367.15,0,0,0,215,47.48c6.91,0,13.64-.17,20.2-.56a45,45,0,0,0,21.27,5.47q17.44,0,25.65-1.1h22.93a77.75,77.75,0,0,1,24,7.65,114,114,0,0,1,27.82-3.29H364q27.25,2.2,39.29,2.19,16.34,0,36.55-5.45,19.1-6.55,27.83-22.93h2.72A20.48,20.48,0,0,0,484.58,24c2.17-4.71,6.17-7.09,12-7.09a26.6,26.6,0,0,1,4.92.54v-.54c0-1.08.72-3.46,2.19-7.11a36.74,36.74,0,0,1,6-6.54C512.57,1.1,515.12,0,517.32,0,521,0,524.41,1.82,527.7,5.45Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
52
client/icons/book-back-cover.svg
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 541.53217 512"
|
||||||
|
version="1.1"
|
||||||
|
id="back-cover-icon"
|
||||||
|
sodipodi:docname="book-front-cover.svg"
|
||||||
|
width="541.53217"
|
||||||
|
height="512"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-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">
|
||||||
|
<defs
|
||||||
|
id="defs22131" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview22129"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.39257813"
|
||||||
|
inkscape:cx="-263.64179"
|
||||||
|
inkscape:cy="444.49751"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg22127" />
|
||||||
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<g id="g20308" transform="matrix(3.7795276,0,0,3.7795276,-201.76367,-251.58203)">
|
||||||
|
<path id="rect20232" d="M95.1,66.6h-8.5c-4.7,0-8.5,3.8-8.5,8.5v21.4c3.5-0.4,7.4-0.5,12-0.5c0.7,0,0.6,0,1.2,0
|
||||||
|
c0-2.4,0-4.2,0.3-6.2c0.3-2.2,2.2-5.8,3.5-7c0.9-0.9,3-3.2,7-3.7c1-0.1,2-0.1,2.8,0c2.6,0.3,4.6,1.6,6.1,2.6
|
||||||
|
c3.9,2.7,7.4,6.4,14.8,13.8c6.3,6.3,9.8,9.8,12,12.4c1.1,1.3,2.1,2.4,2.9,4c0.9,1.7,1.4,4.2,1.4,5.6c0,1.4-0.5,4-1.4,5.6
|
||||||
|
c-0.9,1.6-1.8,2.7-2.9,4c-2.2,2.6-5.6,6-11.8,12.2c-3.8,3.8-7.4,7.3-10.2,9.9c-1.4,1.3-2.6,2.4-3.6,3.3c-0.5,0.4-1,0.8-1.5,1.2
|
||||||
|
c-0.3,0.2-0.5,0.4-1,0.7s-0.7,0.7-2.8,1.2c-4.3,1.1-6.3,0.4-9.4-1.3c-0.5-0.3-1.9-0.9-3.3-2.6c-1.4-1.7-2.1-3.7-2.4-5.1
|
||||||
|
c-0.5-2.4-0.5-4.3-0.6-7.2c-3.9,0-6,0.1-6.5,0.1c-0.5,0.1,0.2-0.2-1.2,0.5c-1.7,0.8-3.6,2.8-4.4,4.5c-0.3,0.8-0.5,1-0.6,6.6
|
||||||
|
c-0.1,2.2-0.2,4.3-0.4,6c0,0.3-0.1,0.6-0.1,0.8v1.9c0,4.7,3.8,8.5,8.5,8.5v16.9c-4.7,0-8.5,3.8-8.5,8.5c0,4.7,3.8,8.5,8.5,8.5h8.5
|
||||||
|
h76.2c14,0,25.4-11.4,25.4-25.4V92c0-14-11.4-25.4-25.4-25.4L95.1,66.6z M171.3,168.2c4.7,0,8.5,3.8,8.5,8.5c0,4.7-3.8,8.5-8.5,8.5
|
||||||
|
h-67.7v-16.9L171.3,168.2L171.3,168.2z"/>
|
||||||
|
<path id="path20297" d="M63.4,158c1.8,1.6,4.5,1.9,5.5,0.7c0.3-0.4,0.7-4,0.8-8.1c0.2-5.9,0.5-7.9,1.4-10c1.7-3.7,4.9-7,8.6-8.9
|
||||||
|
c3.1-1.5,3.6-1.6,11.7-1.6h8.5l0.3,7.6c0.3,7.5,0.3,7.7,1.7,8.5c0.8,0.5,2.1,0.7,2.8,0.5c0.8-0.2,7.4-6.4,14.9-13.9
|
||||||
|
c12.4-12.4,13.5-13.7,13.5-15.5c0-1.8-1.1-3.1-13.8-15.7c-14.7-14.7-15.4-15.2-18-12.7c-1,1-1.1,1.9-1.1,7.6c0,3.6-0.2,6.9-0.3,7.4
|
||||||
|
c-0.3,0.8-1.7,0.9-9.8,0.9c-15.6,0-21.1,1.7-27.9,8.5c-6.5,6.5-8.8,12-8.8,21.1c0,4.7,0.3,6.8,1.3,9.8
|
||||||
|
C56.2,148.6,60.7,155.7,63.4,158L63.4,158z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
48
client/icons/book-front-cover.svg
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 541.53217 512"
|
||||||
|
version="1.1"
|
||||||
|
id="front-cover-icon"
|
||||||
|
sodipodi:docname="book-front-cover.svg"
|
||||||
|
width="541.53217"
|
||||||
|
height="512"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-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">
|
||||||
|
<defs
|
||||||
|
id="defs22131" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview22129"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.39257813"
|
||||||
|
inkscape:cx="-263.64179"
|
||||||
|
inkscape:cy="444.49751"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg22127" />
|
||||||
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<g
|
||||||
|
id="g20308"
|
||||||
|
transform="matrix(3.7795276,0,0,3.7795276,-201.76367,-251.58203)">
|
||||||
|
<path
|
||||||
|
id="rect20232"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:17.9;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill;stop-color:#000000"
|
||||||
|
d="m 78.783305,66.564412 c -14.022889,0 -25.4,11.377111 -25.4,25.4 v 84.666668 c 0,14.02289 11.377111,25.4 25.4,25.4 h 76.199995 8.46667 c 4.68312,0 8.46667,-3.78355 8.46667,-8.46667 0,-4.68311 -3.78355,-8.46666 -8.46667,-8.46666 v -16.93334 c 4.68312,0 8.46667,-3.78355 8.46667,-8.46666 v -1.9327 c -0.0322,-0.27545 -0.0652,-0.54693 -0.0946,-0.83923 -0.17511,-1.74441 -0.30542,-3.81626 -0.37672,-6.02909 -0.18285,-5.67612 -0.29322,-5.86808 -0.63459,-6.62698 -0.74838,-1.66366 -2.65792,-3.64941 -4.38681,-4.49844 -1.41973,-0.69716 -0.72585,-0.45434 -1.20923,-0.51934 -0.47548,-0.0639 -2.54581,-0.13856 -6.47454,-0.14056 -0.0907,2.9929 -0.0862,4.81682 -0.58601,7.244 -0.28023,1.36071 -0.97957,3.42078 -2.40812,5.10356 -1.42519,1.67884 -2.81498,2.35811 -3.28145,2.61896 -3.14428,1.76375 -5.09549,2.43427 -9.41597,1.33997 -2.05224,-0.5197 -2.32631,-0.92288 -2.76159,-1.19527 -0.43528,-0.27239 -0.71007,-0.47684 -0.97461,-0.67593 -0.52909,-0.39816 -0.97871,-0.77171 -1.48622,-1.20664 -1.015,-0.86987 -2.20927,-1.95397 -3.6096,-3.26182 -2.80065,-2.61568 -6.38094,-6.09226 -10.18335,-9.90844 -6.19117,-6.21357 -9.5466,-9.59164 -11.7874,-12.16412 -1.1204,-1.28623 -2.03413,-2.38181 -2.90576,-4.03127 -0.87162,-1.64948 -1.40664,-4.21493 -1.40664,-5.61103 0,-1.4012 0.54783,-3.99366 1.42989,-5.64668 0.88206,-1.65304 1.8039,-2.74855 2.94142,-4.04679 2.27504,-2.59646 5.70131,-6.03358 12.03699,-12.369267 7.37691,-7.376888 10.87768,-11.090687 14.75208,-13.810527 1.45289,-1.019939 3.46378,-2.249133 6.08386,-2.580204 0.87337,-0.110323 1.8133,-0.120299 2.82412,0.0098 4.0433,0.520471 6.12413,2.832857 7.01973,3.728454 1.29782,1.297845 3.1373,4.826955 3.46852,7.049182 0.29817,2.00025 0.26393,3.770666 0.25993,6.212541 0.57954,0.0034 0.50388,0.0217 1.17564,0.0217 4.54211,0 8.44363,0.111537 11.991,0.50953 v -21.41004 c 0,-4.683115 -3.78355,-8.466667 -8.46667,-8.466667 h -8.46667 z m 0,101.599998 h 67.733335 v 16.93334 H 78.783305 c -4.683115,0 -8.466667,-3.78357 -8.466667,-8.46667 0,-4.68313 3.783552,-8.46667 8.466667,-8.46667 z" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#000000;stroke-width:17.9;stroke-linejoin:round;-inkscape-stroke:none;paint-order:stroke markers fill"
|
||||||
|
d="m 186.69094,157.95633 c 2.67243,-2.24871 7.17957,-9.39389 8.63888,-13.69528 1.03796,-3.05942 1.31928,-5.13546 1.33362,-9.84167 0.0278,-9.1246 -2.25302,-14.5915 -8.79325,-21.07662 -6.8535,-6.79576 -12.35348,-8.46107 -27.94423,-8.46107 -8.05417,0 -9.45684,-0.12924 -9.75203,-0.89852 -0.18964,-0.49417 -0.34479,-3.81715 -0.34479,-7.384389 0,-5.728497 -0.13266,-6.618534 -1.13607,-7.621956 -2.57777,-2.57775 -3.29907,-2.07141 -18.02212,12.651595 -12.64444,12.64444 -13.78771,13.94921 -13.78771,15.73575 0,1.78396 1.13629,3.08846 13.49078,15.48766 7.47518,7.50224 14.10644,13.69554 14.8715,13.88928 0.78576,0.19902 2.0096,-0.002 2.84016,-0.46789 1.42969,-0.80092 1.46523,-0.97351 1.74346,-8.46583 l 0.28402,-7.64825 h 8.52049 c 8.16738,0 8.65373,0.0655 11.73586,1.579 3.72428,1.82893 6.9202,5.12058 8.60236,8.86006 0.94352,2.09748 1.22898,4.1112 1.41901,10.01012 0.13083,4.06143 0.49647,7.70394 0.81253,8.09446 0.94895,1.17251 3.64241,0.80611 5.48753,-0.74645 z"
|
||||||
|
id="path20297" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
53
client/icons/book-inside-cover.svg
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 704.00001 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg22127"
|
||||||
|
sodipodi:docname="book-inside-cover.svg"
|
||||||
|
width="704"
|
||||||
|
height="512"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
inkscape:export-filename="InsideCover3.png"
|
||||||
|
inkscape:export-xdpi="300"
|
||||||
|
inkscape:export-ydpi="300"
|
||||||
|
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">
|
||||||
|
<defs
|
||||||
|
id="defs22131" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview22129"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.47274833"
|
||||||
|
inkscape:cx="83.55397"
|
||||||
|
inkscape:cy="178.74204"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg22127" />
|
||||||
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
id="path2161-6"
|
||||||
|
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
|
||||||
|
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
|
||||||
|
<path
|
||||||
|
style="color:#000000;fill:#000000;fill-opacity:1;stroke-width:1;-inkscape-stroke:none"
|
||||||
|
d="m 206.76992,184 c -36.98368,0 -73.07301,9.2343 -94.76923,16.9066 v 185.1887 c 27.62799,-7.7405 62.70503,-15.0804 94.76923,-15.0804 28.33376,0 58.16312,7.6425 81.23077,14.806 V 203.0154 C 273.60322,195.1776 243.44241,184 206.76992,184 Z"
|
||||||
|
id="path4372-8"
|
||||||
|
sodipodi:nodetypes="sccsccs" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:63.9999;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 255.99995,122.53007 c -31.8285,-15.342 -80.43462,-15.4137 -112,0"
|
||||||
|
id="path2371-6"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
54
client/icons/book-part-cover.svg
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 704.00001 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg22127"
|
||||||
|
sodipodi:docname="book-part-cover.svg"
|
||||||
|
width="704"
|
||||||
|
height="512"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
inkscape:export-filename="InsideCover3.png"
|
||||||
|
inkscape:export-xdpi="300"
|
||||||
|
inkscape:export-ydpi="300"
|
||||||
|
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">
|
||||||
|
<defs
|
||||||
|
id="defs22131" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview22129"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#111111"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="0"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="1"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="0.6685671"
|
||||||
|
inkscape:cx="299.8951"
|
||||||
|
inkscape:cy="80.021886"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg22127" />
|
||||||
|
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
|
||||||
|
<path
|
||||||
|
id="path2161-6"
|
||||||
|
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
|
||||||
|
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
|
||||||
|
<path
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 417.64553,213.53304 c 88.71546,-18.9285 95.50522,-18.6158 172.79707,0.054"
|
||||||
|
id="path2371-8"
|
||||||
|
sodipodi:nodetypes="cc" />
|
||||||
|
<path
|
||||||
|
id="path2315"
|
||||||
|
style="stroke-width:67.6532;stroke-linejoin:bevel;paint-order:stroke markers fill;stop-color:#000000"
|
||||||
|
inkscape:transform-center-x="-3.4164388e-06"
|
||||||
|
inkscape:transform-center-y="-8.443352"
|
||||||
|
d="m 505.27489,52.89544 25.98603,52.6535 58.10652,8.4434 -42.04628,40.985 9.92578,57.8717 -51.97205,-27.3234 -51.97204,27.3234 9.92578,-57.8717 -42.04627,-40.985 58.10651,-8.4434 z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
57
client/icons/customIcons.less
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
.fac {
|
||||||
|
display : inline-block;
|
||||||
|
}
|
||||||
|
.position-top-left {
|
||||||
|
content: url('../icons/position-top-left.svg');
|
||||||
|
}
|
||||||
|
.position-top-right {
|
||||||
|
content: url('../icons/position-top-right.svg');
|
||||||
|
}
|
||||||
|
.position-bottom-left {
|
||||||
|
content: url('../icons/position-bottom-left.svg');
|
||||||
|
}
|
||||||
|
.position-bottom-right {
|
||||||
|
content: url('../icons/position-bottom-right.svg');
|
||||||
|
}
|
||||||
|
.position-top {
|
||||||
|
content: url('../icons/position-top.svg');
|
||||||
|
}
|
||||||
|
.position-right {
|
||||||
|
content: url('../icons/position-right.svg');
|
||||||
|
}
|
||||||
|
.position-bottom {
|
||||||
|
content: url('../icons/position-bottom.svg');
|
||||||
|
}
|
||||||
|
.position-left {
|
||||||
|
content: url('../icons/position-left.svg');
|
||||||
|
}
|
||||||
|
.mask-edge {
|
||||||
|
content: url('../icons/mask-edge.svg');
|
||||||
|
}
|
||||||
|
.mask-corner {
|
||||||
|
content: url('../icons/mask-corner.svg');
|
||||||
|
}
|
||||||
|
.mask-center {
|
||||||
|
content: url('../icons/mask-center.svg');
|
||||||
|
}
|
||||||
|
.book-front-cover {
|
||||||
|
content: url('../icons/book-front-cover.svg');
|
||||||
|
}
|
||||||
|
.book-back-cover {
|
||||||
|
content: url('../icons/book-back-cover.svg');
|
||||||
|
}
|
||||||
|
.book-inside-cover {
|
||||||
|
content: url('../icons/book-inside-cover.svg');
|
||||||
|
}
|
||||||
|
.book-part-cover {
|
||||||
|
content: url('../icons/book-part-cover.svg');
|
||||||
|
}
|
||||||
|
.davek {
|
||||||
|
content: url('../icons/Davek.svg');
|
||||||
|
}
|
||||||
|
.rellanic {
|
||||||
|
content: url('../icons/Rellanic.svg');
|
||||||
|
}
|
||||||
|
.iokharic {
|
||||||
|
content: url('../icons/Iokharic.svg');
|
||||||
|
}
|
||||||
63
client/icons/mask-center.svg
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="mask-center.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs139"><pattern
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#Strips1_1"
|
||||||
|
id="pattern3077"
|
||||||
|
patternTransform="matrix(23.13193,-23.131931,19.25517,19.25517,18.091544,-20.306833)" /><pattern
|
||||||
|
inkscape:collect="always"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
width="2"
|
||||||
|
height="1"
|
||||||
|
patternTransform="translate(0,0) scale(10,10)"
|
||||||
|
id="Strips1_1"
|
||||||
|
inkscape:stockid="Stripes 1:1"><rect
|
||||||
|
style="fill:black;stroke:none"
|
||||||
|
x="0"
|
||||||
|
y="-0.5"
|
||||||
|
width="1"
|
||||||
|
height="2"
|
||||||
|
id="rect2097" /></pattern></defs><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.67711183"
|
||||||
|
inkscape:cx="31.75251"
|
||||||
|
inkscape:cy="260.66595"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="M 48,-5.2e-6 C 21.40803,-5.2e-6 1.98e-5,21.408025 1.98e-5,47.999995 V 464 C 1.98e-5,490.59197 21.40803,512 48,512 h 352 c 26.59198,0 48,-21.40803 48,-48 V 47.999995 C 448,21.408025 426.59198,-5.2e-6 400,-5.2e-6 Z M 64,63.999995 H 384 V 448 H 64 Z" /><rect
|
||||||
|
style="fill:url(#pattern3077);fill-opacity:1;stroke:#000000;stroke-width:48;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206"
|
||||||
|
width="176"
|
||||||
|
height="240"
|
||||||
|
x="136.00002"
|
||||||
|
y="136"
|
||||||
|
rx="48"
|
||||||
|
ry="48" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
63
client/icons/mask-corner.svg
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="mask-corner.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs139"><pattern
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#Strips1_1"
|
||||||
|
id="pattern3077"
|
||||||
|
patternTransform="matrix(23.131931,-23.131931,19.25517,19.25517,26.214281,-26.952711)" /><pattern
|
||||||
|
inkscape:collect="always"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
width="2"
|
||||||
|
height="1"
|
||||||
|
patternTransform="translate(0,0) scale(10,10)"
|
||||||
|
id="Strips1_1"
|
||||||
|
inkscape:stockid="Stripes 1:1"><rect
|
||||||
|
style="fill:black;stroke:none"
|
||||||
|
x="0"
|
||||||
|
y="-0.5"
|
||||||
|
width="1"
|
||||||
|
height="2"
|
||||||
|
id="rect2097" /></pattern></defs><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.95758074"
|
||||||
|
inkscape:cx="275.17262"
|
||||||
|
inkscape:cy="306.50157"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="M 48,-5.2e-6 C 21.40803,-5.2e-6 1.98e-5,21.408025 1.98e-5,47.999995 V 464 C 1.98e-5,490.59197 21.40803,512 48,512 h 352 c 26.59198,0 48,-21.40803 48,-48 V 47.999995 C 448,21.408025 426.59198,-5.2e-6 400,-5.2e-6 Z M 64,63.999995 H 384 V 448 H 64 Z" /><rect
|
||||||
|
style="fill:url(#pattern3077);fill-opacity:1;stroke:#000000;stroke-width:48;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206"
|
||||||
|
width="208"
|
||||||
|
height="240"
|
||||||
|
x="32.000011"
|
||||||
|
y="32.000011"
|
||||||
|
rx="48"
|
||||||
|
ry="48" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
69
client/icons/mask-edge.svg
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="mask-edge.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs139"><pattern
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#Strips1_1"
|
||||||
|
id="pattern3077"
|
||||||
|
patternTransform="matrix(23.131931,-23.13193,19.25517,19.25517,26.214281,-26.952711)" /><pattern
|
||||||
|
inkscape:collect="always"
|
||||||
|
patternUnits="userSpaceOnUse"
|
||||||
|
width="2"
|
||||||
|
height="1"
|
||||||
|
patternTransform="translate(0,0) scale(10,10)"
|
||||||
|
id="Strips1_1"
|
||||||
|
inkscape:stockid="Stripes 1:1"><rect
|
||||||
|
style="fill:black;stroke:none"
|
||||||
|
x="0"
|
||||||
|
y="-0.5"
|
||||||
|
width="1"
|
||||||
|
height="2"
|
||||||
|
id="rect2097" /></pattern></defs><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.95758074"
|
||||||
|
inkscape:cx="231.31209"
|
||||||
|
inkscape:cy="171.78708"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="M 48,-5.2e-6 C 21.40803,-5.2e-6 1.98e-5,21.408025 1.98e-5,47.999995 V 464 C 1.98e-5,490.59197 21.40803,512 48,512 h 352 c 26.59198,0 48,-21.40803 48,-48 V 47.999995 C 448,21.408025 426.59198,-5.2e-6 400,-5.2e-6 Z M 64,63.999995 H 384 V 448 H 64 Z" /><rect
|
||||||
|
style="fill:url(#pattern3077);fill-opacity:1;stroke:#000000;stroke-width:48;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206"
|
||||||
|
width="208"
|
||||||
|
height="447.99997"
|
||||||
|
x="32.000011"
|
||||||
|
y="32.000011"
|
||||||
|
rx="48"
|
||||||
|
ry="48" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke-width:47.9999;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect4640"
|
||||||
|
width="48"
|
||||||
|
height="512"
|
||||||
|
x="216"
|
||||||
|
y="0" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
46
client/icons/position-bottom-left.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-bottom-left.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.70792086"
|
||||||
|
inkscape:cx="174.45453"
|
||||||
|
inkscape:cy="325.60137"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="m 48,511.99998 c -26.59197,0 -48.00000035682677,-21.40803 -48.00000035682677,-48 v -416 C -3.5682677e-7,21.40801 21.40803,-1.9692461e-5 48,-1.9692461e-5 h 352 c 26.59198,0 48,21.408029692461 48,47.999999692461 v 416 c 0,26.59197 -21.40802,48 -48,48 z m 16,-64 h 320 v -384 H 64 Z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206"
|
||||||
|
width="208"
|
||||||
|
height="240"
|
||||||
|
x="-3.5682677e-07"
|
||||||
|
y="-512"
|
||||||
|
rx="48"
|
||||||
|
ry="48"
|
||||||
|
transform="scale(1,-1)" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
46
client/icons/position-bottom-right.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-bottom-right.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.70792086"
|
||||||
|
inkscape:cx="174.45453"
|
||||||
|
inkscape:cy="325.60137"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="m 400,511.99998 c 26.59197,0 48,-21.40803 48,-48 v -416 C 448,21.40801 426.59197,-1.9692461e-5 400,-1.9692461e-5 H 48 C 21.40802,-1.9692461e-5 -3.5682677e-7,21.40801 -3.5682677e-7,47.99998 v 416 c 0,26.59197 21.40802035682677,48 48.00000035682677,48 z m -16,-64 H 64 v -384 h 320 z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206"
|
||||||
|
width="208"
|
||||||
|
height="240"
|
||||||
|
x="-448"
|
||||||
|
y="-512"
|
||||||
|
rx="48"
|
||||||
|
ry="48"
|
||||||
|
transform="scale(-1)" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
46
client/icons/position-bottom.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-bottom.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="1.0011513"
|
||||||
|
inkscape:cx="273.18549"
|
||||||
|
inkscape:cy="216.25103"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201-2"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="m 48,512.00004 c -26.5919,0 -48,-21.4081 -48,-48 V 47.999996 C 0,21.408026 21.4081,-3.8146973e-6 48,-3.8146973e-6 h 352 c 26.592,0 48,21.4080298146973 48,47.9999998146973 V 464.00004 c 0,26.5919 -21.408,48 -48,48 z m 16,-64 H 384 V 63.999996 H 64 Z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.0001;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206-8"
|
||||||
|
width="447.99997"
|
||||||
|
height="240"
|
||||||
|
x="1.40625e-05"
|
||||||
|
y="-512.00006"
|
||||||
|
rx="48"
|
||||||
|
ry="48"
|
||||||
|
transform="scale(1,-1)" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
45
client/icons/position-left.svg
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-left.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.70792086"
|
||||||
|
inkscape:cx="164.56642"
|
||||||
|
inkscape:cy="243.6713"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201-0"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="M 48,0 C 21.4081,0 0,21.40803 0,48 v 416 c 0,26.59197 21.4081,48 48,48 h 352.0001 c 26.5919,0 48,-21.40803 48,-48 V 48 c 0,-26.59197 -21.4081,-48 -48,-48 z M 64,64 H 384.0001 V 448 H 64 Z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206-2"
|
||||||
|
width="208"
|
||||||
|
height="512.00006"
|
||||||
|
x="7.0762391e-05"
|
||||||
|
y="-8.8710935e-05"
|
||||||
|
rx="48"
|
||||||
|
ry="48.000004" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
46
client/icons/position-right.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-right.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.70792086"
|
||||||
|
inkscape:cx="164.56642"
|
||||||
|
inkscape:cy="243.6713"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201-0"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="m 400.0001,0 c 26.5919,0 48,21.40803 48,48 v 416 c 0,26.59197 -21.4081,48 -48,48 H 48 C 21.4081,512 0,490.59197 0,464 V 48 C 0,21.40803 21.4081,0 48,0 Z m -16,64 H 64 v 384 h 320.0001 z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206-2"
|
||||||
|
width="208"
|
||||||
|
height="512.00006"
|
||||||
|
x="-448.00003"
|
||||||
|
y="-8.8710935e-05"
|
||||||
|
rx="48"
|
||||||
|
ry="48.000004"
|
||||||
|
transform="scale(-1,1)" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
45
client/icons/position-top-left.svg
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-top-left.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.70792086"
|
||||||
|
inkscape:cx="174.45453"
|
||||||
|
inkscape:cy="325.60137"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="M 48,0 C 21.40803,0 0,21.40803 0,48 v 416 c 0,26.59197 21.40803,48 48,48 h 352 c 26.59198,0 48,-21.40803 48,-48 V 48 C 448,21.40803 426.59198,0 400,0 Z M 64,64 H 384 V 448 H 64 Z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206"
|
||||||
|
width="208"
|
||||||
|
height="240"
|
||||||
|
x="-3.5682677e-07"
|
||||||
|
y="-1.9692461e-05"
|
||||||
|
rx="48"
|
||||||
|
ry="48" /></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
46
client/icons/position-top-right.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-top-right.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="0.70792086"
|
||||||
|
inkscape:cx="174.45453"
|
||||||
|
inkscape:cy="325.60137"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="m 400,0 c 26.59197,0 48,21.40803 48,48 v 416 c 0,26.59197 -21.40803,48 -48,48 H 48 C 21.40802,512 -3.5682677e-7,490.59197 -3.5682677e-7,464 V 48 C -3.5682677e-7,21.40803 21.40802,0 48,0 Z M 384,64 H 64 v 384 h 320 z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206"
|
||||||
|
width="208"
|
||||||
|
height="240"
|
||||||
|
x="-448"
|
||||||
|
y="-1.9692461e-05"
|
||||||
|
rx="48"
|
||||||
|
ry="48"
|
||||||
|
transform="scale(-1,1)" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
45
client/icons/position-top.svg
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 448 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg135"
|
||||||
|
sodipodi:docname="position-top.svg"
|
||||||
|
width="448"
|
||||||
|
height="512"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
|
||||||
|
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"><defs
|
||||||
|
id="defs139" /><sodipodi:namedview
|
||||||
|
id="namedview137"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
showgrid="false"
|
||||||
|
showguides="false"
|
||||||
|
inkscape:zoom="1.0011513"
|
||||||
|
inkscape:cx="273.18549"
|
||||||
|
inkscape:cy="216.25103"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="991"
|
||||||
|
inkscape:window-x="-9"
|
||||||
|
inkscape:window-y="-9"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
|
||||||
|
id="rect12201-2"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
d="M 48,0 C 21.4081,0 0,21.4081 0,48 v 416.00004 c 0,26.59197 21.4081,48 48,48 h 352 c 26.592,0 48,-21.40803 48,-48 V 48 C 448,21.4081 426.592,0 400,0 Z M 64,64 H 384 V 448.00004 H 64 Z" /><rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.0001;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||||
|
id="rect12206-8"
|
||||||
|
width="447.99997"
|
||||||
|
height="240"
|
||||||
|
x="1.40625e-05"
|
||||||
|
y="-3.8146973e-06"
|
||||||
|
rx="48"
|
||||||
|
ry="48" /></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1,28 +1,31 @@
|
|||||||
module.exports = async(name, title = '', props = {})=>{
|
const template = async function(name, title='', props = {}){
|
||||||
const HOMEBREWERY_PUBLIC_URL=props.config.publicUrl;
|
const ogTags = [];
|
||||||
|
const ogMeta = props.ogMeta ?? {};
|
||||||
|
Object.entries(ogMeta).forEach(([key, value])=>{
|
||||||
|
if(!value) return;
|
||||||
|
const tag = `<meta property="og:${key}" content="${value}">`;
|
||||||
|
ogTags.push(tag);
|
||||||
|
});
|
||||||
|
const ogMetaTags = ogTags.join('\n');
|
||||||
|
|
||||||
return `
|
return `<!DOCTYPE html>
|
||||||
<!DOCTYPE html>
|
<html>
|
||||||
<html>
|
<head>
|
||||||
<head>
|
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||||
<link href=${`/${name}/bundle.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" />
|
${ogMetaTags}
|
||||||
<meta property="og:title" content="${props.brew?.title || 'Homebrewery - Untitled Brew'}">
|
<meta name="twitter:card" content="summary">
|
||||||
<meta property="og:url" content="${HOMEBREWERY_PUBLIC_URL}/${props.brew?.shareId ? `share/${props.brew.shareId}` : ''}">
|
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||||
<meta property="og:image" content="${props.brew?.thumbnail || `${HOMEBREWERY_PUBLIC_URL}/thumbnail.png`}">
|
</head>
|
||||||
<meta property="og:description" content="${props.brew?.description || 'No description.'}">
|
<body>
|
||||||
<meta property="og:site_name" content="The Homebrewery - Make your Homebrew content look legit!">
|
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||||
<meta property="og:type" content="article">
|
<script src=${`/${name}/bundle.js`}></script>
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<script>start_app(${JSON.stringify(props)})</script>
|
||||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
</body>
|
||||||
</head>
|
</html>
|
||||||
<body>
|
`;
|
||||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
|
||||||
<script src=${`/${name}/bundle.js`}></script>
|
|
||||||
<script>start_app(${JSON.stringify(props)})</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = template;
|
||||||
43
install/README.WINDOWS.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Windows Installation Instructions
|
||||||
|
|
||||||
|
## Before Installing
|
||||||
|
|
||||||
|
These instructions assume that you are installing to a completely new, fresh Windows 10 installation. As such, some steps may not be necessary if you are installing to an existing Windows 10 instance.
|
||||||
|
|
||||||
|
## Installation instructions
|
||||||
|
|
||||||
|
1. Download the installation script from https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/windows/install.ps1.
|
||||||
|
|
||||||
|
2. Run Powershell as an Administrator.
|
||||||
|
a. Click the Start menu or press the Windows key.
|
||||||
|
b. Type `powershell` into the Search box.
|
||||||
|
c. Right click on the Powershell app and select "Run As Administrator".
|
||||||
|
d. Click YES in the prompt that appears.
|
||||||
|
|
||||||
|
3. Change the script execution policy.
|
||||||
|
a. Run the Powershell command `Set-ExecutionPolicy Bypass -Scope Process`.
|
||||||
|
b. Allow the change to be made - press Y at the prompt that appears.
|
||||||
|
|
||||||
|
4. Run the installation script.
|
||||||
|
a. Navigate to the location of the script, e.g. `cd C:\Users\ExampleUser\Downloads`.
|
||||||
|
b. Start the script - `.\install.ps1`
|
||||||
|
|
||||||
|
5. Once the script has completed, it will start the Homebrewery server. This will normally cause a Network Access prompt for NodeJS - if this appears, click "Allow".
|
||||||
|
|
||||||
|
**NOTE:** At this time, the script **ONLY** installs HomeBrewery. It does **NOT** install the NaturalCrit login system, as that is currently a completely separate project.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
These installation instructions have been tested on the following Ubuntu releases:
|
||||||
|
|
||||||
|
- *Windows 10 Home - OS Build 19045.2546*
|
||||||
|
|
||||||
|
## Final Notes
|
||||||
|
|
||||||
|
While this installation process works successfully at the time of writing (January 23, 2023), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
||||||
|
|
||||||
|
Regards,
|
||||||
|
G
|
||||||
|
January 23, 2023
|
||||||
51
install/windows/install.ps1
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
Write-Host Homebrewery Install -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
Write-Host =================== -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
Write-Host Install Chocolatey -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
Write-Host Instructions from https://chocolate.org/install -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||||
|
|
||||||
|
Write-Host Install Node JS v16.11.1 -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
choco install nodejs --version=16.11.1 -y
|
||||||
|
|
||||||
|
Write-Host Install MongoDB v 4.4.4 -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
choco install mongodb --version=4.4.4 -y
|
||||||
|
|
||||||
|
Write-Host Install GIT -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
choco install git -y
|
||||||
|
|
||||||
|
Write-Host Refresh Environment -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
|
||||||
|
Update-SessionEnvironment
|
||||||
|
|
||||||
|
Write-Host Create Homebrewery directory - C:\Homebrewery -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
mkdir C:\Hombrewery
|
||||||
|
cd C:\Hombrewery
|
||||||
|
|
||||||
|
Write-Host Download Homebrewery project files -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
git clone https://github.com/naturalcrit/homebrewery.git
|
||||||
|
|
||||||
|
Write-Host Install Homebrewery files -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
cd homebrewery
|
||||||
|
|
||||||
|
npm install
|
||||||
|
npm audit fix
|
||||||
|
|
||||||
|
Write-Host Set install type to 'local' -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
[System.Environment]::SetEnvironmentVariable('NODE_ENV', 'local')
|
||||||
|
|
||||||
|
Write-Host INSTALL COMPLETE -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
Write-Host To start Homebrewery in the future, open a terminal in the Homebrewery directory and run npm start -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
Write-Host ================================================================================================== -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
Write-Host Start Homebrewery -BackgroundColor Black -ForegroundColor Yellow
|
||||||
|
|
||||||
|
npm start
|
||||||
24195
package-lock.json
generated
93
package.json
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.3.0",
|
"version": "3.9.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.11.x"
|
"node": ">=18.16.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -12,20 +12,27 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/dev.js",
|
"dev": "node scripts/dev.js",
|
||||||
"quick": "node scripts/quick.js",
|
"quick": "node scripts/quick.js",
|
||||||
"build": "node scripts/buildHomebrew.js",
|
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||||
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||||
"lint": "eslint --fix **/*.{js,jsx}",
|
"lint": "eslint --fix **/*.{js,jsx}",
|
||||||
"lint:dry": "eslint **/*.{js,jsx}",
|
"lint:dry": "eslint **/*.{js,jsx}",
|
||||||
|
"stylelint": "stylelint --fix **/*.{less}",
|
||||||
|
"stylelint:dry": "stylelint **/*.less",
|
||||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "jest",
|
"test": "jest --runInBand",
|
||||||
|
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||||
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
"test:mustache-syntax": "jest '.*(mustache-syntax).*' --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:injection": "jest '.*(mustache-syntax).*' -t '^Injection:.*' --verbose --noStackTrace",
|
||||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"phb": "node scripts/phb.js",
|
"phb": "node scripts/phb.js",
|
||||||
"prod": "set NODE_ENV=production && npm run build",
|
"prod": "set NODE_ENV=production && npm run build",
|
||||||
"postinstall": "npm run buildall",
|
"postinstall": "npm run build",
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"author": "stolksdorf",
|
"author": "stolksdorf",
|
||||||
@@ -34,11 +41,31 @@
|
|||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"jest": {
|
"jest": {
|
||||||
"testTimeout": 15000,
|
"testTimeout": 30000,
|
||||||
"modulePaths": [
|
"modulePaths": [
|
||||||
"mode_modules",
|
"node_modules",
|
||||||
"shared",
|
"shared",
|
||||||
"server"
|
"server"
|
||||||
|
],
|
||||||
|
"coveragePathIgnorePatterns": [
|
||||||
|
"build/*"
|
||||||
|
],
|
||||||
|
"coverageThreshold": {
|
||||||
|
"global": {
|
||||||
|
"statements": 25,
|
||||||
|
"branches": 10,
|
||||||
|
"functions": 22,
|
||||||
|
"lines": 25
|
||||||
|
},
|
||||||
|
"server/homebrew.api.js": {
|
||||||
|
"statements": 65,
|
||||||
|
"branches": 50,
|
||||||
|
"functions": 60,
|
||||||
|
"lines": 70
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"jest-expect-message"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
@@ -51,44 +78,54 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.22.8",
|
||||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
"@babel/plugin-transform-runtime": "^7.22.7",
|
||||||
"@babel/preset-env": "^7.19.4",
|
"@babel/preset-env": "^7.22.7",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.22.5",
|
||||||
"body-parser": "^1.20.1",
|
"@googleapis/drive": "^5.1.0",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.1",
|
"dedent-tabs": "^0.10.3",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "10.1.0",
|
"fs-extra": "11.1.1",
|
||||||
"googleapis": "108.0.0",
|
|
||||||
"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": "4.1.1",
|
"marked": "5.1.1",
|
||||||
"marked-extended-tables": "^1.0.5",
|
"marked-extended-tables": "^1.0.6",
|
||||||
|
"marked-gfm-heading-id": "^3.0.4",
|
||||||
|
"marked-smartypants-lite": "^1.0.0",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongoose": "^6.7.0",
|
"mongoose": "^7.3.2",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.0",
|
||||||
"react": "^16.14.0",
|
"npm": "^9.8.0",
|
||||||
"react-dom": "^16.14.0",
|
"react": "^18.2.0",
|
||||||
"react-frame-component": "4.1.3",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "6.4.2",
|
"react-frame-component": "^4.1.3",
|
||||||
|
"react-router-dom": "6.14.1",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.44.0",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-jest": "^27.2.2",
|
||||||
"jest": "^29.2.2",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"supertest": "^6.3.1"
|
"jest": "^29.6.1",
|
||||||
|
"jest-expect-message": "^1.1.3",
|
||||||
|
"postcss-less": "^6.0.0",
|
||||||
|
"stylelint": "^15.10.1",
|
||||||
|
"stylelint-config-recess-order": "^4.3.0",
|
||||||
|
"stylelint-config-recommended": "^13.0.0",
|
||||||
|
"stylelint-stylistic": "^0.4.3",
|
||||||
|
"supertest": "^6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const transforms = {
|
|||||||
|
|
||||||
const build = async ({ bundle, render, ssr })=>{
|
const build = async ({ bundle, render, ssr })=>{
|
||||||
const css = await lessTransform.generate({ paths: './shared' });
|
const css = await lessTransform.generate({ paths: './shared' });
|
||||||
|
//css = `@layer bundle {\n${css}\n}`;
|
||||||
await fs.outputFile('./build/homebrew/bundle.css', css);
|
await fs.outputFile('./build/homebrew/bundle.css', css);
|
||||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||||
@@ -72,6 +73,7 @@ fs.emptyDirSync('./build');
|
|||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.V3[dir] = (themeData);
|
themes.V3[dir] = (themeData);
|
||||||
fs.copy(`./themes/V3/${dir}/dropdownTexture.png`, `./build/themes/V3/${dir}/dropdownTexture.png`);
|
fs.copy(`./themes/V3/${dir}/dropdownTexture.png`, `./build/themes/V3/${dir}/dropdownTexture.png`);
|
||||||
|
fs.copy(`./themes/V3/${dir}/dropdownPreview.png`, `./build/themes/V3/${dir}/dropdownPreview.png`);
|
||||||
const src = `./themes/V3/${dir}/style.less`;
|
const src = `./themes/V3/${dir}/style.less`;
|
||||||
((outputDirectory)=>{
|
((outputDirectory)=>{
|
||||||
less.render(fs.readFileSync(src).toString(), {
|
less.render(fs.readFileSync(src).toString(), {
|
||||||
@@ -95,6 +97,7 @@ fs.emptyDirSync('./build');
|
|||||||
// Move assets
|
// Move assets
|
||||||
await fs.copy('./themes/fonts', './build/fonts');
|
await fs.copy('./themes/fonts', './build/fonts');
|
||||||
await fs.copy('./themes/assets', './build/assets');
|
await fs.copy('./themes/assets', './build/assets');
|
||||||
|
await fs.copy('./client/icons', './build/icons');
|
||||||
|
|
||||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||||
|
|
||||||
@@ -132,10 +135,12 @@ fs.emptyDirSync('./build');
|
|||||||
|
|
||||||
})().catch(console.error);
|
})().catch(console.error);
|
||||||
|
|
||||||
//In development set up a watch server and livereload
|
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
|
||||||
if(isDev){
|
if(isDev){
|
||||||
livereload('./build');
|
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
|
||||||
watchFile('./server.js', {
|
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
|
||||||
watch : ['./client', './server'] // Watch additional folders if you want
|
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
|
||||||
|
ext : 'js json' // Extensions to watch (only .js/.json by default)
|
||||||
|
//watch : ['./server', './themes'], // Watch additional folders if needed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const mw = {
|
|||||||
.status(401)
|
.status(401)
|
||||||
.send('Authorization Required');
|
.send('Authorization Required');
|
||||||
}
|
}
|
||||||
const [username, password] = new Buffer(req.get('authorization').split(' ').pop(), 'base64')
|
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
|
||||||
.toString('ascii')
|
.toString('ascii')
|
||||||
.split(':');
|
.split(':');
|
||||||
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
||||||
|
|||||||
200
server/app.js
@@ -1,4 +1,4 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
|
|
||||||
@@ -15,13 +15,15 @@ const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
|||||||
const sanitizeFilename = require('sanitize-filename');
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
const splitTextStyleAndMetadata = (brew)=>{
|
||||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||||
if(brew.text.startsWith('```metadata')) {
|
if(brew.text.startsWith('```metadata')) {
|
||||||
const index = brew.text.indexOf('```\n\n');
|
const index = brew.text.indexOf('```\n\n');
|
||||||
const metadataSection = brew.text.slice(12, index - 1);
|
const metadataSection = brew.text.slice(12, index - 1);
|
||||||
const metadata = yaml.load(metadataSection);
|
const metadata = yaml.load(metadataSection);
|
||||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']));
|
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']));
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
}
|
}
|
||||||
if(brew.text.startsWith('```css')) {
|
if(brew.text.startsWith('```css')) {
|
||||||
@@ -29,7 +31,6 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
brew.style = brew.text.slice(7, index - 1);
|
brew.style = brew.text.slice(7, index - 1);
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
}
|
}
|
||||||
_.defaults(brew, { 'renderer': 'legacy', 'theme': '5ePHB' });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
@@ -42,8 +43,7 @@ const sanitizeBrew = (brew, accessType)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
|
app.use(require('./middleware/content-negotiation.js'));
|
||||||
//app.use(express.static(`${__dirname}/build`));
|
|
||||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||||
app.use(require('cookie-parser')());
|
app.use(require('cookie-parser')());
|
||||||
app.use(require('./forcessl.mw.js'));
|
app.use(require('./forcessl.mw.js'));
|
||||||
@@ -77,6 +77,14 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
|||||||
|
|
||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
|
const defaultMetaTags = {
|
||||||
|
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
|
||||||
|
title : 'The Homebrewery',
|
||||||
|
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
|
||||||
|
image : `${config.get('publicUrl')}/thumbnail.png`,
|
||||||
|
type : 'website'
|
||||||
|
};
|
||||||
|
|
||||||
//Robots.txt
|
//Robots.txt
|
||||||
app.get('/robots.txt', (req, res)=>{
|
app.get('/robots.txt', (req, res)=>{
|
||||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||||
@@ -87,17 +95,29 @@ app.get('/', (req, res, next)=>{
|
|||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeText,
|
text : welcomeText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Homepage',
|
||||||
|
description : 'Homepage'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Home page v3
|
//Home page Legacy
|
||||||
app.get('/legacy', (req, res, next)=>{
|
app.get('/legacy', (req, res, next)=>{
|
||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeTextLegacy,
|
text : welcomeTextLegacy,
|
||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Homepage (Legacy)',
|
||||||
|
description : 'Homepage'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -107,7 +127,13 @@ app.get('/migrate', (req, res, next)=>{
|
|||||||
req.brew = {
|
req.brew = {
|
||||||
text : migrateText,
|
text : migrateText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'v3 Migration Guide',
|
||||||
|
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -118,7 +144,13 @@ app.get('/changelog', async (req, res, next)=>{
|
|||||||
title : 'Changelog',
|
title : 'Changelog',
|
||||||
text : changelogText,
|
text : changelogText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Changelog',
|
||||||
|
description : 'Development changelog.'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -129,7 +161,13 @@ app.get('/faq', async (req, res, next)=>{
|
|||||||
title : 'FAQ',
|
title : 'FAQ',
|
||||||
text : faqText,
|
text : faqText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'FAQ',
|
||||||
|
description : 'Frequently Asked Questions'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -153,12 +191,19 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
sanitizeBrew(brew, 'share');
|
sanitizeBrew(brew, 'share');
|
||||||
const prefix = 'HB - ';
|
const prefix = 'HB - ';
|
||||||
|
|
||||||
|
const encodeRFC3986ValueChars = (str)=>{
|
||||||
|
return (
|
||||||
|
encodeURIComponent(str)
|
||||||
|
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||||
res.set({
|
res.set({
|
||||||
'Cache-Control' : 'no-cache',
|
'Cache-Control' : 'no-cache',
|
||||||
'Content-Type' : 'text/plain',
|
'Content-Type' : 'text/plain',
|
||||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
|
||||||
});
|
});
|
||||||
res.status(200).send(brew.text);
|
res.status(200).send(brew.text);
|
||||||
});
|
});
|
||||||
@@ -167,12 +212,19 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
app.get('/user/:username', async (req, res, next)=>{
|
app.get('/user/:username', async (req, res, next)=>{
|
||||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : `${req.params.username}'s Collection`,
|
||||||
|
description : 'View my collection of homebrew on the Homebrewery.'
|
||||||
|
// type : could be 'profile'?
|
||||||
|
};
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
'googleId',
|
'googleId',
|
||||||
'title',
|
'title',
|
||||||
'pageCount',
|
'pageCount',
|
||||||
'description',
|
'description',
|
||||||
'authors',
|
'authors',
|
||||||
|
'lang',
|
||||||
'published',
|
'published',
|
||||||
'views',
|
'views',
|
||||||
'shareId',
|
'shareId',
|
||||||
@@ -224,6 +276,14 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : req.brew.title || 'Untitled Brew',
|
||||||
|
description : req.brew.description || 'No description.',
|
||||||
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
|
type : 'article'
|
||||||
|
};
|
||||||
|
|
||||||
sanitizeBrew(req.brew, 'edit');
|
sanitizeBrew(req.brew, 'edit');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||||
@@ -234,7 +294,21 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
|||||||
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||||
sanitizeBrew(req.brew, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew.title = `CLONE - ${req.brew.title}`;
|
const brew = {
|
||||||
|
shareId : req.brew.shareId,
|
||||||
|
title : `CLONE - ${req.brew.title}`,
|
||||||
|
text : req.brew.text,
|
||||||
|
style : req.brew.style,
|
||||||
|
renderer : req.brew.renderer,
|
||||||
|
theme : req.brew.theme
|
||||||
|
};
|
||||||
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'New',
|
||||||
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||||
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,9 +316,16 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (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,
|
||||||
|
title : req.brew.title || 'Untitled Brew',
|
||||||
|
description : req.brew.description || 'No description.',
|
||||||
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
|
type : 'article'
|
||||||
|
};
|
||||||
|
|
||||||
if(req.params.id.length > 12 && !brew._id) {
|
if(req.params.id.length > 12 && !brew._id) {
|
||||||
const googleId = req.params.id.slice(0, -12);
|
const googleId = brew.googleId;
|
||||||
const shareId = req.params.id.slice(-12);
|
const shareId = brew.shareId;
|
||||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||||
.catch((err)=>{next(err);});
|
.catch((err)=>{next(err);});
|
||||||
} else {
|
} else {
|
||||||
@@ -262,6 +343,60 @@ app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Account Page
|
||||||
|
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||||
|
const data = {};
|
||||||
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
|
let auth;
|
||||||
|
let googleCount = [];
|
||||||
|
if(req.account) {
|
||||||
|
if(req.account.googleId) {
|
||||||
|
try {
|
||||||
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
|
} catch (e) {
|
||||||
|
auth = undefined;
|
||||||
|
console.log('Google auth check failed!');
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
if(auth.credentials.access_token) {
|
||||||
|
try {
|
||||||
|
googleCount = await GoogleActions.listGoogleBrews(auth);
|
||||||
|
} catch (e) {
|
||||||
|
googleCount = undefined;
|
||||||
|
console.log('List Google files failed!');
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
|
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||||
|
.catch((err)=>{
|
||||||
|
mongoCount = 0;
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.uiItems = {
|
||||||
|
username : req.account.username,
|
||||||
|
issued : req.account.issued,
|
||||||
|
googleId : Boolean(req.account.googleId),
|
||||||
|
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||||
|
mongoCount : mongoCount,
|
||||||
|
googleCount : googleCount?.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
req.brew = data;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : `Account Page`,
|
||||||
|
description : null
|
||||||
|
};
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
const nodeEnv = config.get('node_env');
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
// Local only
|
// Local only
|
||||||
@@ -276,11 +411,9 @@ if(isLocalEnvironment){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Render the page
|
//Render the page
|
||||||
const templateFn = require('./../client/template.js');
|
const templateFn = require('./../client/template.js');
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
const renderPage = async (req, res)=>{
|
||||||
// Create configuration object
|
// Create configuration object
|
||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
@@ -289,28 +422,34 @@ app.use(asyncHandler(async (req, res, next)=>{
|
|||||||
};
|
};
|
||||||
const props = {
|
const props = {
|
||||||
version : require('./../package.json').version,
|
version : require('./../package.json').version,
|
||||||
url : req.originalUrl,
|
url : req.customUrl || req.originalUrl,
|
||||||
brew : req.brew,
|
brew : req.brew,
|
||||||
brews : req.brews,
|
brews : req.brews,
|
||||||
googleBrews : req.googleBrews,
|
googleBrews : req.googleBrews,
|
||||||
account : req.account,
|
account : req.account,
|
||||||
enable_v3 : config.get('enable_v3'),
|
enable_v3 : config.get('enable_v3'),
|
||||||
enable_themes : config.get('enable_themes'),
|
enable_themes : config.get('enable_themes'),
|
||||||
config : configuration
|
config : configuration,
|
||||||
|
ogMeta : req.ogMeta
|
||||||
};
|
};
|
||||||
const title = req.brew ? req.brew.title : '';
|
const title = req.brew ? req.brew.title : '';
|
||||||
const page = await templateFn('homebrew', title, props)
|
const page = await templateFn('homebrew', title, props)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return res.sendStatus(500);
|
|
||||||
});
|
});
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
//Send rendered page
|
||||||
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
|
const page = await renderPage(req, res);
|
||||||
if(!page) return;
|
if(!page) return;
|
||||||
res.send(page);
|
res.send(page);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//v=====----- Error-Handling Middleware -----=====v//
|
//v=====----- Error-Handling Middleware -----=====v//
|
||||||
//Format Errors so all fields will be sent
|
//Format Errors as plain objects so all fields will appear in the string sent
|
||||||
const replaceErrors = (key, value)=>{
|
const formatErrors = (key, value)=>{
|
||||||
if(value instanceof Error) {
|
if(value instanceof Error) {
|
||||||
const error = {};
|
const error = {};
|
||||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||||
@@ -322,13 +461,30 @@ const replaceErrors = (key, value)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getPureError = (error)=>{
|
const getPureError = (error)=>{
|
||||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
return JSON.parse(JSON.stringify(error, formatErrors));
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use((err, req, res, next)=>{
|
app.use(async (err, req, res, next)=>{
|
||||||
const status = err.status || 500;
|
const status = err.status || err.code || 500;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(status).send(getPureError(err));
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Error Page',
|
||||||
|
description : 'Something went wrong!'
|
||||||
|
};
|
||||||
|
req.brew = {
|
||||||
|
...err,
|
||||||
|
title : 'Error - Something went wrong!',
|
||||||
|
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
|
||||||
|
status : status,
|
||||||
|
HBErrorCode : err.HBErrorCode ?? '00',
|
||||||
|
pureError : getPureError(err)
|
||||||
|
};
|
||||||
|
req.customUrl= '/error';
|
||||||
|
|
||||||
|
const page = await renderPage(req, res);
|
||||||
|
if(!page) return;
|
||||||
|
res.send(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.use((req, res)=>{
|
app.use((req, res)=>{
|
||||||
|
|||||||
38
server/brewDefaults.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
// Default properties for newly-created brews
|
||||||
|
const DEFAULT_BREW = {
|
||||||
|
title : '',
|
||||||
|
text : '',
|
||||||
|
style : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : undefined,
|
||||||
|
shareId : undefined,
|
||||||
|
createdAt : undefined,
|
||||||
|
updatedAt : undefined,
|
||||||
|
renderer : 'V3',
|
||||||
|
theme : '5ePHB',
|
||||||
|
authors : [],
|
||||||
|
tags : [],
|
||||||
|
systems : [],
|
||||||
|
lang : 'en',
|
||||||
|
thumbnail : '',
|
||||||
|
views : 0,
|
||||||
|
published : false,
|
||||||
|
pageCount : 1,
|
||||||
|
gDrive : false,
|
||||||
|
trashed : false
|
||||||
|
|
||||||
|
};
|
||||||
|
// Default values for older brews with missing properties
|
||||||
|
// e.g., missing "renderer" is assumed to be "legacy"
|
||||||
|
const DEFAULT_BREW_LOAD = _.defaults(
|
||||||
|
{
|
||||||
|
renderer : 'legacy',
|
||||||
|
},
|
||||||
|
DEFAULT_BREW);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DEFAULT_BREW,
|
||||||
|
DEFAULT_BREW_LOAD
|
||||||
|
};
|
||||||
@@ -27,8 +27,8 @@ const disconnect = async ()=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const connect = async (config)=>{
|
const connect = async (config)=>{
|
||||||
return await Mongoose.connect(getMongoDBURL(config),
|
return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
|
||||||
{ retryWrites: false }, handleConnectionError);
|
.catch((error)=>handleConnectionError(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -1,35 +1,39 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { google } = require('googleapis');
|
const googleDrive = require('@googleapis/drive');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const token = require('./token.js');
|
const token = require('./token.js');
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
|
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
|
||||||
JSON.parse(config.get('service_account')) :
|
|
||||||
config.get('service_account');
|
|
||||||
let serviceAuth;
|
let serviceAuth;
|
||||||
try {
|
if(!config.get('service_account')){
|
||||||
serviceAuth = google.auth.fromJSON(keys);
|
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
|
||||||
serviceAuth.scopes = [
|
} else {
|
||||||
'https://www.googleapis.com/auth/drive'
|
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||||
];
|
JSON.parse(config.get('service_account')) :
|
||||||
} catch (err) {
|
config.get('service_account');
|
||||||
console.warn(err);
|
|
||||||
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
|
try {
|
||||||
|
serviceAuth = googleDrive.auth.fromJSON(keys);
|
||||||
|
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
console.log('Please make sure the Google Service Account is set up properly in your config files.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
|
||||||
|
const defaultAuth = serviceAuth || config.get('google_api_key');
|
||||||
|
|
||||||
const GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
authCheck : (account, res)=>{
|
authCheck : (account, res, updateTokens=true)=>{
|
||||||
if(!account || !account.googleId){ // If not signed into Google
|
if(!account || !account.googleId){ // If not signed into Google
|
||||||
const err = new Error('Not Signed In');
|
const err = new Error('Not Signed In');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
throw (err);
|
throw (err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oAuth2Client = new google.auth.OAuth2(
|
const oAuth2Client = new googleDrive.auth.OAuth2(
|
||||||
config.get('google_client_id'),
|
config.get('google_client_id'),
|
||||||
config.get('google_client_secret'),
|
config.get('google_client_secret'),
|
||||||
'/auth/google/redirect'
|
'/auth/google/redirect'
|
||||||
@@ -40,7 +44,7 @@ const GoogleActions = {
|
|||||||
refresh_token : account.googleRefreshToken
|
refresh_token : account.googleRefreshToken
|
||||||
});
|
});
|
||||||
|
|
||||||
oAuth2Client.on('tokens', (tokens)=>{
|
updateTokens && oAuth2Client.on('tokens', (tokens)=>{
|
||||||
if(tokens.refresh_token) {
|
if(tokens.refresh_token) {
|
||||||
account.googleRefreshToken = tokens.refresh_token;
|
account.googleRefreshToken = tokens.refresh_token;
|
||||||
}
|
}
|
||||||
@@ -56,7 +60,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getGoogleFolder : async (auth)=>{
|
getGoogleFolder : async (auth)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
fileMetadata = {
|
fileMetadata = {
|
||||||
'name' : 'Homebrewery',
|
'name' : 'Homebrewery',
|
||||||
@@ -93,25 +97,33 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
listGoogleBrews : async (auth)=>{
|
listGoogleBrews : async (auth)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const obj = await drive.files.list({
|
const fileList = [];
|
||||||
pageSize : 1000,
|
let NextPageToken = '';
|
||||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
|
||||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(`Error Listing Google Brews`);
|
|
||||||
console.error(err);
|
|
||||||
throw (err);
|
|
||||||
//TODO: Should break out here, but continues on for some reason.
|
|
||||||
});
|
|
||||||
|
|
||||||
if(!obj.data.files.length) {
|
do {
|
||||||
|
const obj = await drive.files.list({
|
||||||
|
pageSize : 1000,
|
||||||
|
pageToken : NextPageToken || '',
|
||||||
|
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||||
|
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.log(`Error Listing Google Brews`);
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
|
//TODO: Should break out here, but continues on for some reason.
|
||||||
|
});
|
||||||
|
fileList.push(...obj.data.files);
|
||||||
|
NextPageToken = obj.data.nextPageToken;
|
||||||
|
} while (NextPageToken);
|
||||||
|
|
||||||
|
if(!fileList.length) {
|
||||||
console.log('No files found.');
|
console.log('No files found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const brews = obj.data.files.map((file)=>{
|
const brews = fileList.map((file)=>{
|
||||||
return {
|
return {
|
||||||
text : '',
|
text : '',
|
||||||
shareId : file.properties.shareId,
|
shareId : file.properties.shareId,
|
||||||
@@ -125,14 +137,16 @@ const GoogleActions = {
|
|||||||
description : file.description,
|
description : file.description,
|
||||||
views : parseInt(file.properties.views),
|
views : parseInt(file.properties.views),
|
||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
systems : []
|
systems : [],
|
||||||
|
lang : file.properties.lang,
|
||||||
|
thumbnail : file.properties.thumbnail
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return brews;
|
return brews;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGoogleBrew : async (brew)=>{
|
updateGoogleBrew : async (brew)=>{
|
||||||
const drive = google.drive({ version: 'v3' });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
fileId : brew.googleId,
|
fileId : brew.googleId,
|
||||||
@@ -145,7 +159,8 @@ const GoogleActions = {
|
|||||||
editId : brew.editId || nanoid(12),
|
editId : brew.editId || nanoid(12),
|
||||||
pageCount : brew.pageCount,
|
pageCount : brew.pageCount,
|
||||||
renderer : brew.renderer || 'legacy',
|
renderer : brew.renderer || 'legacy',
|
||||||
isStubbed : true
|
isStubbed : true,
|
||||||
|
lang : brew.lang || 'en'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
media : {
|
media : {
|
||||||
@@ -163,7 +178,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
newGoogleBrew : async (auth, brew)=>{
|
newGoogleBrew : async (auth, brew)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const media = {
|
const media = {
|
||||||
mimeType : 'text/plain',
|
mimeType : 'text/plain',
|
||||||
@@ -183,7 +198,8 @@ const GoogleActions = {
|
|||||||
pageCount : brew.pageCount,
|
pageCount : brew.pageCount,
|
||||||
renderer : brew.renderer || 'legacy',
|
renderer : brew.renderer || 'legacy',
|
||||||
isStubbed : true,
|
isStubbed : true,
|
||||||
version : 1
|
version : 1,
|
||||||
|
lang : brew.lang || 'en'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -214,7 +230,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||||
const drive = google.drive({ version: 'v3' });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -227,9 +243,9 @@ const GoogleActions = {
|
|||||||
|
|
||||||
if(obj) {
|
if(obj) {
|
||||||
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
if(accessType == 'edit' && obj.data.properties.editId != accessId){
|
||||||
throw ('Edit ID does not match');
|
throw ({ message: 'Edit ID does not match' });
|
||||||
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
} else if(accessType == 'share' && obj.data.properties.shareId != accessId){
|
||||||
throw ('Share ID does not match');
|
throw ({ message: 'Share ID does not match' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await drive.files.get({
|
const file = await drive.files.get({
|
||||||
@@ -249,9 +265,9 @@ const GoogleActions = {
|
|||||||
text : file.data,
|
text : file.data,
|
||||||
|
|
||||||
description : obj.data.description,
|
description : obj.data.description,
|
||||||
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
|
||||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||||
authors : [],
|
authors : [],
|
||||||
|
lang : obj.data.properties.lang,
|
||||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||||
trashed : obj.data.trashed,
|
trashed : obj.data.trashed,
|
||||||
|
|
||||||
@@ -271,7 +287,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteGoogleBrew : async (auth, id, accessId)=>{
|
deleteGoogleBrew : async (auth, id, accessId)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -297,7 +313,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
increaseView : async (id, accessId, accessType, brew)=>{
|
increaseView : async (id, accessId, accessType, brew)=>{
|
||||||
const drive = google.drive({ version: 'v3' });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
fileId : brew.googleId,
|
fileId : brew.googleId,
|
||||||
|
|||||||
@@ -9,330 +9,356 @@ const yaml = require('js-yaml');
|
|||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||||
|
|
||||||
// const getTopBrews = (cb) => {
|
// const getTopBrews = (cb) => {
|
||||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||||
// cb(brews);
|
// cb(brews);
|
||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const getId = (req)=>{
|
|
||||||
// Set the id and initial potential google id, where the google id is present on the existing brew.
|
|
||||||
let id = req.params.id, googleId = req.body?.googleId;
|
|
||||||
|
|
||||||
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
|
||||||
if(id.length > 12) {
|
|
||||||
googleId = id.slice(0, -12);
|
|
||||||
id = id.slice(-12);
|
|
||||||
}
|
|
||||||
return { id, googleId };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBrew = (accessType)=>{
|
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
|
||||||
return async (req, res, next)=>{
|
|
||||||
// Get relevant IDs for the brew
|
|
||||||
const { id, googleId } = getId(req);
|
|
||||||
|
|
||||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
|
||||||
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
|
||||||
.catch((err)=>{
|
|
||||||
if(googleId) {
|
|
||||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
|
||||||
} else {
|
|
||||||
console.warn(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stub = stub?.toObject();
|
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
|
||||||
if(googleId || stub?.googleId) {
|
|
||||||
let googleError;
|
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.warn(err);
|
|
||||||
googleError = err;
|
|
||||||
});
|
|
||||||
// If we can't find the google brew and there is a google id for the brew, throw an error.
|
|
||||||
if(!googleBrew) throw googleError;
|
|
||||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
|
||||||
stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
|
||||||
if(!stub) {
|
|
||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
|
||||||
}
|
|
||||||
|
|
||||||
if(typeof stub.tags === 'string') {
|
|
||||||
stub.tags = [];
|
|
||||||
}
|
|
||||||
req.brew = stub;
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeBrewText = (brew)=>{
|
|
||||||
let text = brew.text;
|
|
||||||
if(brew.style !== undefined) {
|
|
||||||
text = `\`\`\`css\n` +
|
|
||||||
`${brew.style || ''}\n` +
|
|
||||||
`\`\`\`\n\n` +
|
|
||||||
`${text}`;
|
|
||||||
}
|
|
||||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
|
|
||||||
text = `\`\`\`metadata\n` +
|
|
||||||
`${yaml.dump(metadata)}\n` +
|
|
||||||
`\`\`\`\n\n` +
|
|
||||||
`${text}`;
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_TITLE_LENGTH = 100;
|
const MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
const getGoodBrewTitle = (text)=>{
|
const api = {
|
||||||
const tokens = Markdown.marked.lexer(text);
|
homebrewApi : router,
|
||||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
getId : (req)=>{
|
||||||
.slice(0, MAX_TITLE_LENGTH);
|
// Set the id and initial potential google id, where the google id is present on the existing brew.
|
||||||
};
|
let id = req.params.id, googleId = req.body?.googleId;
|
||||||
|
|
||||||
const excludePropsFromUpdate = (brew)=>{
|
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
||||||
// Remove undesired properties
|
if(id.length > 12) {
|
||||||
const modified = _.clone(brew);
|
if(id.length >= (33 + 12)) { // googleId is minimum 33 chars (may increase)
|
||||||
const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId'];
|
googleId = id.slice(0, -12); // current editId is 12 chars
|
||||||
for (const prop of propsToExclude) {
|
} else { // old editIds used to be 10 chars;
|
||||||
delete modified[prop];
|
googleId = id.slice(0, -10); // if total string is too short, must be old brew
|
||||||
}
|
console.log('Old brew, using 10-char Id');
|
||||||
return modified;
|
}
|
||||||
};
|
id = id.slice(googleId.length);
|
||||||
|
|
||||||
const excludeGoogleProps = (brew)=>{
|
|
||||||
const modified = _.clone(brew);
|
|
||||||
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
|
||||||
for (const prop of propsToExclude) {
|
|
||||||
delete modified[prop];
|
|
||||||
}
|
|
||||||
return modified;
|
|
||||||
};
|
|
||||||
|
|
||||||
const excludeStubProps = (brew)=>{
|
|
||||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version'];
|
|
||||||
for (const prop of propsToExclude) {
|
|
||||||
brew[prop] = undefined;
|
|
||||||
}
|
|
||||||
return brew;
|
|
||||||
};
|
|
||||||
|
|
||||||
const beforeNewSave = (account, brew)=>{
|
|
||||||
if(!brew.title) {
|
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.authors = (account) ? [account.username] : [];
|
|
||||||
brew.text = mergeBrewText(brew);
|
|
||||||
};
|
|
||||||
|
|
||||||
const newGoogleBrew = async (account, brew, res)=>{
|
|
||||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
|
||||||
|
|
||||||
const newBrew = excludeGoogleProps(brew);
|
|
||||||
|
|
||||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
|
||||||
};
|
|
||||||
|
|
||||||
const newBrew = async (req, res)=>{
|
|
||||||
const brew = req.body;
|
|
||||||
const { saveToGoogle } = req.query;
|
|
||||||
|
|
||||||
delete brew.editId;
|
|
||||||
delete brew.shareId;
|
|
||||||
delete brew.googleId;
|
|
||||||
|
|
||||||
beforeNewSave(req.account, brew);
|
|
||||||
|
|
||||||
const newHomebrew = new HomebrewModel(brew);
|
|
||||||
newHomebrew.editId = nanoid(12);
|
|
||||||
newHomebrew.shareId = nanoid(12);
|
|
||||||
|
|
||||||
let googleId, saved;
|
|
||||||
if(saveToGoogle) {
|
|
||||||
googleId = await newGoogleBrew(req.account, newHomebrew, res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
|
||||||
});
|
|
||||||
if(!googleId) return;
|
|
||||||
excludeStubProps(newHomebrew);
|
|
||||||
newHomebrew.googleId = googleId;
|
|
||||||
} else {
|
|
||||||
// Compress brew text to binary before saving
|
|
||||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
|
||||||
newHomebrew.text = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
saved = await newHomebrew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err, err.toString(), err.stack);
|
|
||||||
throw `Error while creating new brew, ${err.toString()}`;
|
|
||||||
});
|
|
||||||
if(!saved) return;
|
|
||||||
saved = saved.toObject();
|
|
||||||
|
|
||||||
res.status(200).send(saved);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBrew = async (req, res)=>{
|
|
||||||
// Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
|
|
||||||
let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
|
|
||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
|
||||||
const googleId = brew.googleId;
|
|
||||||
let afterSave = async ()=>true;
|
|
||||||
|
|
||||||
brew.text = mergeBrewText(brew);
|
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
|
||||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
|
||||||
afterSave = async ()=>{
|
|
||||||
return await deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
brew.googleId = undefined;
|
|
||||||
} else if(!brew.googleId && saveToGoogle) {
|
|
||||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
|
||||||
brew.googleId = await newGoogleBrew(req.account, excludeGoogleProps(brew), res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || err.response.status).send(err.message || err);
|
|
||||||
});
|
|
||||||
if(!brew.googleId) return;
|
|
||||||
} else if(brew.googleId) {
|
|
||||||
// If the google id exists and no other actions are being performed, update the google brew
|
|
||||||
const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew))
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err?.response?.status || 500).send(err);
|
|
||||||
});
|
|
||||||
if(!updated) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(brew.googleId) {
|
|
||||||
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
|
||||||
excludeStubProps(brew);
|
|
||||||
} else {
|
|
||||||
// Compress brew text to binary before saving
|
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
|
||||||
brew.text = undefined;
|
|
||||||
}
|
|
||||||
brew.updatedAt = new Date();
|
|
||||||
|
|
||||||
if(req.account) {
|
|
||||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it
|
|
||||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
|
||||||
|
|
||||||
if(!brew.markModified) {
|
|
||||||
// If it wasn't in the database, create a new db brew
|
|
||||||
brew = new HomebrewModel(brew);
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.markModified('authors');
|
|
||||||
brew.markModified('systems');
|
|
||||||
|
|
||||||
// Save the database brew
|
|
||||||
const saved = await brew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
|
||||||
});
|
|
||||||
if(!saved) return;
|
|
||||||
// Call and wait for afterSave to complete
|
|
||||||
const after = await afterSave();
|
|
||||||
if(!after) return;
|
|
||||||
|
|
||||||
res.status(200).send(saved);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteGoogleBrew = async (account, id, editId, res)=>{
|
|
||||||
const auth = await GoogleActions.authCheck(account, res);
|
|
||||||
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteBrew = async (req, res, next)=>{
|
|
||||||
// Delete an orphaned stub if its Google brew doesn't exist
|
|
||||||
try {
|
|
||||||
await getBrew('edit')(req, res, ()=>{});
|
|
||||||
} catch (err) {
|
|
||||||
const { id, googleId } = getId(req);
|
|
||||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
|
||||||
await HomebrewModel.deleteOne({ editId: id });
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
let brew = req.brew;
|
|
||||||
const { googleId, editId } = brew;
|
|
||||||
const account = req.account;
|
|
||||||
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
|
|
||||||
// If the user is the owner and the file is saved to google, mark the google brew for deletion
|
|
||||||
const shouldDeleteGoogleBrew = googleId && isOwner;
|
|
||||||
|
|
||||||
if(brew._id) {
|
|
||||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
|
||||||
if(account) {
|
|
||||||
// Remove current user as author
|
|
||||||
brew.authors = _.pull(brew.authors, account.username);
|
|
||||||
brew.markModified('authors');
|
|
||||||
}
|
}
|
||||||
|
return { id, googleId };
|
||||||
|
},
|
||||||
|
getBrew : (accessType, stubOnly = false)=>{
|
||||||
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
|
return async (req, res, next)=>{
|
||||||
|
// Get relevant IDs for the brew
|
||||||
|
const { id, googleId } = api.getId(req);
|
||||||
|
|
||||||
if(brew.authors.length === 0) {
|
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||||
// Delete brew if there are no authors left
|
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
||||||
await brew.remove()
|
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
if(googleId) {
|
||||||
throw { status: 500, message: 'Error while removing' };
|
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||||
|
} else {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
stub = stub?.toObject();
|
||||||
if(shouldDeleteGoogleBrew) {
|
|
||||||
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
// If there is a google id, try to find the google brew
|
||||||
brew.googleId = undefined;
|
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
let googleError;
|
||||||
brew.text = undefined;
|
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||||
|
.catch((err)=>{
|
||||||
|
googleError = err;
|
||||||
|
});
|
||||||
|
// Throw any error caught while attempting to retrieve Google brew.
|
||||||
|
if(googleError) {
|
||||||
|
const reason = googleError.errors?.[0].reason;
|
||||||
|
if(reason == 'notFound') {
|
||||||
|
throw { ...googleError, HBErrorCode: '02', authors: stub?.authors, account: req.account?.username };
|
||||||
|
} else {
|
||||||
|
throw { ...googleError, HBErrorCode: '01' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||||
|
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||||
|
}
|
||||||
|
const authorsExist = stub?.authors?.length > 0;
|
||||||
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||||
|
const accessError = { name: 'Access Error', status: 401 };
|
||||||
|
if(req.account){
|
||||||
|
throw { ...accessError, message: 'User is not an Author', HBErrorCode: '03', authors: stub.authors, brewTitle: stub.title };
|
||||||
|
}
|
||||||
|
throw { ...accessError, message: 'User is not logged in', HBErrorCode: '04', authors: stub.authors, brewTitle: stub.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, save the brew with updated author list
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
await brew.save()
|
if(!stub && !stubOnly) {
|
||||||
.catch((err)=>{
|
throw { name: 'BrewLoad Error', message: 'Brew not found', status: 404, HBErrorCode: '05', accessType: accessType, brewId: id };
|
||||||
throw { status: 500, message: err };
|
}
|
||||||
});
|
|
||||||
|
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||||
|
if(stub) {
|
||||||
|
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||||
|
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||||
|
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||||
|
}
|
||||||
|
|
||||||
|
req.brew = stub ?? {};
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mergeBrewText : (brew)=>{
|
||||||
|
let text = brew.text;
|
||||||
|
if(brew.style !== undefined) {
|
||||||
|
text = `\`\`\`css\n` +
|
||||||
|
`${brew.style || ''}\n` +
|
||||||
|
`\`\`\`\n\n` +
|
||||||
|
`${text}`;
|
||||||
}
|
}
|
||||||
}
|
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
|
||||||
if(shouldDeleteGoogleBrew) {
|
text = `\`\`\`metadata\n` +
|
||||||
const deleted = await deleteGoogleBrew(account, googleId, editId, res)
|
`${yaml.dump(metadata)}\n` +
|
||||||
|
`\`\`\`\n\n` +
|
||||||
|
`${text}`;
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
getGoodBrewTitle : (text)=>{
|
||||||
|
const tokens = Markdown.marked.lexer(text);
|
||||||
|
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||||
|
.slice(0, MAX_TITLE_LENGTH);
|
||||||
|
},
|
||||||
|
excludePropsFromUpdate : (brew)=>{
|
||||||
|
// Remove undesired properties
|
||||||
|
const modified = _.clone(brew);
|
||||||
|
const propsToExclude = ['_id', 'views', 'lastViewed'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
delete modified[prop];
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
},
|
||||||
|
excludeGoogleProps : (brew)=>{
|
||||||
|
const modified = _.clone(brew);
|
||||||
|
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
delete modified[prop];
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
},
|
||||||
|
excludeStubProps : (brew)=>{
|
||||||
|
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
brew[prop] = undefined;
|
||||||
|
}
|
||||||
|
return brew;
|
||||||
|
},
|
||||||
|
beforeNewSave : (account, brew)=>{
|
||||||
|
if(!brew.title) {
|
||||||
|
brew.title = api.getGoodBrewTitle(brew.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
brew.authors = (account) ? [account.username] : [];
|
||||||
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
|
_.defaults(brew, DEFAULT_BREW);
|
||||||
|
},
|
||||||
|
newGoogleBrew : async (account, brew, res)=>{
|
||||||
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
|
|
||||||
|
const newBrew = api.excludeGoogleProps(brew);
|
||||||
|
|
||||||
|
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||||
|
},
|
||||||
|
newBrew : async (req, res)=>{
|
||||||
|
const brew = req.body;
|
||||||
|
const { saveToGoogle } = req.query;
|
||||||
|
|
||||||
|
delete brew.editId;
|
||||||
|
delete brew.shareId;
|
||||||
|
delete brew.googleId;
|
||||||
|
|
||||||
|
api.beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
|
const newHomebrew = new HomebrewModel(brew);
|
||||||
|
newHomebrew.editId = nanoid(12);
|
||||||
|
newHomebrew.shareId = nanoid(12);
|
||||||
|
|
||||||
|
let googleId, saved;
|
||||||
|
if(saveToGoogle) {
|
||||||
|
googleId = await api.newGoogleBrew(req.account, newHomebrew, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
||||||
|
});
|
||||||
|
if(!googleId) return;
|
||||||
|
api.excludeStubProps(newHomebrew);
|
||||||
|
newHomebrew.googleId = googleId;
|
||||||
|
} else {
|
||||||
|
// Compress brew text to binary before saving
|
||||||
|
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||||
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
|
newHomebrew.text = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
saved = await newHomebrew.save()
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err, err.toString(), err.stack);
|
||||||
res.status(500).send(err);
|
throw { name: 'BrewSave Error', message: `Error while creating new brew, ${err.toString()}`, status: 500, HBErrorCode: '06' };
|
||||||
});
|
});
|
||||||
if(!deleted) return;
|
if(!saved) return;
|
||||||
|
saved = saved.toObject();
|
||||||
|
|
||||||
|
res.status(200).send(saved);
|
||||||
|
},
|
||||||
|
updateBrew : async (req, res)=>{
|
||||||
|
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||||
|
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
|
const brewFromServer = req.brew;
|
||||||
|
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
||||||
|
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let brew = _.assign(brewFromServer, brewFromClient);
|
||||||
|
const googleId = brew.googleId;
|
||||||
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
|
if(brew.googleId && removeFromGoogle) {
|
||||||
|
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||||
|
afterSave = async ()=>{
|
||||||
|
return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
brew.googleId = undefined;
|
||||||
|
} else if(!brew.googleId && saveToGoogle) {
|
||||||
|
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||||
|
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
if(!brew.googleId) return;
|
||||||
|
} else if(brew.googleId) {
|
||||||
|
// If the google id exists and no other actions are being performed, update the google brew
|
||||||
|
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew))
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.response?.status || 500).send(err);
|
||||||
|
});
|
||||||
|
if(!updated) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(brew.googleId) {
|
||||||
|
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
||||||
|
api.excludeStubProps(brew);
|
||||||
|
} else {
|
||||||
|
// Compress brew text to binary before saving
|
||||||
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
|
brew.text = undefined;
|
||||||
|
}
|
||||||
|
brew.updatedAt = new Date();
|
||||||
|
brew.version = (brew.version || 1) + 1;
|
||||||
|
|
||||||
|
if(req.account) {
|
||||||
|
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||||
|
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// define a function to catch our save errors
|
||||||
|
const saveError = (err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||||
|
};
|
||||||
|
let saved;
|
||||||
|
if(!brew._id) {
|
||||||
|
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
|
||||||
|
saved = await new HomebrewModel(brew).save().catch(saveError);
|
||||||
|
} else {
|
||||||
|
// if the brew does have a stub id, update it using the stub id as the key.
|
||||||
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
saved = await brew.save()
|
||||||
|
.catch(saveError);
|
||||||
|
}
|
||||||
|
if(!saved) return;
|
||||||
|
// Call and wait for afterSave to complete
|
||||||
|
const after = await afterSave();
|
||||||
|
if(!after) return;
|
||||||
|
|
||||||
|
res.status(200).send(saved);
|
||||||
|
},
|
||||||
|
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||||
|
const auth = await GoogleActions.authCheck(account, res);
|
||||||
|
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteBrew : async (req, res, next)=>{
|
||||||
|
// Delete an orphaned stub if its Google brew doesn't exist
|
||||||
|
try {
|
||||||
|
await api.getBrew('edit')(req, res, ()=>{});
|
||||||
|
} catch (err) {
|
||||||
|
// Only if the error code is HBErrorCode '02', that is, Google returned "404 - Not Found"
|
||||||
|
if(err.HBErrorCode == '02') {
|
||||||
|
const { id, googleId } = api.getId(req);
|
||||||
|
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||||
|
await HomebrewModel.deleteOne({ editId: id });
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let brew = req.brew;
|
||||||
|
const { googleId, editId } = brew;
|
||||||
|
const account = req.account;
|
||||||
|
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
|
||||||
|
// If the user is the owner and the file is saved to google, mark the google brew for deletion
|
||||||
|
const shouldDeleteGoogleBrew = googleId && isOwner;
|
||||||
|
|
||||||
|
if(brew._id) {
|
||||||
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
if(account) {
|
||||||
|
// Remove current user as author
|
||||||
|
brew.authors = _.pull(brew.authors, account.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(brew.authors.length === 0) {
|
||||||
|
// Delete brew if there are no authors left
|
||||||
|
await HomebrewModel.deleteOne({ _id: brew._id })
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
throw { name: 'BrewDelete Error', message: 'Error while removing', status: 500, HBErrorCode: '07', brewId: brew._id };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if(shouldDeleteGoogleBrew) {
|
||||||
|
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
||||||
|
brew.googleId = undefined;
|
||||||
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
|
brew.text = undefined;
|
||||||
|
}
|
||||||
|
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||||
|
await brew.save()
|
||||||
|
.catch((err)=>{
|
||||||
|
throw { name: 'BrewAuthorDelete Error', message: err, status: 500, HBErrorCode: '08', brewId: brew._id };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(shouldDeleteGoogleBrew) {
|
||||||
|
const deleted = await api.deleteGoogleBrew(account, googleId, editId, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
if(!deleted) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(204).send();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', asyncHandler(newBrew));
|
router.use('/api', require('./middleware/check-client-version.js'));
|
||||||
router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.post('/api', asyncHandler(api.newBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(deleteBrew));
|
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||||
|
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||||
|
|
||||||
module.exports = {
|
module.exports = api;
|
||||||
homebrewApi : router,
|
|
||||||
getBrew
|
|
||||||
};
|
|
||||||
|
|||||||
792
server/homebrew.api.spec.js
Normal file
@@ -0,0 +1,792 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
describe('Tests for api', ()=>{
|
||||||
|
let api;
|
||||||
|
let google;
|
||||||
|
let model;
|
||||||
|
let hbBrew;
|
||||||
|
let googleBrew;
|
||||||
|
let res;
|
||||||
|
|
||||||
|
let modelBrew;
|
||||||
|
let saveFunc;
|
||||||
|
let markModifiedFunc;
|
||||||
|
let saved;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
saved = undefined;
|
||||||
|
saveFunc = jest.fn(async function() {
|
||||||
|
saved = { ...this, _id: '1' };
|
||||||
|
return saved;
|
||||||
|
});
|
||||||
|
markModifiedFunc = jest.fn(()=>true);
|
||||||
|
|
||||||
|
modelBrew = (brew)=>({
|
||||||
|
...brew,
|
||||||
|
save : saveFunc,
|
||||||
|
markModified : markModifiedFunc,
|
||||||
|
toObject : function() {
|
||||||
|
delete this.save;
|
||||||
|
delete this.toObject;
|
||||||
|
delete this.markModified;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
google = require('./googleActions.js');
|
||||||
|
model = require('./homebrew.model.js').model;
|
||||||
|
|
||||||
|
jest.mock('./googleActions.js');
|
||||||
|
google.authCheck = jest.fn(()=>'client');
|
||||||
|
google.newGoogleBrew = jest.fn(()=>'id');
|
||||||
|
google.deleteGoogleBrew = jest.fn(()=>true);
|
||||||
|
|
||||||
|
jest.mock('./homebrew.model.js');
|
||||||
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
|
res = {
|
||||||
|
status : jest.fn(()=>res),
|
||||||
|
send : jest.fn(()=>{})
|
||||||
|
};
|
||||||
|
|
||||||
|
api = require('./homebrew.api');
|
||||||
|
|
||||||
|
hbBrew = {
|
||||||
|
text : `brew text`,
|
||||||
|
style : 'hello yes i am css',
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
lang : 'en',
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
published : true,
|
||||||
|
authors : ['1', '2'],
|
||||||
|
owner : '1',
|
||||||
|
thumbnail : '',
|
||||||
|
_id : 'mongoid',
|
||||||
|
editId : 'abcdefg',
|
||||||
|
shareId : 'hijklmnop',
|
||||||
|
views : 1,
|
||||||
|
lastViewed : new Date(),
|
||||||
|
version : 1,
|
||||||
|
pageCount : 1,
|
||||||
|
textBin : '',
|
||||||
|
views : 0
|
||||||
|
};
|
||||||
|
googleBrew = {
|
||||||
|
...hbBrew,
|
||||||
|
googleId : '12345'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getId', ()=>{
|
||||||
|
it('should return only id if google id is not present', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefgh'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefgh');
|
||||||
|
expect(googleId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return id and google id from request body', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefgh'
|
||||||
|
},
|
||||||
|
body : {
|
||||||
|
googleId : '12345'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefgh');
|
||||||
|
expect(googleId).toEqual('12345');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 12-char id and google id from params', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : '123456789012345678901234567890123abcdefghijkl'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||||
|
expect(id).toEqual('abcdefghijkl');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 10-char id and google id from params', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : '123456789012345678901234567890123abcdefghij'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(googleId).toEqual('123456789012345678901234567890123');
|
||||||
|
expect(id).toEqual('abcdefghij');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBrew', ()=>{
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
const notFoundError = { HBErrorCode: '05', message: 'Brew not found', name: 'BrewLoad Error', status: 404, accessType: 'share', brewId: '1' };
|
||||||
|
|
||||||
|
it('returns middleware', ()=>{
|
||||||
|
const getFn = api.getBrew('share');
|
||||||
|
expect(getFn).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch from mongoose', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', authors: [] };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual(testBrew);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mongoose error', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>new Promise((_, rej)=>rej('Unable to find brew')));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, next);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual(notFoundError);
|
||||||
|
expect(req.brew).toEqual({});
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes tags from string to array', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', authors: [], tags: '' };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew.tags).toEqual([]);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if not logged in as author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, null);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '04', message: 'User is not logged in', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if logged in as invalid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {}, account: { username: 'b' } };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, null);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual({ HBErrorCode: '03', message: 'User is not an Author', name: 'Access Error', status: 401, brewTitle: 'test brew', authors: ['a'] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw if no authors', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: [] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(req.brew.title).toEqual('test brew');
|
||||||
|
expect(req.brew.authors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw if valid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {}, account: { username: 'a' } };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(req.brew.title).toEqual('test brew');
|
||||||
|
expect(req.brew.authors).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches google brew if needed', async()=>{
|
||||||
|
const stubBrew = { title: 'test brew', authors: ['a'] };
|
||||||
|
const googleBrew = { title: 'test google brew', text: 'brew text' };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(stubBrew));
|
||||||
|
google.getGoogleBrew = jest.fn(()=>new Promise((res)=>res(googleBrew)));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual({
|
||||||
|
title : 'test google brew',
|
||||||
|
authors : ['a'],
|
||||||
|
text : 'brew text',
|
||||||
|
stubbed : true,
|
||||||
|
description : '',
|
||||||
|
editId : undefined,
|
||||||
|
pageCount : 1,
|
||||||
|
published : false,
|
||||||
|
renderer : 'legacy',
|
||||||
|
lang : 'en',
|
||||||
|
shareId : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
textBin : undefined,
|
||||||
|
version : undefined,
|
||||||
|
createdAt : undefined,
|
||||||
|
gDrive : false,
|
||||||
|
style : undefined,
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined,
|
||||||
|
views : 0
|
||||||
|
});
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mergeBrewText', ()=>{
|
||||||
|
it('should set metadata and no style if it is not present', ()=>{
|
||||||
|
const result = api.mergeBrewText({
|
||||||
|
text : `brew`,
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
googleId : '12345'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(`\`\`\`metadata
|
||||||
|
title: some title
|
||||||
|
description: this is a description
|
||||||
|
tags:
|
||||||
|
- something
|
||||||
|
- fun
|
||||||
|
systems:
|
||||||
|
- D&D 5e
|
||||||
|
renderer: v3
|
||||||
|
theme: phb
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
brew`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set metadata and style', ()=>{
|
||||||
|
const result = api.mergeBrewText({
|
||||||
|
text : `brew`,
|
||||||
|
style : 'hello yes i am css',
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
googleId : '12345'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(`\`\`\`metadata
|
||||||
|
title: some title
|
||||||
|
description: this is a description
|
||||||
|
tags:
|
||||||
|
- something
|
||||||
|
- fun
|
||||||
|
systems:
|
||||||
|
- D&D 5e
|
||||||
|
renderer: v3
|
||||||
|
theme: phb
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
\`\`\`css
|
||||||
|
hello yes i am css
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
brew`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exclusion methods', ()=>{
|
||||||
|
it('excludePropsFromUpdate removes the correct keys', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludePropsFromUpdate(sent);
|
||||||
|
|
||||||
|
expect(sent).toEqual(googleBrew);
|
||||||
|
expect(result._id).toBeUndefined();
|
||||||
|
expect(result.views).toBeUndefined();
|
||||||
|
expect(result.lastViewed).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludeGoogleProps removes the correct keys', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludeGoogleProps(sent);
|
||||||
|
|
||||||
|
expect(sent).toEqual(googleBrew);
|
||||||
|
expect(result.tags).toBeUndefined();
|
||||||
|
expect(result.systems).toBeUndefined();
|
||||||
|
expect(result.published).toBeUndefined();
|
||||||
|
expect(result.authors).toBeUndefined();
|
||||||
|
expect(result.owner).toBeUndefined();
|
||||||
|
expect(result.views).toBeUndefined();
|
||||||
|
expect(result.thumbnail).toBeUndefined();
|
||||||
|
expect(result.version).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludeStubProps removes the correct keys from the original object', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludeStubProps(sent);
|
||||||
|
|
||||||
|
expect(sent).not.toEqual(googleBrew);
|
||||||
|
expect(result.text).toBeUndefined();
|
||||||
|
expect(result.textBin).toBeUndefined();
|
||||||
|
expect(result.renderer).toBeUndefined();
|
||||||
|
expect(result.pageCount).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('beforeNewSave', ()=>{
|
||||||
|
it('sets the title if none', ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
title : undefined
|
||||||
|
};
|
||||||
|
api.beforeNewSave({}, brew);
|
||||||
|
|
||||||
|
expect(brew.title).toEqual('brew text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not override the title if present', ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
title : 'test'
|
||||||
|
};
|
||||||
|
api.beforeNewSave({}, brew);
|
||||||
|
|
||||||
|
expect(brew.title).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set authors if account missing username', ()=>{
|
||||||
|
api.beforeNewSave({}, hbBrew);
|
||||||
|
|
||||||
|
expect(hbBrew.authors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets authors if account has username', ()=>{
|
||||||
|
api.beforeNewSave({ username: 'hi' }, hbBrew);
|
||||||
|
|
||||||
|
expect(hbBrew.authors).toEqual(['hi']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges brew text', ()=>{
|
||||||
|
api.mergeBrewText = jest.fn(()=>'merged');
|
||||||
|
api.beforeNewSave({}, hbBrew);
|
||||||
|
|
||||||
|
expect(api.mergeBrewText).toHaveBeenCalled();
|
||||||
|
expect(hbBrew.text).toEqual('merged');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newGoogleBrew', ()=>{
|
||||||
|
it('should call the correct methods', ()=>{
|
||||||
|
api.excludeGoogleProps = jest.fn(()=>'newBrew');
|
||||||
|
|
||||||
|
const acct = { username: 'test' };
|
||||||
|
const brew = { title: 'test title' };
|
||||||
|
api.newGoogleBrew(acct, brew, res);
|
||||||
|
|
||||||
|
expect(google.authCheck).toHaveBeenCalledWith(acct, res);
|
||||||
|
expect(api.excludeGoogleProps).toHaveBeenCalledWith(brew);
|
||||||
|
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newBrew', ()=>{
|
||||||
|
it('should set up a default brew via Homebrew model', async ()=>{
|
||||||
|
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
_id : '1',
|
||||||
|
authors : ['test user'],
|
||||||
|
createdAt : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : expect.any(String),
|
||||||
|
gDrive : false,
|
||||||
|
pageCount : 1,
|
||||||
|
published : false,
|
||||||
|
renderer : 'V3',
|
||||||
|
lang : 'en',
|
||||||
|
shareId : expect.any(String),
|
||||||
|
style : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
text : undefined,
|
||||||
|
textBin : expect.objectContaining({}),
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
title : 'asdf',
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined,
|
||||||
|
views : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove edit/share/google ids', async ()=>{
|
||||||
|
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalled();
|
||||||
|
const sent = res.send.mock.calls[0][0];
|
||||||
|
expect(sent.editId).not.toEqual('1234');
|
||||||
|
expect(sent.shareId).not.toEqual('1234');
|
||||||
|
expect(sent.googleId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mongo error', async ()=>{
|
||||||
|
saveFunc = jest.fn(async function() {
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(res.send).not.toHaveBeenCalled();
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save to google if requested', async()=>{
|
||||||
|
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(google.newGoogleBrew).toHaveBeenCalled();
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
_id : '1',
|
||||||
|
authors : ['test user'],
|
||||||
|
createdAt : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : expect.any(String),
|
||||||
|
gDrive : false,
|
||||||
|
pageCount : undefined,
|
||||||
|
published : false,
|
||||||
|
renderer : undefined,
|
||||||
|
lang : 'en',
|
||||||
|
shareId : expect.any(String),
|
||||||
|
googleId : expect.any(String),
|
||||||
|
style : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
text : undefined,
|
||||||
|
textBin : undefined,
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
title : 'asdf',
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined,
|
||||||
|
views : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle google error', async()=>{
|
||||||
|
google.newGoogleBrew = jest.fn(()=>{
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('err');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteGoogleBrew', ()=>{
|
||||||
|
it('should check auth and delete brew', async ()=>{
|
||||||
|
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
|
||||||
|
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteBrew', ()=>{
|
||||||
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
|
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
const next = jest.fn(()=>{});
|
||||||
|
|
||||||
|
await api.deleteBrew(null, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalledWith({ editId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete if no authors', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : []
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
const req = {};
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on delete error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : []
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{ throw 'err'; });
|
||||||
|
const req = {};
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete when one author', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove one author when multiple present', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(markModifiedFunc).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle save error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
saveFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete google brew', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle google brew delete error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>{
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete google brew and retain stub when multiple authors and owner request deletion', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(markModifiedFunc).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test2']);
|
||||||
|
expect(saved.googleId).toEqual(undefined);
|
||||||
|
expect(saved.text).toEqual(undefined);
|
||||||
|
expect(saved.textBin).not.toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test2' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).not.toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test']);
|
||||||
|
expect(saved.googleId).toEqual(brew.googleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,13 +12,15 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
textBin : { type: Buffer },
|
textBin : { type: Buffer },
|
||||||
pageCount : { type: Number, default: 1 },
|
pageCount : { type: Number, default: 1 },
|
||||||
|
|
||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : [String],
|
tags : [String],
|
||||||
systems : [String],
|
systems : [String],
|
||||||
renderer : { type: String, default: '' },
|
lang : { type: String, default: 'en' },
|
||||||
authors : [String],
|
renderer : { type: String, default: '' },
|
||||||
published : { type: Boolean, default: false },
|
authors : [String],
|
||||||
thumbnail : { type: String, default: '' },
|
invitedAuthors : [String],
|
||||||
|
published : { type: Boolean, default: false },
|
||||||
|
thumbnail : { type: String, default: '' },
|
||||||
|
|
||||||
createdAt : { type: Date, default: Date.now },
|
createdAt : { type: Date, default: Date.now },
|
||||||
updatedAt : { type: Date, default: Date.now },
|
updatedAt : { type: Date, default: Date.now },
|
||||||
@@ -38,32 +40,24 @@ HomebrewSchema.statics.increaseView = async function(query) {
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.get = function(query, fields=null){
|
HomebrewSchema.statics.get = async function(query, fields=null){
|
||||||
return new Promise((resolve, reject)=>{
|
const brew = await Homebrew.findOne(query, fields).orFail()
|
||||||
Homebrew.find(query, fields, null, (err, brews)=>{
|
.catch((error)=>{throw 'Can not find brew';});
|
||||||
if(err || !brews.length) return reject('Can not find brew');
|
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
||||||
if(!_.isNil(brews[0].textBin)) { // Uncompress zipped text field
|
unzipped = zlib.inflateRawSync(brew.textBin);
|
||||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
brew.text = unzipped.toString();
|
||||||
brews[0].text = unzipped.toString();
|
}
|
||||||
}
|
return brew;
|
||||||
if(!brews[0].renderer)
|
|
||||||
brews[0].renderer = 'legacy';
|
|
||||||
return resolve(brews[0]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.getByUser = function(username, allowAccess=false, fields=null){
|
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
|
||||||
return new Promise((resolve, reject)=>{
|
const query = { authors: username, published: true };
|
||||||
const query = { authors: username, published: true };
|
if(allowAccess){
|
||||||
if(allowAccess){
|
delete query.published;
|
||||||
delete query.published;
|
}
|
||||||
}
|
const brews = await Homebrew.find(query, fields).lean().exec() //lean() converts results to JSObjects
|
||||||
Homebrew.find(query, fields).lean().exec((err, brews)=>{ //lean() converts results to JSObjects
|
.catch((error)=>{throw 'Can not find brews';});
|
||||||
if(err) return reject('Can not find brew');
|
return brews;
|
||||||
return resolve(brews);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||||
|
|||||||
12
server/middleware/check-client-version.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = (req, res, next)=>{
|
||||||
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = require('../../package.json').version;
|
||||||
|
|
||||||
|
if(userVersion != version) {
|
||||||
|
return res.status(412).send({
|
||||||
|
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
12
server/middleware/content-negotiation.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = (req, res, next)=>{
|
||||||
|
const isImageRequest = req.get('Accept')?.split(',')
|
||||||
|
?.filter((h)=>!h.includes('q='))
|
||||||
|
?.every((h)=>/image\/.*/.test(h));
|
||||||
|
if(isImageRequest) {
|
||||||
|
return res.status(406).send({
|
||||||
|
message : 'Request for image at this URL is not supported'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const contentNegotiationMiddleware = require('./content-negotiation.js');
|
||||||
|
|
||||||
|
describe('content-negotiation-middleware', ()=>{
|
||||||
|
let request;
|
||||||
|
let response;
|
||||||
|
let next;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
request = {
|
||||||
|
get : function(key) {
|
||||||
|
return this[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
response = {
|
||||||
|
status : jest.fn(()=>response),
|
||||||
|
send : jest.fn(()=>{})
|
||||||
|
};
|
||||||
|
next = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 406 on image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response);
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenLastCalledWith(406);
|
||||||
|
expect(response.send).toHaveBeenCalledWith({
|
||||||
|
message : 'Request for image at this URL is not supported'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next on non-image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
36
shared/naturalcrit/codeEditor/code-mirror.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
let CodeMirror;
|
||||||
|
if(typeof navigator !== 'undefined'){
|
||||||
|
CodeMirror = require('codemirror');
|
||||||
|
|
||||||
|
//Language Modes
|
||||||
|
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
||||||
|
require('codemirror/mode/css/css.js');
|
||||||
|
require('codemirror/mode/javascript/javascript.js');
|
||||||
|
|
||||||
|
//Addons
|
||||||
|
//Code folding
|
||||||
|
require('codemirror/addon/fold/foldcode.js');
|
||||||
|
require('codemirror/addon/fold/foldgutter.js');
|
||||||
|
//Search and replace
|
||||||
|
require('codemirror/addon/search/search.js');
|
||||||
|
require('codemirror/addon/search/searchcursor.js');
|
||||||
|
require('codemirror/addon/search/jump-to-line.js');
|
||||||
|
require('codemirror/addon/search/match-highlighter.js');
|
||||||
|
require('codemirror/addon/search/matchesonscrollbar.js');
|
||||||
|
require('codemirror/addon/dialog/dialog.js');
|
||||||
|
//Trailing space highlighting
|
||||||
|
// require('codemirror/addon/edit/trailingspace.js');
|
||||||
|
//Active line highlighting
|
||||||
|
// require('codemirror/addon/selection/active-line.js');
|
||||||
|
//Scroll past last line
|
||||||
|
require('codemirror/addon/scroll/scrollpastend.js');
|
||||||
|
//Auto-closing
|
||||||
|
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
||||||
|
require('codemirror/addon/fold/xml-fold.js');
|
||||||
|
require('codemirror/addon/edit/closetag.js');
|
||||||
|
|
||||||
|
const foldCode = require('./helpers/fold-code');
|
||||||
|
foldCode.registerHomebreweryHelper(CodeMirror);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CodeMirror;
|
||||||
@@ -4,58 +4,32 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const closeTag = require('./close-tag');
|
const closeTag = require('./helpers/close-tag');
|
||||||
|
const Hints = require('./helpers/widget-elements/hints/hints.jsx');
|
||||||
|
const CodeMirror = require('./code-mirror.js');
|
||||||
|
|
||||||
let CodeMirror;
|
const themeWidgets = require('../../../themes/V3/5ePHB/widgets');
|
||||||
if(typeof navigator !== 'undefined'){
|
|
||||||
CodeMirror = require('codemirror');
|
|
||||||
|
|
||||||
//Language Modes
|
|
||||||
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
|
|
||||||
require('codemirror/mode/css/css.js');
|
|
||||||
require('codemirror/mode/javascript/javascript.js');
|
|
||||||
|
|
||||||
//Addons
|
|
||||||
//Code folding
|
|
||||||
require('codemirror/addon/fold/foldcode.js');
|
|
||||||
require('codemirror/addon/fold/foldgutter.js');
|
|
||||||
//Search and replace
|
|
||||||
require('codemirror/addon/search/search.js');
|
|
||||||
require('codemirror/addon/search/searchcursor.js');
|
|
||||||
require('codemirror/addon/search/jump-to-line.js');
|
|
||||||
require('codemirror/addon/search/match-highlighter.js');
|
|
||||||
require('codemirror/addon/search/matchesonscrollbar.js');
|
|
||||||
require('codemirror/addon/dialog/dialog.js');
|
|
||||||
//Trailing space highlighting
|
|
||||||
// require('codemirror/addon/edit/trailingspace.js');
|
|
||||||
//Active line highlighting
|
|
||||||
// require('codemirror/addon/selection/active-line.js');
|
|
||||||
//Scroll past last line
|
|
||||||
require('codemirror/addon/scroll/scrollpastend.js');
|
|
||||||
//Auto-closing
|
|
||||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
|
||||||
require('codemirror/addon/fold/xml-fold.js');
|
|
||||||
require('codemirror/addon/edit/closetag.js');
|
|
||||||
|
|
||||||
const foldCode = require('./fold-code');
|
|
||||||
foldCode.registerHomebreweryHelper(CodeMirror);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CodeEditor = createClass({
|
const CodeEditor = createClass({
|
||||||
displayName : 'CodeEditor',
|
displayName : 'CodeEditor',
|
||||||
|
hintsRef : React.createRef(),
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
language : '',
|
language : '',
|
||||||
value : '',
|
value : '',
|
||||||
wrap : true,
|
wrap : true,
|
||||||
onChange : ()=>{},
|
onChange : ()=>{},
|
||||||
enableFolding : true
|
enableFolding : true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
docs : {}
|
docs : {},
|
||||||
|
widgetUtils : {},
|
||||||
|
widgets : {},
|
||||||
|
hints : [],
|
||||||
|
hintsField : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -91,17 +65,23 @@ const CodeEditor = createClass({
|
|||||||
} else {
|
} else {
|
||||||
this.codeMirror.setOption('foldOptions', false);
|
this.codeMirror.setOption('foldOptions', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.state.widgetUtils.updateWidgetGutter();
|
||||||
|
this.state.widgetUtils.updateAllLineWidgets();
|
||||||
},
|
},
|
||||||
|
|
||||||
buildEditor : function() {
|
buildEditor : function() {
|
||||||
this.codeMirror = CodeMirror(this.refs.editor, {
|
this.codeMirror = CodeMirror(this.refs.editor, {
|
||||||
lineNumbers : true,
|
lineNumbers : true,
|
||||||
lineWrapping : this.props.wrap,
|
lineWrapping : this.props.wrap,
|
||||||
indentWithTabs : true,
|
indentWithTabs : false,
|
||||||
tabSize : 2,
|
tabSize : 2,
|
||||||
|
smartIndent : false,
|
||||||
historyEventDelay : 250,
|
historyEventDelay : 250,
|
||||||
scrollPastEnd : true,
|
scrollPastEnd : true,
|
||||||
extraKeys : {
|
extraKeys : {
|
||||||
|
'Tab' : this.indent,
|
||||||
|
'Shift-Tab' : this.dedent,
|
||||||
'Ctrl-B' : this.makeBold,
|
'Ctrl-B' : this.makeBold,
|
||||||
'Cmd-B' : this.makeBold,
|
'Cmd-B' : this.makeBold,
|
||||||
'Ctrl-I' : this.makeItalic,
|
'Ctrl-I' : this.makeItalic,
|
||||||
@@ -152,7 +132,7 @@ const CodeEditor = createClass({
|
|||||||
},
|
},
|
||||||
foldGutter : true,
|
foldGutter : true,
|
||||||
foldOptions : this.foldOptions(this.codeMirror),
|
foldOptions : this.foldOptions(this.codeMirror),
|
||||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'widget-gutter'],
|
||||||
autoCloseTags : true,
|
autoCloseTags : true,
|
||||||
styleActiveLine : true,
|
styleActiveLine : true,
|
||||||
showTrailingSpace : false,
|
showTrailingSpace : false,
|
||||||
@@ -166,9 +146,82 @@ const CodeEditor = createClass({
|
|||||||
});
|
});
|
||||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
widgetUtils : require('./helpers/widgets')(themeWidgets, this.codeMirror, (hints, field)=>{
|
||||||
|
this.setState({
|
||||||
|
hints,
|
||||||
|
hintsField : field
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
// Note: codeMirror passes a copy of itself in this callback. cm === this.codeMirror. Either one works.
|
||||||
this.codeMirror.on('change', (cm)=>{this.props.onChange(cm.getValue());});
|
this.codeMirror.on('change', (cm)=>{
|
||||||
|
this.props.onChange(cm.getValue());
|
||||||
|
this.state.widgetUtils.updateWidgetGutter();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.codeMirror.on('cursorActivity', (cm)=>{
|
||||||
|
const { line } = cm.getCursor();
|
||||||
|
for (const key in this.state.widgets) {
|
||||||
|
if(key != line) {
|
||||||
|
this.state.widgetUtils.removeLineWidget(key, this.state.widgets[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
hints : [],
|
||||||
|
hintsField : undefined
|
||||||
|
});
|
||||||
|
const { widgets } = this.codeMirror.lineInfo(line);
|
||||||
|
if(!widgets) {
|
||||||
|
const widget = this.state.widgetUtils.updateLineWidgets(line);
|
||||||
|
if(widget) {
|
||||||
|
this.setState({
|
||||||
|
widgets : {
|
||||||
|
[line] : widget
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.updateSize();
|
this.updateSize();
|
||||||
|
|
||||||
|
this.codeMirror.on('gutterClick', (cm, n)=>{
|
||||||
|
// Open line widgets when 'widget-gutter' marker clicked
|
||||||
|
if(this.codeMirror.lineInfo(n).gutterMarkers?.['widget-gutter']) {
|
||||||
|
const { widgets } = this.codeMirror.lineInfo(n);
|
||||||
|
if(!widgets) {
|
||||||
|
const widget = this.state.widgetUtils.updateLineWidgets(n);
|
||||||
|
if(widget) {
|
||||||
|
this.setState({
|
||||||
|
widgets : { ...this.state.widgets, [n]: widget }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const widget of widgets) {
|
||||||
|
this.state.widgetUtils.removeLineWidget(n, widget);
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
hints : [],
|
||||||
|
hintsField : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
indent : function () {
|
||||||
|
const cm = this.codeMirror;
|
||||||
|
if(cm.somethingSelected()) {
|
||||||
|
cm.execCommand('indentMore');
|
||||||
|
} else {
|
||||||
|
cm.execCommand('insertSoftTab');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dedent : function () {
|
||||||
|
this.codeMirror.execCommand('indentLess');
|
||||||
},
|
},
|
||||||
|
|
||||||
makeHeader : function (number) {
|
makeHeader : function (number) {
|
||||||
@@ -387,10 +440,19 @@ const CodeEditor = createClass({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
keyDown : function(e) {
|
||||||
|
if(this.hintsRef.current) {
|
||||||
|
this.hintsRef.current.keyDown(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
//----------------------//
|
//----------------------//
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='codeEditor' ref='editor' style={this.props.style}/>;
|
const { hints, hintsField } = this.state;
|
||||||
|
return <React.Fragment>
|
||||||
|
<div className='codeEditor' ref='editor' style={this.props.style} onKeyDown={this.keyDown}/>
|
||||||
|
<Hints ref={this.hintsRef} hints={hints} field={hintsField}/>
|
||||||
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
@import (less) 'codemirror/addon/fold/foldgutter.css';
|
||||||
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
@import (less) 'codemirror/addon/search/matchesonscrollbar.css';
|
||||||
@import (less) 'codemirror/addon/dialog/dialog.css';
|
@import (less) 'codemirror/addon/dialog/dialog.css';
|
||||||
|
@import (less) 'codemirror/addon/hint/show-hint.css';
|
||||||
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
@keyframes sourceMoveAnimation {
|
@keyframes sourceMoveAnimation {
|
||||||
50% {background-color: red; color: white;}
|
50% {background-color: red; color: white;}
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: grey;
|
color: grey;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sourceMoveFlash .CodeMirror-line{
|
.sourceMoveFlash .CodeMirror-line{
|
||||||
animation-name: sourceMoveAnimation;
|
animation-name: sourceMoveAnimation;
|
||||||
@@ -30,4 +32,51 @@
|
|||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
.widget-gutter {
|
||||||
|
width: .7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-options-widget {
|
||||||
|
padding: 2px 0;
|
||||||
|
|
||||||
|
>div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0 2px 2px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
max-width: 10vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.widget-field {
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
|
||||||
|
&.default {
|
||||||
|
background-color: @purple;
|
||||||
|
border: 2px solid @purple;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
>input {
|
||||||
|
background-color: @purple;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.suggested {
|
||||||
|
background-color: #ddd;
|
||||||
|
border: 2px dashed grey;
|
||||||
|
color: grey;
|
||||||
|
|
||||||
|
>input {
|
||||||
|
background-color: #ddd;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
require('./checkbox.less');
|
||||||
|
const CodeMirror = require('../../../code-mirror.js');
|
||||||
|
|
||||||
|
const Checkbox = createClass({
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
value : '',
|
||||||
|
prefix : '',
|
||||||
|
cm : {},
|
||||||
|
n : -1,
|
||||||
|
default : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange : function (e) {
|
||||||
|
const { cm, n, value, prefix } = this.props;
|
||||||
|
const { text } = cm.lineInfo(n);
|
||||||
|
const updatedPrefix = `{{${prefix}`;
|
||||||
|
if(e.target?.checked)
|
||||||
|
cm.replaceRange(`,${value}`, CodeMirror.Pos(n, updatedPrefix.length), CodeMirror.Pos(n, updatedPrefix.length), '+insert');
|
||||||
|
else {
|
||||||
|
const start = text.indexOf(`,${value}`);
|
||||||
|
if(start > -1)
|
||||||
|
cm.replaceRange('', CodeMirror.Pos(n, start), CodeMirror.Pos(n, start + value.length + 1), '-delete');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function() {
|
||||||
|
const { cm, n, value, prefix, def } = this.props;
|
||||||
|
const { text } = cm.lineInfo(n);
|
||||||
|
const id = [prefix, value, n].join('-');
|
||||||
|
let className = 'widget-field widget-checkbox';
|
||||||
|
if(def) {
|
||||||
|
className += ' default';
|
||||||
|
}
|
||||||
|
return <React.Fragment>
|
||||||
|
<div className={className}>
|
||||||
|
<input type='checkbox' id={id} onChange={this.handleChange} checked={_.includes(text, `,${value}`)}/>
|
||||||
|
<label htmlFor={id}>{_.startCase(value)}</label>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Checkbox;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.widget-checkbox {
|
||||||
|
display: inline-block;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
background-color: #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 4px 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
require('./color-selector.less');
|
||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const { PATTERNS, STYLE_FN, SNIPPET_TYPE } = require('../constants');
|
||||||
|
const CodeMirror = require('../../../code-mirror');
|
||||||
|
const debounce = require('lodash.debounce');
|
||||||
|
|
||||||
|
const ColorSelector = createClass({
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
field : {},
|
||||||
|
cm : {},
|
||||||
|
n : undefined,
|
||||||
|
text : '',
|
||||||
|
def : false
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
value : ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
componentDidMount : function() {
|
||||||
|
const { field, text } = this.props;
|
||||||
|
const pattern = PATTERNS.field[field.type](field.name);
|
||||||
|
const [_, __, value] = text.match(pattern) ?? [];
|
||||||
|
this.setState({
|
||||||
|
value : value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
componentDidUpdate({ text }) {
|
||||||
|
const { field } = this.props;
|
||||||
|
if(this.props.text !== text) {
|
||||||
|
const pattern = PATTERNS.field[field.type](field.name);
|
||||||
|
const [_, __, value] = this.props.text.match(pattern) ?? [];
|
||||||
|
this.setState({
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onChange : function(e) {
|
||||||
|
const { cm, text, field, n, snippetType } = this.props;
|
||||||
|
const pattern = PATTERNS.field[field.type](field.name);
|
||||||
|
const [_, label, current] = text.match(pattern) ?? [null, field.name, ''];
|
||||||
|
let index = text.indexOf(`${label}:${current}`);
|
||||||
|
while (index !== -1 && text[index - 1] === '-') {
|
||||||
|
index = text.indexOf(`${label}:${current}`, index + 1);
|
||||||
|
}
|
||||||
|
let value = e.target.value;
|
||||||
|
if(index === -1) {
|
||||||
|
if(snippetType === SNIPPET_TYPE.INLINE) {
|
||||||
|
index = text.indexOf('}');
|
||||||
|
}
|
||||||
|
index = index === -1 ? text.length : index;
|
||||||
|
value = `,${field.name}:${value}`;
|
||||||
|
} else {
|
||||||
|
index = index + 1 + field.name.length;
|
||||||
|
}
|
||||||
|
cm.replaceRange(value, CodeMirror.Pos(n, index), CodeMirror.Pos(n, index + current.length), '+insert');
|
||||||
|
this.setState({
|
||||||
|
value : e.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
debounce : debounce((self, e)=>self.onChange(e), 300),
|
||||||
|
onChangeDebounce : function(e) {
|
||||||
|
this.setState({
|
||||||
|
value : e.target.value,
|
||||||
|
});
|
||||||
|
this.debounce(this, e);
|
||||||
|
},
|
||||||
|
render : function() {
|
||||||
|
const { field, n, text, def } = this.props;
|
||||||
|
const { value } = this.state;
|
||||||
|
const style = STYLE_FN(value);
|
||||||
|
const id = `${field?.name}-${n}`;
|
||||||
|
const pattern = PATTERNS.field[field.type](field.name);
|
||||||
|
const [_, label, __] = text.match(pattern) ?? [null, undefined, ''];
|
||||||
|
let className = 'widget-field color-selector';
|
||||||
|
if(!label) {
|
||||||
|
className += ' suggested';
|
||||||
|
}
|
||||||
|
if(def) {
|
||||||
|
className += ' default';
|
||||||
|
}
|
||||||
|
return <React.Fragment>
|
||||||
|
<div className={className}>
|
||||||
|
<label htmlFor={id}>{field.name}:</label>
|
||||||
|
<input className='color' type='color' value={value} onChange={this.onChangeDebounce}/>
|
||||||
|
<input id={id} className='text' type='text' style={style} value={value} onChange={this.onChange}/>
|
||||||
|
</div>
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = ColorSelector;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.color-selector {
|
||||||
|
.color {
|
||||||
|
height: 17px;
|
||||||
|
width: 13px;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
export const UNITS = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%'];
|
||||||
|
|
||||||
|
export const HINT_TYPE = {
|
||||||
|
VALUE : 0,
|
||||||
|
NUMBER_SUFFIX : 1
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SNIPPET_TYPE = {
|
||||||
|
BLOCK : 0,
|
||||||
|
INLINE : 1,
|
||||||
|
INJECTOR : 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FIELD_TYPE = {
|
||||||
|
TEXT : 0,
|
||||||
|
CHECKBOX : 1,
|
||||||
|
IMAGE_SELECTOR : 2,
|
||||||
|
COLOR_SELECTOR : 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
const textField = (name)=>new RegExp(`[{,;](${name}):([^};,"\\(]*\\((?!,)[^};"\\)]*\\)|"[^},;"]*"|[^},;]*)`);
|
||||||
|
export const PATTERNS = {
|
||||||
|
snippet : {
|
||||||
|
[SNIPPET_TYPE.BLOCK] : (name)=>new RegExp(`^{{${name}(?:[^a-zA-Z].*)?`),
|
||||||
|
[SNIPPET_TYPE.INLINE] : (name)=>new RegExp(`{{${name}`),
|
||||||
|
[SNIPPET_TYPE.INJECTOR] : ()=>new RegExp(`^\\!\\[(?:[a-zA-Z -]+)?\\]\\(.*\\).*{[a-zA-Z0-9:, "'-]+}$`),
|
||||||
|
},
|
||||||
|
field : {
|
||||||
|
[FIELD_TYPE.TEXT] : textField,
|
||||||
|
[FIELD_TYPE.IMAGE_SELECTOR] : (name)=>new RegExp(`{{(${name})(\\d*)`),
|
||||||
|
[FIELD_TYPE.COLOR_SELECTOR] : textField
|
||||||
|
},
|
||||||
|
collectStyles : new RegExp(`(?:([a-zA-Z-]+):(?!\\/))+`, 'g'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NUMBER_PATTERN = new RegExp(`([^-\\d]*)([-\\d]+)(${UNITS.join('|')})?(.*)`);
|
||||||
|
|
||||||
|
export const fourDigitNumberFromValue = (value)=>typeof value === 'number' ? (()=>{
|
||||||
|
const str = String(value);
|
||||||
|
return _.range(0, 4 - str.length).map(()=>'0').join('') + str;
|
||||||
|
})() : value;
|
||||||
|
|
||||||
|
const DEFAULT_WIDTH = '30px';
|
||||||
|
|
||||||
|
export const STYLE_FN = (value, extras = {})=>({
|
||||||
|
width : `calc(${value?.length ?? 0}ch + ${value?.length ? `${DEFAULT_WIDTH} / 2` : DEFAULT_WIDTH})`,
|
||||||
|
...extras
|
||||||
|
});
|
||||||