mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-25 22:43:03 +00:00
Compare commits
445 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3f885012a | ||
|
|
969eac4c5a | ||
|
|
0bc20112b2 | ||
|
|
9e137fa6a8 | ||
|
|
1f8701bea6 | ||
|
|
313ffab7c1 | ||
|
|
9f602e416f | ||
|
|
de90e2d801 | ||
|
|
3c12f1133e | ||
|
|
0625c57824 | ||
|
|
a234fdbab7 | ||
|
|
5830ee471b | ||
|
|
c35d97bf69 | ||
|
|
6c3a9370dc | ||
|
|
edb74237bd | ||
|
|
cd8496b62b | ||
|
|
af1821e697 | ||
|
|
c0a14a5618 | ||
|
|
8709545f14 | ||
|
|
e0d8846c44 | ||
|
|
cae1bc9f0a | ||
|
|
2a3aeec459 | ||
|
|
746e8f8a6a | ||
|
|
4f52cc075a | ||
|
|
8cf8c5cabb | ||
|
|
2f44c35ede | ||
|
|
62dee13881 | ||
|
|
85cad49b03 | ||
|
|
76203928d2 | ||
|
|
cfbc089207 | ||
|
|
ba693365ec | ||
|
|
b4be9bb741 | ||
|
|
e5fe6b1fd9 | ||
|
|
8d10800cb6 | ||
|
|
67099bba40 | ||
|
|
d94b274439 | ||
|
|
99c2ff15a0 | ||
|
|
0b89a895e7 | ||
|
|
64740ba528 | ||
|
|
f35634a295 | ||
|
|
9807e24e0a | ||
|
|
de43bd46a5 | ||
|
|
b9f64092b8 | ||
|
|
3f1bc02885 | ||
|
|
407d3565cd | ||
|
|
2fcccfb48f | ||
|
|
3262751fea | ||
|
|
80428fc412 | ||
|
|
66626b3427 | ||
|
|
fc3a599f90 | ||
|
|
e355621bbf | ||
|
|
9571cb0cc4 | ||
|
|
98e2d57691 | ||
|
|
6038b90798 | ||
|
|
0ca25c06f2 | ||
|
|
b85b3be1d1 | ||
|
|
78b7e07d09 | ||
|
|
cd3dcfce86 | ||
|
|
0a0e57ad38 | ||
|
|
4c565657a9 | ||
|
|
165a15e106 | ||
|
|
d27e07b620 | ||
|
|
545ea8342e | ||
|
|
0831c68e2d | ||
|
|
4ad674040d | ||
|
|
052fd4132b | ||
|
|
6c54c0a960 | ||
|
|
17704e5010 | ||
|
|
59451f47e6 | ||
|
|
f8ea556ed5 | ||
|
|
9a60d60b6d | ||
|
|
4c50f60484 | ||
|
|
e6d034b163 | ||
|
|
7034da03ab | ||
|
|
289fcc531d | ||
|
|
4e5f86e8b6 | ||
|
|
a25c7a5ccd | ||
|
|
1fb0b694f6 | ||
|
|
f93d46d2b1 | ||
|
|
4bb5e96c42 | ||
|
|
0811da79c4 | ||
|
|
17c426cc91 | ||
|
|
4b5b6e3b02 | ||
|
|
e40dbf7935 | ||
|
|
2ea853aeda | ||
|
|
215b64f5a6 | ||
|
|
239526f4ee | ||
|
|
697d4d98bf | ||
|
|
87bc27544b | ||
|
|
1bbddacbf3 | ||
|
|
cf2e6aa900 | ||
|
|
6aa9eda5fb | ||
|
|
3271eae4df | ||
|
|
5227bbc1d4 | ||
|
|
f3cdb0dfc2 | ||
|
|
9a6d409800 | ||
|
|
7f343d4634 | ||
|
|
ed933bdf92 | ||
|
|
1c4dac6a52 | ||
|
|
f734b29a00 | ||
|
|
889fcb84b2 | ||
|
|
fa4db2acfa | ||
|
|
167e5c87ae | ||
|
|
42db46fa33 | ||
|
|
257920d9db | ||
|
|
88a09415e5 | ||
|
|
a86f5232cc | ||
|
|
4fa0d701c9 | ||
|
|
2544563fea | ||
|
|
65f943bc21 | ||
|
|
ae9eef4fba | ||
|
|
539fb8365f | ||
|
|
0e65685b8e | ||
|
|
a3038868b9 | ||
|
|
80334a1649 | ||
|
|
3fe43a1f4d | ||
|
|
b257b4c0fc | ||
|
|
10cc040f16 | ||
|
|
985f4abb45 | ||
|
|
e20fa09170 | ||
|
|
b2a4054561 | ||
|
|
23e378dcdf | ||
|
|
6d6c9ab102 | ||
|
|
3f191d5b47 | ||
|
|
5362cc90f4 | ||
|
|
66ae4dc67d | ||
|
|
7d8890a61a | ||
|
|
0697673382 | ||
|
|
1646fa14e6 | ||
|
|
2f104712ed | ||
|
|
c23ba26171 | ||
|
|
fa59f8b156 | ||
|
|
bc66ad0221 | ||
|
|
08c52e4bac | ||
|
|
0741066554 | ||
|
|
24458ad2af | ||
|
|
e38975177f | ||
|
|
abebe1b3a0 | ||
|
|
ae8942d989 | ||
|
|
3cded29729 | ||
|
|
c7cfa86205 | ||
|
|
8ee832e633 | ||
|
|
c7d5d6800b | ||
|
|
e963672b65 | ||
|
|
1965218e74 | ||
|
|
3649099d00 | ||
|
|
5d7a46abfe | ||
|
|
209f0ec32a | ||
|
|
b336a6b6a3 | ||
|
|
32833459e4 | ||
|
|
0e450f724b | ||
|
|
c4ee39461e | ||
|
|
586ff67fa0 | ||
|
|
dbbc529a57 | ||
|
|
a4c80f2bbf | ||
|
|
a76715d2b3 | ||
|
|
9073435ff3 | ||
|
|
802e798492 | ||
|
|
c0405fae08 | ||
|
|
755b9dcc89 | ||
|
|
038d0c5b68 | ||
|
|
5ac65245b7 | ||
|
|
06cbcb047b | ||
|
|
60811a3295 | ||
|
|
5ca2899b54 | ||
|
|
60a372de68 | ||
|
|
01c16b4a1c | ||
|
|
2ebfd19169 | ||
|
|
f7d8c6434f | ||
|
|
936fdff871 | ||
|
|
da3460f3bf | ||
|
|
8ea58d22ba | ||
|
|
4f9ac9cad0 | ||
|
|
7dafd3ee77 | ||
|
|
4e403dd0e9 | ||
|
|
530d1bee8d | ||
|
|
e8c7bcfee9 | ||
|
|
6ed12f41f9 | ||
|
|
a754959a7d | ||
|
|
d0334c69a2 | ||
|
|
2063d12c42 | ||
|
|
5601b932c8 | ||
|
|
204b379fcb | ||
|
|
5f7abe2a8f | ||
|
|
385a22f199 | ||
|
|
1045867d73 | ||
|
|
0061e44339 | ||
|
|
29eb2fe39e | ||
|
|
f9af27b159 | ||
|
|
86f0abbfd1 | ||
|
|
e2a46c4883 | ||
|
|
389bcf1bbd | ||
|
|
b3ccbc183d | ||
|
|
362cb4f728 | ||
|
|
7bc2e5a71e | ||
|
|
890fbcc250 | ||
|
|
18ce232be9 | ||
|
|
92b3f8252e | ||
|
|
b1066a1df5 | ||
|
|
111ade7719 | ||
|
|
a60b8f9acc | ||
|
|
8b55e55277 | ||
|
|
4b57745275 | ||
|
|
4f69b1e7e0 | ||
|
|
2ac9c190a9 | ||
|
|
d04df9aeb0 | ||
|
|
6bc5edd33f | ||
|
|
6c8cdffd8f | ||
|
|
ae6ad1eaff | ||
|
|
ee3c521094 | ||
|
|
ff2268871d | ||
|
|
1d0cceda04 | ||
|
|
4e8cbf621b | ||
|
|
c9a182f1e2 | ||
|
|
df41b58441 | ||
|
|
bd681dffae | ||
|
|
f21d79d4e0 | ||
|
|
87db57239c | ||
|
|
0dbe84a91a | ||
|
|
e9adc4de82 | ||
|
|
0c0d817cc2 | ||
|
|
6c307856e1 | ||
|
|
24248c2dc6 | ||
|
|
fe374f7de0 | ||
|
|
3e9ecc2b34 | ||
|
|
bce9cc2586 | ||
|
|
7de747b264 | ||
|
|
d14466f1d3 | ||
|
|
97a0a17149 | ||
|
|
bb1fba27c2 | ||
|
|
eff2ec20b2 | ||
|
|
4cc3dd93e2 | ||
|
|
ab8dd8ae76 | ||
|
|
08db2e8492 | ||
|
|
7b2486e411 | ||
|
|
03bcbee1fc | ||
|
|
4e5daf583a | ||
|
|
eca39369de | ||
|
|
45950cb8b4 | ||
|
|
f1f82abdb4 | ||
|
|
69ea9a8f94 | ||
|
|
7eaec80d96 | ||
|
|
3ccd1ebb7b | ||
|
|
db5e4fc36c | ||
|
|
160cfcce4d | ||
|
|
ca8bd448a2 | ||
|
|
fdaf56b91a | ||
|
|
bade09c24c | ||
|
|
b54b7e317e | ||
|
|
627c52e845 | ||
|
|
53367579f4 | ||
|
|
9ebe5fa989 | ||
|
|
55c91217ab | ||
|
|
3f852ccff3 | ||
|
|
7691e4d24e | ||
|
|
46853a121c | ||
|
|
757ba04d67 | ||
|
|
fb40817826 | ||
|
|
7ca6d362f3 | ||
|
|
7e124641e7 | ||
|
|
d68c306c95 | ||
|
|
2e5cfaea1f | ||
|
|
87c9650f17 | ||
|
|
fbf02c3393 | ||
|
|
f5ca950b0b | ||
|
|
aea9b55a01 | ||
|
|
dc8c1c359d | ||
|
|
c1d8c8f341 | ||
|
|
ec07feee80 | ||
|
|
52835a5bbd | ||
|
|
18d5d96ddb | ||
|
|
1323a433d2 | ||
|
|
4aad03074c | ||
|
|
cadf0efbd8 | ||
|
|
53e7c78141 | ||
|
|
57687c83e6 | ||
|
|
d7fb137263 | ||
|
|
2493442e3f | ||
|
|
353a438849 | ||
|
|
37a4d8edca | ||
|
|
79dff10240 | ||
|
|
d7c62b2d38 | ||
|
|
5f073932ec | ||
|
|
63d0d9447f | ||
|
|
fbd772ecef | ||
|
|
452aa1feaf | ||
|
|
74d22a05d0 | ||
|
|
ac905ddf3f | ||
|
|
ff885dc6c2 | ||
|
|
01441e0610 | ||
|
|
80fa48e592 | ||
|
|
9f7ee39b09 | ||
|
|
412ac5f331 | ||
|
|
7c0e702ead | ||
|
|
02c8bf7292 | ||
|
|
b26ff9eb00 | ||
|
|
be0b1abe41 | ||
|
|
3c36b7f328 | ||
|
|
3d9704a392 | ||
|
|
d798cb1407 | ||
|
|
ac6af3cbcc | ||
|
|
e84f68aa15 | ||
|
|
af3a403971 | ||
|
|
7034f1d2d5 | ||
|
|
a56601196d | ||
|
|
451f06ca74 | ||
|
|
f202e45e49 | ||
|
|
010309b04f | ||
|
|
f6db66b28c | ||
|
|
cde611eed5 | ||
|
|
b9f2517696 | ||
|
|
5709a15f52 | ||
|
|
27d1f6daa6 | ||
|
|
ef6f8b4433 | ||
|
|
d1e8223843 | ||
|
|
85e28c201f | ||
|
|
e4a429180d | ||
|
|
4b953bc264 | ||
|
|
e28aaf4fd1 | ||
|
|
e54f3c46b7 | ||
|
|
9a62393df1 | ||
|
|
8f8b32e873 | ||
|
|
4465ecd372 | ||
|
|
b059bf68b5 | ||
|
|
c46c4a9320 | ||
|
|
cffe08b785 | ||
|
|
bac8b53984 | ||
|
|
ee10b10c2d | ||
|
|
3fb4c5bdd9 | ||
|
|
08ca061af8 | ||
|
|
fbb092562d | ||
|
|
1e47f896fe | ||
|
|
5d5bf9586e | ||
|
|
f836277f4f | ||
|
|
09e1702dd2 | ||
|
|
ac18acc2f4 | ||
|
|
3dbc90b3b8 | ||
|
|
504628f59c | ||
|
|
752430bb59 | ||
|
|
8e0e1c5946 | ||
|
|
156abe3b9e | ||
|
|
2e145e7ff1 | ||
|
|
8a5e9aa1f6 | ||
|
|
e7fe68d20c | ||
|
|
0393fcd5c4 | ||
|
|
784c0ec8f8 | ||
|
|
e86c24943f | ||
|
|
2bc3aaf796 | ||
|
|
56d3d38cad | ||
|
|
b2bcc9ef95 | ||
|
|
0fb0f4b6b0 | ||
|
|
a73b3648ae | ||
|
|
0e25793f0a | ||
|
|
3ad6f1d794 | ||
|
|
5f734096d6 | ||
|
|
7dbac5d848 | ||
|
|
eac4d4ed94 | ||
|
|
de977b3b94 | ||
|
|
3787cdf11c | ||
|
|
84bc3d0be2 | ||
|
|
6fdd415fcb | ||
|
|
0999125678 | ||
|
|
ad8d5bd8a0 | ||
|
|
5d3fe719b3 | ||
|
|
b11fe0e760 | ||
|
|
85312859d2 | ||
|
|
f8086a098b | ||
|
|
811d1347ea | ||
|
|
1bdd08f878 | ||
|
|
0023e87d54 | ||
|
|
42878ea99d | ||
|
|
16ae32b881 | ||
|
|
f3ee95f9db | ||
|
|
af950159e0 | ||
|
|
46eaad1226 | ||
|
|
4600624edf | ||
|
|
daa0832740 | ||
|
|
c9353c06da | ||
|
|
2a88d143fd | ||
|
|
39d29abb19 | ||
|
|
153802cd5b | ||
|
|
b0b39e638a | ||
|
|
39e5f4aaf4 | ||
|
|
2860b8d50a | ||
|
|
a9b2e098ac | ||
|
|
394171ebb7 | ||
|
|
0a05f37774 | ||
|
|
9f69763572 | ||
|
|
4941dbb5bd | ||
|
|
9565f1d0ba | ||
|
|
172d2a8bd6 | ||
|
|
c70857af6f | ||
|
|
a98b23411c | ||
|
|
a734a7da25 | ||
|
|
6672ec0149 | ||
|
|
6c7eb339f4 | ||
|
|
468ccd748d | ||
|
|
613b9c4405 | ||
|
|
6318b1f84c | ||
|
|
8800808925 | ||
|
|
12670d4d9b | ||
|
|
41c0b7fef9 | ||
|
|
9a4c7da375 | ||
|
|
34b4556e5c | ||
|
|
bb935f4727 | ||
|
|
2719bb8280 | ||
|
|
ebfb0d8fcf | ||
|
|
8a0d74d323 | ||
|
|
e5021259bb | ||
|
|
6791df7f75 | ||
|
|
fa8d47400f | ||
|
|
84d0010702 | ||
|
|
e610c12b2a | ||
|
|
eeef9dcc1b | ||
|
|
c17db043ba | ||
|
|
89dbd19556 | ||
|
|
e9cf7be488 | ||
|
|
48c05081a4 | ||
|
|
36fcce7f1e | ||
|
|
c0b2fb8ed9 | ||
|
|
c8f6dea1e1 | ||
|
|
a0a02f5375 | ||
|
|
396a49a16d | ||
|
|
3628fb690a | ||
|
|
58428fbcc2 | ||
|
|
b6933406ed | ||
|
|
47a4c8829c | ||
|
|
4b6fb9f595 | ||
|
|
17dd738ac1 | ||
|
|
32a5e71015 | ||
|
|
1c641e3aff | ||
|
|
9c511a6c64 | ||
|
|
0d8ad50e2a | ||
|
|
57c5f28938 | ||
|
|
a11e93ef54 | ||
|
|
a163e611be | ||
|
|
222ad3e73e | ||
|
|
5d5d0633ab | ||
|
|
535291a91a | ||
|
|
7c2663fa56 | ||
|
|
e2fe77ade7 | ||
|
|
19c13342c4 | ||
|
|
330bf20d61 | ||
|
|
22f9efd58a | ||
|
|
4bebdfda79 |
115
changelog.md
115
changelog.md
@@ -39,6 +39,121 @@ pre {
|
|||||||
## 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).
|
||||||
|
|
||||||
|
### Friday 08/09/2022 - v3.2.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai:
|
||||||
|
|
||||||
|
* [x] Fix brews not deleting from User page when removed from Google Drive externally.
|
||||||
|
|
||||||
|
Fixes issues: [#2325](https://github.com/naturalcrit/homebrewery/issues/2325)
|
||||||
|
|
||||||
|
##### G-Ambatte:
|
||||||
|
|
||||||
|
* [x] Brew Tags are now searchable on the User page
|
||||||
|
|
||||||
|
Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [#2319](https://github.com/naturalcrit/homebrewery/issues/2319), [#2334](https://github.com/naturalcrit/homebrewery/issues/2334)
|
||||||
|
|
||||||
|
* [x] Several tweaks to the User page
|
||||||
|
|
||||||
|
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Reference Links should now work inside tables
|
||||||
|
|
||||||
|
Fixes issues: [#2307](https://github.com/naturalcrit/homebrewery/issues/2307)
|
||||||
|
|
||||||
|
##### Jeddai:
|
||||||
|
|
||||||
|
* [x] Fix printing from `/new` not working
|
||||||
|
|
||||||
|
Fixes issues: [#1789](https://github.com/naturalcrit/homebrewery/issues/1789), [#1806](https://github.com/naturalcrit/homebrewery/issues/1806)
|
||||||
|
|
||||||
|
* [x] Fix broken snippet buttons on `/new`
|
||||||
|
|
||||||
|
Fixes issues: [#2311](https://github.com/naturalcrit/homebrewery/issues/2311)
|
||||||
|
|
||||||
|
##### G-Ambatte:
|
||||||
|
|
||||||
|
* [x] Several small tweaks to the User page
|
||||||
|
|
||||||
|
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] The V3 renderer is now the default for new brews.
|
||||||
|
|
||||||
|
* [x] Small tweaks to the spacing around the `classTable` style
|
||||||
|
|
||||||
|
##### Jeddai:
|
||||||
|
|
||||||
|
* [x] Brew transfers between Homebrewery and Google Drive now keep the same share and edit links! Metadata is now also kept across transfers.
|
||||||
|
|
||||||
|
Fixes issues: [#1838](https://github.com/naturalcrit/homebrewery/issues/1838)
|
||||||
|
|
||||||
|
* [x] Brews can now be labeled with tags; these will be searchable on the My Brews page in a future update.
|
||||||
|
|
||||||
|
Fixes issues: [#758](https://github.com/naturalcrit/homebrewery/issues/758)
|
||||||
|
|
||||||
|
##### Jlgraves:
|
||||||
|
|
||||||
|
* [x] Small tweaks to the `ClassFeature` snippet
|
||||||
|
|
||||||
|
Fixes issues: [#2215](https://github.com/naturalcrit/homebrewery/issues/2215)
|
||||||
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
### Thursday 09/06/2022 - v3.1.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild:
|
||||||
|
|
||||||
|
* [x] Fixed class table decorations appearing on top of the table in PDF output.
|
||||||
|
|
||||||
|
Fixes issues: [#1784](https://github.com/naturalcrit/homebrewery/issues/1784)
|
||||||
|
|
||||||
|
* [x] Fix bottom decoration on half class tables disappearing when the table is too short.
|
||||||
|
|
||||||
|
Fixes issues: [#2202](https://github.com/naturalcrit/homebrewery/issues/2202)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 06/06/2022 - v3.1.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte:
|
||||||
|
|
||||||
|
* [x] "Jump to Preview/Editor" buttons added to the divider bar. Easily sync between the editor and preview panels!
|
||||||
|
|
||||||
|
Fixes issues: [#1756](https://github.com/naturalcrit/homebrewery/issues/1756)
|
||||||
|
|
||||||
|
* [x] Speedups to the user page for users with large and/or many brews.
|
||||||
|
|
||||||
|
Fixes issues: [#2147](https://github.com/naturalcrit/homebrewery/issues/2147)
|
||||||
|
|
||||||
|
* [x] Search text on the user page is saved to the URL for easy bookmarking in your browser
|
||||||
|
|
||||||
|
Fixes issues: [#1858](https://github.com/naturalcrit/homebrewery/issues/1858)
|
||||||
|
|
||||||
|
* [x] Added easy login system for offline installs.
|
||||||
|
|
||||||
|
Fixes issues: [#269](https://github.com/naturalcrit/homebrewery/issues/269)
|
||||||
|
|
||||||
|
* [x] New **THUMBNAIL** option in the {{fa,fa-info-circle}} **Properties** menu. This image will show up in social media links.
|
||||||
|
|
||||||
|
Fixes issues: [#820](https://github.com/naturalcrit/homebrewery/issues/820)
|
||||||
|
}}
|
||||||
|
|
||||||
### Wednesday 27/03/2022 - v3.0.8
|
### Wednesday 27/03/2022 - v3.0.8
|
||||||
{{taskList
|
{{taskList
|
||||||
* [x] Style updates to user page.
|
* [x] Style updates to user page.
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const BrewRenderer = createClass({
|
|||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
<Frame 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}>
|
||||||
<div className={'brewRenderer'}
|
<div className={'brewRenderer'}
|
||||||
|
|||||||
@@ -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_notification09-9-21';
|
const DISMISS_KEY = 'dismiss_notification08-27-22';
|
||||||
|
|
||||||
const NotificationPopup = createClass({
|
const NotificationPopup = createClass({
|
||||||
displayName : 'NotificationPopup',
|
displayName : 'NotificationPopup',
|
||||||
@@ -22,45 +22,45 @@ const NotificationPopup = createClass({
|
|||||||
},
|
},
|
||||||
notifications : {
|
notifications : {
|
||||||
psa : function(){
|
psa : function(){
|
||||||
return <li key='psa'>
|
return (
|
||||||
<em>V3.0.0 Released!</em> <br />
|
<>
|
||||||
After a long and bumpy road, we decided it was high time we finally release version 3 of the homebrewery into the wild. You can check out a
|
<li key='psa'>
|
||||||
brief overview and see how to opt-in to the new features here:
|
<em>V3.2.0 Released!</em> <br />
|
||||||
<a target='_blank' href='https://homebrewery.naturalcrit.com/v3_preview'>V3 Welcome Page</a> and
|
We are happy to announce that after nearly a year of use by our many users,
|
||||||
<a target='_blank' href='https://homebrewery.naturalcrit.com/changelog'>the Changelog</a>.
|
we are making the V3 render mode the default setting for all new brews.
|
||||||
<br /><br />
|
This mode has become quite popular, and has proven to be stable and powerful.
|
||||||
<em>BE WARNED:</em> As we continue to develop V3, expect small tweaks in the styling, fonts, and snippets; your brews may look slightly
|
Of course, we will always keep the option to use the Legacy renderer for any
|
||||||
different from day-to-day. All of your old documents will continue to work as normal; we are not touching them. If you don't want to deal
|
brew, which can still be accessed from the Properties menu.
|
||||||
with the possibility of slight formatting changes, you may choose to stick with the Legacy renderer on any of your brews for as long as you like.
|
</li>
|
||||||
<br /><br />
|
|
||||||
With this in mind, if you still wish to try out V3, you can opt-in any of your brews to the the V3 renderer.
|
<li key='stubs'>
|
||||||
This will likely break much of your formatting as a lot of the Markdown code has been updated, and starting from scratch may be cleaner.
|
<em>Change to Google Drive Storage!</em> <br />
|
||||||
(Don't worry, you can always change the renderer back to Legacy for any brew at any time).
|
We have made a change to the process of tranferring brews between Google
|
||||||
</li>;
|
Drive and the Homebrewery storage. Starting now, any time a brew is
|
||||||
},
|
transferred, it will keep the same links instead of generating new ones!
|
||||||
refreshGoogle : function (){
|
We hope this change will help reduce issues where people "lost" their work
|
||||||
return <li key='refreshGoogle'>
|
by trying to visit old links.
|
||||||
<em>Refresh your Google Drive Credentials!</em> <br />
|
</li>
|
||||||
Currently a lot of people are striking issues with their Google credentials expiring, which happens one year after the last sign in via
|
|
||||||
Google. This can cause errors when trying to save your brews. If this happens, simply visit the
|
<li key='googleDriveFolder'>
|
||||||
<a target='_blank' href='https://www.naturalcrit.com/login'>
|
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
||||||
logout page
|
We have had several reports of users losing their brews, not realizing
|
||||||
</a>
|
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
||||||
, sign out, and then sign back in "with Google" to refresh your credentials. See
|
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
||||||
<a target='_blank' href='https://github.com/naturalcrit/homebrewery/discussions/1580'>
|
We cannot help you recover files that you have deleted from your own
|
||||||
this discussion on Github
|
Google Drive.
|
||||||
</a> for more details.
|
</li>
|
||||||
</li>;
|
|
||||||
},
|
<li key='faq'>
|
||||||
faq : function(){
|
<em>Protect your work! </em> <br />
|
||||||
return <li key='faq'>
|
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
||||||
<em>Protect your work! </em> <br />
|
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||||
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
See the FAQ
|
||||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
</a> to learn how to avoid losing your work!
|
||||||
See the FAQ
|
</li>
|
||||||
</a> to learn how to avoid losing your work!
|
</>
|
||||||
</li>;
|
);
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
checkNotifications : function(){
|
checkNotifications : function(){
|
||||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
||||||
|
|||||||
@@ -61,8 +61,14 @@ const Editor = createClass({
|
|||||||
window.removeEventListener('resize', this.updateEditorSize);
|
window.removeEventListener('resize', this.updateEditorSize);
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : function() {
|
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
|
if(prevProps.moveBrew !== this.props.moveBrew) {
|
||||||
|
this.brewJump();
|
||||||
|
};
|
||||||
|
if(prevProps.moveSource !== this.props.moveSource) {
|
||||||
|
this.sourceJump();
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
@@ -90,15 +96,20 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleViewChange : function(newView){
|
handleViewChange : function(newView){
|
||||||
|
this.props.setMoveArrows(newView === 'text');
|
||||||
this.setState({
|
this.setState({
|
||||||
view : newView
|
view : newView
|
||||||
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
|
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
|
||||||
},
|
},
|
||||||
|
|
||||||
getCurrentPage : function(){
|
getCurrentPage : function(){
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, this.cursorPosition.line + 1);
|
const lines = this.props.brew.text.split('\n').slice(0, this.refs.codeEditor.getCursorPosition().line + 1);
|
||||||
return _.reduce(lines, (r, line)=>{
|
return _.reduce(lines, (r, line)=>{
|
||||||
if(line.indexOf('\\page') !== -1) r++;
|
if(
|
||||||
|
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
|
||||||
|
||
|
||||||
|
(this.props.renderer == 'V3' && line.match(/^\\page$/))
|
||||||
|
) r++;
|
||||||
return r;
|
return r;
|
||||||
}, 1);
|
}, 1);
|
||||||
},
|
},
|
||||||
@@ -120,6 +131,7 @@ const Editor = createClass({
|
|||||||
//reset custom line styles
|
//reset custom line styles
|
||||||
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
||||||
codeMirror.removeLineClass(lineNumber, 'text');
|
codeMirror.removeLineClass(lineNumber, 'text');
|
||||||
|
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||||
|
|
||||||
// Styling for \page breaks
|
// Styling for \page breaks
|
||||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||||
@@ -174,9 +186,76 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
brewJump : function(){
|
brewJump : function(targetPage=this.getCurrentPage()){
|
||||||
const currentPage = this.getCurrentPage();
|
if(!window) return;
|
||||||
window.location.hash = `p${currentPage}`;
|
// console.log(`Scroll to: p${targetPage}`);
|
||||||
|
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
||||||
|
const currentPos = brewRenderer.scrollTop;
|
||||||
|
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
||||||
|
const interimPos = targetPos >= 0 ? -30 : 30;
|
||||||
|
|
||||||
|
const bounceDelay = 100;
|
||||||
|
const scrollDelay = 500;
|
||||||
|
|
||||||
|
if(!this.throttleBrewMove) {
|
||||||
|
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
|
||||||
|
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
|
||||||
|
setTimeout(()=>{
|
||||||
|
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
|
||||||
|
}, bounceDelay);
|
||||||
|
}, scrollDelay, { leading: true, trailing: false });
|
||||||
|
};
|
||||||
|
this.throttleBrewMove(currentPos, interimPos, targetPos);
|
||||||
|
|
||||||
|
// const hashPage = (page != 1) ? `p${page}` : '';
|
||||||
|
// window.location.hash = hashPage;
|
||||||
|
},
|
||||||
|
|
||||||
|
sourceJump : function(targetLine=null){
|
||||||
|
if(this.isText()) {
|
||||||
|
if(targetLine == null) {
|
||||||
|
targetLine = 0;
|
||||||
|
|
||||||
|
const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page');
|
||||||
|
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height;
|
||||||
|
|
||||||
|
let currentPage = 1;
|
||||||
|
for (const page of pageCollection) {
|
||||||
|
if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) {
|
||||||
|
currentPage = parseInt(page.id.slice(1)) || 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
||||||
|
const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-1).join(textSplit);
|
||||||
|
const textPosition = textString.length;
|
||||||
|
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
|
||||||
|
|
||||||
|
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
||||||
|
|
||||||
|
let currentY = this.refs.codeEditor.codeMirror.getScrollInfo().top;
|
||||||
|
let targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
|
|
||||||
|
//Scroll 1/10 of the way every 10ms until 1px off.
|
||||||
|
const incrementalScroll = setInterval(()=>{
|
||||||
|
currentY += (targetY - currentY) / 10;
|
||||||
|
this.refs.codeEditor.codeMirror.scrollTo(null, currentY);
|
||||||
|
|
||||||
|
// Update target: target height is not accurate until within +-10 lines of the visible window
|
||||||
|
if(Math.abs(targetY - currentY > 100))
|
||||||
|
targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
|
|
||||||
|
// End when close enough
|
||||||
|
if(Math.abs(targetY - currentY) < 1) {
|
||||||
|
this.refs.codeEditor.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||||
|
this.refs.codeEditor.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
||||||
|
this.refs.codeEditor.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
||||||
|
clearInterval(incrementalScroll);
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//Called when there are changes to the editor's dimensions
|
//Called when there are changes to the editor's dimensions
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
require('./metadataEditor.less');
|
require('./metadataEditor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
|
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||||
|
|
||||||
const MetadataEditor = createClass({
|
const MetadataEditor = createClass({
|
||||||
displayName : 'MetadataEditor',
|
displayName : 'MetadataEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -15,7 +19,7 @@ const MetadataEditor = createClass({
|
|||||||
editId : null,
|
editId : null,
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
tags : '',
|
tags : [],
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : [],
|
systems : [],
|
||||||
@@ -25,10 +29,28 @@ const MetadataEditor = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState : function(){
|
||||||
|
return {
|
||||||
|
showThumbnail : true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleThumbnailDisplay : function(){
|
||||||
|
this.setState({
|
||||||
|
showThumbnail : !this.state.showThumbnail
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderThumbnail : function(){
|
||||||
|
if(!this.state.showThumbnail) return;
|
||||||
|
return <img className='thumbnail-preview' src={this.props.metadata.thumbnail || homebreweryThumbnail}></img>;
|
||||||
|
},
|
||||||
|
|
||||||
handleFieldChange : function(name, e){
|
handleFieldChange : function(name, e){
|
||||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
this.props.onChange({
|
||||||
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
handleSystem : function(system, e){
|
handleSystem : function(system, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
@@ -45,9 +67,10 @@ const MetadataEditor = createClass({
|
|||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
handlePublish : function(val){
|
handlePublish : function(val){
|
||||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
this.props.onChange({
|
||||||
|
...this.props.metadata,
|
||||||
published : val
|
published : val
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDelete : function(){
|
handleDelete : function(){
|
||||||
@@ -59,7 +82,7 @@ const MetadataEditor = createClass({
|
|||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.delete(`/api/${this.props.metadata.googleId}${this.props.metadata.editId}`)
|
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end(function(err, res){
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
@@ -142,8 +165,8 @@ const MetadataEditor = createClass({
|
|||||||
V3
|
V3
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<a href='/v3_preview' target='_blank' rel='noopener noreferrer'>
|
<a href='/legacy' target='_blank' rel='noopener noreferrer'>
|
||||||
Click here for a quick intro to V3!
|
Click here to see the demo page for the old Legacy renderer!
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
@@ -162,13 +185,23 @@ const MetadataEditor = createClass({
|
|||||||
<textarea value={this.props.metadata.description} className='value'
|
<textarea value={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 tags'>
|
<label>thumbnail</label>
|
||||||
<label>tags</label>
|
<input type='text'
|
||||||
<textarea value={this.props.metadata.tags}
|
value={this.props.metadata.thumbnail}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)} />
|
placeholder='my.thumbnail.url'
|
||||||
|
className='value'
|
||||||
|
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||||
|
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||||
|
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||||
|
</button>
|
||||||
|
{this.renderThumbnail()}
|
||||||
</div>
|
</div>
|
||||||
*/}
|
|
||||||
|
<StringArrayEditor label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
||||||
|
placeholder='add tag' unique={true}
|
||||||
|
values={this.props.metadata.tags}
|
||||||
|
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderAuthors()}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
.metadataEditor{
|
.metadataEditor{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -24,6 +25,33 @@
|
|||||||
flex : 1 1 auto;
|
flex : 1 1 auto;
|
||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
}
|
}
|
||||||
|
&.thumbnail{
|
||||||
|
height : 1.4em;
|
||||||
|
label{
|
||||||
|
line-height: 2.0em;
|
||||||
|
}
|
||||||
|
.value{
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
button{
|
||||||
|
border: 1px solid #999;
|
||||||
|
color: white;
|
||||||
|
padding: 0px 5px;
|
||||||
|
background-color: black;
|
||||||
|
&:hover{
|
||||||
|
background-color: #777;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.thumbnail-preview{
|
||||||
|
position : relative;
|
||||||
|
width : 80px;
|
||||||
|
height : min-content;
|
||||||
|
border : 2px solid white;
|
||||||
|
margin-left : 5px;
|
||||||
|
max-height : 115px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.description.field textarea.value{
|
.description.field textarea.value{
|
||||||
resize : none;
|
resize : none;
|
||||||
@@ -81,4 +109,86 @@
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field .list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#groupedIcon {
|
||||||
|
#backgroundColors;
|
||||||
|
display: inline-block;
|
||||||
|
height: ~"calc(100% + 0.6em)";
|
||||||
|
position: relative;
|
||||||
|
top: -0.3em;
|
||||||
|
right: -0.3em;
|
||||||
|
cursor: pointer;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
i {
|
||||||
|
position: relative;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-right: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 0.5em 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: .5em;
|
||||||
|
font-size: .9em;
|
||||||
|
margin: 2px;
|
||||||
|
padding: .3em;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
#groupedIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
height: ~"calc(.9em + 4px + .6em)";
|
||||||
|
|
||||||
|
input {
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:last-child {
|
||||||
|
border-radius: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
width: 7.5vw;
|
||||||
|
min-width: 75px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invalid:focus {
|
||||||
|
background-color: pink;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
#groupedIcon;
|
||||||
|
height: 97%;
|
||||||
|
font-size: .8em;
|
||||||
|
right: 1px;
|
||||||
|
top: -.54em;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 1.125em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ module.exports = function(classname){
|
|||||||
classname = classname.toLowerCase();
|
classname = classname.toLowerCase();
|
||||||
|
|
||||||
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
||||||
|
const spellSkill = _.sample(['Wisdom', 'Charisma', 'Intelligence']);
|
||||||
|
|
||||||
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
||||||
const skillList = ['Acrobatics', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
const skillList = ['Acrobatics', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
||||||
@@ -27,11 +28,19 @@ module.exports = function(classname){
|
|||||||
|
|
||||||
**Armor:** :: ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}
|
**Armor:** :: ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}
|
||||||
**Weapons:** :: ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}
|
**Weapons:** :: ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}
|
||||||
**Tools:** :: ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}
|
**Tools:** :: ${_.sampleSize(['Artisan\'s tools', 'one musical instrument', 'Thieves\' tools'], _.random(0, 2)).join(', ') || 'None'}
|
||||||
|
|
||||||
**Saving Throws:** :: ${_.sampleSize(abilityList, 2).join(', ')}
|
**Saving Throws:** :: ${_.sampleSize(abilityList, 2).join(', ')}
|
||||||
**Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}
|
**Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}
|
||||||
|
|
||||||
|
#### Spellcasting Ability
|
||||||
|
|
||||||
|
{{text-align:center
|
||||||
|
**Spell save DC**:: = ${_.sample([6, 8, 10])} + your proficiency bonus + your ${spellSkill} modifier
|
||||||
|
|
||||||
|
**Spell attack modifier**:: = your proficiency bonus + your ${spellSkill} modifier
|
||||||
|
}}
|
||||||
|
|
||||||
#### Equipment
|
#### Equipment
|
||||||
You start with the following equipment, in addition to the equipment granted by your background:
|
You start with the following equipment, in addition to the equipment granted by your background:
|
||||||
- *(a)* a martial weapon and a shield or *(b)* two martial weapons
|
- *(a)* a martial weapon and a shield or *(b)* two martial weapons
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ module.exports = function(classname){
|
|||||||
classname = classname.toLowerCase();
|
classname = classname.toLowerCase();
|
||||||
|
|
||||||
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
||||||
|
const spellSkill = _.sample(['Wisdom', 'Charisma', 'Intelligence']);
|
||||||
|
|
||||||
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
||||||
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
||||||
@@ -26,12 +27,21 @@ module.exports = function(classname){
|
|||||||
'___',
|
'___',
|
||||||
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
|
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
|
||||||
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
|
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||||
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
`- **Tools:** ${_.sampleSize(['Artisan\'s tools', 'one musical instrument', 'Thieves\' tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||||
'',
|
'',
|
||||||
'___',
|
'___',
|
||||||
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
||||||
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
||||||
'',
|
'',
|
||||||
|
'#### Spellcasting Ability',
|
||||||
|
'',
|
||||||
|
`<div style=text-align:center>`,
|
||||||
|
'___',
|
||||||
|
`- **Spell save DC** = ${_.sample([6, 8, 10])} + your proficiency bonus + your ${spellSkill} modifier`,
|
||||||
|
'',
|
||||||
|
`- **Spell attack modifier** = your proficiency bonus + your ${spellSkill} modifier`,
|
||||||
|
`</div>`,
|
||||||
|
'',
|
||||||
'#### Equipment',
|
'#### Equipment',
|
||||||
'You start with the following equipment, in addition to the equipment granted by your background:',
|
'You start with the following equipment, in addition to the equipment granted by your background:',
|
||||||
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',
|
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',
|
||||||
|
|||||||
142
client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx
Normal file
142
client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const StringArrayEditor = createClass({
|
||||||
|
displayName : 'StringArrayEditor',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
label : '',
|
||||||
|
values : [],
|
||||||
|
valuePatterns : null,
|
||||||
|
placeholder : '',
|
||||||
|
unique : false,
|
||||||
|
cannotEdit : [],
|
||||||
|
onChange : ()=>{}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
valueContext : !!this.props.values ? this.props.values.map((value)=>({
|
||||||
|
value,
|
||||||
|
editing : false
|
||||||
|
})) : [],
|
||||||
|
temporaryValue : '',
|
||||||
|
updateValue : ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate : function(prevProps) {
|
||||||
|
if(!_.eq(this.props.values, prevProps.values)) {
|
||||||
|
this.setState({
|
||||||
|
valueContext : this.props.values ? this.props.values.map((newValue)=>({
|
||||||
|
value : newValue,
|
||||||
|
editing : this.state.valueContext.find(({ value })=>value === newValue)?.editing || false
|
||||||
|
})) : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange : function(value) {
|
||||||
|
this.props.onChange({
|
||||||
|
target : {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addValue : function(value){
|
||||||
|
this.handleChange(_.uniq([...this.props.values, value]));
|
||||||
|
this.setState({
|
||||||
|
temporaryValue : ''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeValue : function(index){
|
||||||
|
this.handleChange(this.props.values.filter((_, i)=>i !== index));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue : function(value, index){
|
||||||
|
const valueContext = this.state.valueContext;
|
||||||
|
valueContext[index].value = value;
|
||||||
|
valueContext[index].editing = false;
|
||||||
|
this.handleChange(valueContext.map((context)=>context.value));
|
||||||
|
this.setState({ valueContext, updateValue: '' });
|
||||||
|
},
|
||||||
|
|
||||||
|
editValue : function(index){
|
||||||
|
if(!!this.props.cannotEdit && this.props.cannotEdit.includes(this.props.values[index])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valueContext = this.state.valueContext.map((context, i)=>{
|
||||||
|
context.editing = index === i;
|
||||||
|
return context;
|
||||||
|
});
|
||||||
|
this.setState({ valueContext, updateValue: this.props.values[index] });
|
||||||
|
},
|
||||||
|
|
||||||
|
valueIsValid : function(value, index) {
|
||||||
|
const values = _.clone(this.props.values);
|
||||||
|
if(index !== undefined) {
|
||||||
|
values.splice(index, 1);
|
||||||
|
}
|
||||||
|
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||||
|
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||||
|
return matchesPatterns && uniqueIfSet;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleValueInputKeyDown : function(event, index) {
|
||||||
|
if(event.key === 'Enter') {
|
||||||
|
if(this.valueIsValid(event.target.value, index)) {
|
||||||
|
if(index !== undefined) {
|
||||||
|
this.updateValue(event.target.value, index);
|
||||||
|
} else {
|
||||||
|
this.addValue(event.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(event.key === 'Escape') {
|
||||||
|
this.closeEditInput(index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeEditInput : function(index) {
|
||||||
|
const valueContext = this.state.valueContext;
|
||||||
|
valueContext[index].editing = false;
|
||||||
|
this.setState({ valueContext, updateValue: '' });
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function() {
|
||||||
|
const valueElements = Object.values(this.state.valueContext).map((context, i)=>context.editing
|
||||||
|
? <React.Fragment key={i}>
|
||||||
|
<div className='input-group'>
|
||||||
|
<input type='text' className={`value ${this.valueIsValid(this.state.updateValue, i) ? '' : 'invalid'}`} autoFocus placeholder={this.props.placeholder}
|
||||||
|
value={this.state.updateValue}
|
||||||
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e, i)}
|
||||||
|
onChange={(e)=>this.setState({ updateValue: e.target.value })}/>
|
||||||
|
{<div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.closeEditInput(i); }}><i className='fa fa-undo fa-fw'/></div>}
|
||||||
|
{this.valueIsValid(this.state.updateValue, i) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
: <div className='badge' key={i} onClick={()=>this.editValue(i)}>{context.value}
|
||||||
|
{!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null : <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.removeValue(i); }}><i className='fa fa-times fa-fw'/></div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <div className='field values'>
|
||||||
|
<label>{this.props.label}</label>
|
||||||
|
<div className='list'>
|
||||||
|
{valueElements}
|
||||||
|
<div className='input-group'>
|
||||||
|
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||||
|
value={this.state.temporaryValue}
|
||||||
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||||
|
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||||
|
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = StringArrayEditor;
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
require('./homebrew.less');
|
require('./homebrew.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const { StaticRouter:Router, Switch, Route } = require('react-router-dom');
|
const { StaticRouter:Router } = require('react-router-dom/server');
|
||||||
const queryString = require('query-string');
|
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
|
||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||||
@@ -12,6 +12,23 @@ 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 WithRoute = (props)=>{
|
||||||
|
const params = useParams();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const queryParams = {};
|
||||||
|
for (const [key, value] of searchParams?.entries() || []) {
|
||||||
|
queryParams[key] = value;
|
||||||
|
}
|
||||||
|
const Element = props.el;
|
||||||
|
const allProps = {
|
||||||
|
...props,
|
||||||
|
...params,
|
||||||
|
query : queryParams,
|
||||||
|
el : undefined
|
||||||
|
};
|
||||||
|
return <Element {...allProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
const Homebrew = createClass({
|
const Homebrew = createClass({
|
||||||
displayName : 'Homebrewery',
|
displayName : 'Homebrewery',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -33,33 +50,34 @@ const Homebrew = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function(){
|
getInitialState : function() {
|
||||||
global.version = this.props.version;
|
|
||||||
global.account = this.props.account;
|
global.account = this.props.account;
|
||||||
|
global.version = this.props.version;
|
||||||
global.enable_v3 = this.props.enable_v3;
|
global.enable_v3 = this.props.enable_v3;
|
||||||
|
global.config = this.props.config;
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function (){
|
render : function (){
|
||||||
return (
|
return <Router location={this.props.url}>
|
||||||
<Router location={this.props.url}>
|
<div className='homebrew'>
|
||||||
<div className='homebrew'>
|
<Routes>
|
||||||
<Switch>
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||||
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||||
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||||
<Route path='/new' exact component={(routeProps)=><NewPage />}/>
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||||
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
|
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||||
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
|
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||||
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/faq' exact component={()=><SharePage brew={this.props.brew} />}/>
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/v3_preview' exact component={()=><HomePage brew={this.props.brew} />}/>
|
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
|
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
</Switch>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const request = require('superagent');
|
||||||
|
|
||||||
const Account = createClass({
|
const Account = createClass({
|
||||||
displayName : 'AccountNavItem',
|
displayName : 'AccountNavItem',
|
||||||
@@ -23,12 +24,42 @@ const Account = createClass({
|
|||||||
// Reset divider position
|
// Reset divider position
|
||||||
window.localStorage.removeItem('naturalcrit-pane-split');
|
window.localStorage.removeItem('naturalcrit-pane-split');
|
||||||
// Clear login cookie
|
// Clear login cookie
|
||||||
document.cookie = `nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;samesite=lax;${window.domain ? `domain=${window.domain}` : ''}`;
|
let domain = '';
|
||||||
|
if(window.location?.hostname) {
|
||||||
|
let domainArray = window.location.hostname.split('.');
|
||||||
|
if(domainArray.length > 2){
|
||||||
|
domainArray = [''].concat(domainArray.slice(-2));
|
||||||
|
}
|
||||||
|
domain = domainArray.join('.');
|
||||||
|
}
|
||||||
|
document.cookie = `nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;samesite=lax;${domain ? `domain=${domain}` : ''}`;
|
||||||
window.location = '/';
|
window.location = '/';
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
localLogin : async function(){
|
||||||
|
const username = prompt('Enter username:');
|
||||||
|
if(!username) {return;}
|
||||||
|
|
||||||
|
const expiry = new Date;
|
||||||
|
expiry.setFullYear(expiry.getFullYear() + 1);
|
||||||
|
|
||||||
|
const token = await request.post('/local/login')
|
||||||
|
.send({ username })
|
||||||
|
.then((response)=>{
|
||||||
|
return response.body;
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.warn(err);
|
||||||
|
});
|
||||||
|
if(!token) return;
|
||||||
|
|
||||||
|
document.cookie = `nc_session=${token};expires=${expiry};path=/;samesite=lax;${window.domain ? `domain=${window.domain}` : ''}`;
|
||||||
|
window.location.reload(true);
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
|
// Logged in
|
||||||
if(global.account){
|
if(global.account){
|
||||||
return <Nav.dropdown>
|
return <Nav.dropdown>
|
||||||
<Nav.item
|
<Nav.item
|
||||||
@@ -56,6 +87,16 @@ const Account = createClass({
|
|||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logged out
|
||||||
|
// LOCAL ONLY
|
||||||
|
if(global.config.local) {
|
||||||
|
return <Nav.item color='teal' icon='fas fa-sign-in-alt' onClick={this.localLogin}>
|
||||||
|
login
|
||||||
|
</Nav.item>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logged out
|
||||||
|
// Production site
|
||||||
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
||||||
login
|
login
|
||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
||||||
}
|
}
|
||||||
.item{
|
.item{
|
||||||
#backgroundColors;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : relative;
|
position : relative;
|
||||||
display : block;
|
display : block;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const BrewItem = createClass({
|
|||||||
brew : {
|
brew : {
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
|
authors : [],
|
||||||
authors : []
|
stubbed : true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -31,7 +31,7 @@ const BrewItem = createClass({
|
|||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
request.delete(`/api/${this.props.brew.googleId}${this.props.brew.editId}`)
|
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end(function(err, res){
|
||||||
location.reload();
|
location.reload();
|
||||||
@@ -50,7 +50,7 @@ const BrewItem = createClass({
|
|||||||
if(!this.props.brew.editId) return;
|
if(!this.props.brew.editId) return;
|
||||||
|
|
||||||
let editLink = this.props.brew.editId;
|
let editLink = this.props.brew.editId;
|
||||||
if(this.props.brew.googleId) {
|
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
||||||
editLink = this.props.brew.googleId + editLink;
|
editLink = this.props.brew.googleId + editLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ const BrewItem = createClass({
|
|||||||
if(!this.props.brew.shareId) return;
|
if(!this.props.brew.shareId) return;
|
||||||
|
|
||||||
let shareLink = this.props.brew.shareId;
|
let shareLink = this.props.brew.shareId;
|
||||||
if(this.props.brew.googleId) {
|
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = this.props.brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ const BrewItem = createClass({
|
|||||||
if(!this.props.brew.shareId) return;
|
if(!this.props.brew.shareId) return;
|
||||||
|
|
||||||
let shareLink = this.props.brew.shareId;
|
let shareLink = this.props.brew.shareId;
|
||||||
if(this.props.brew.googleId) {
|
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
||||||
shareLink = this.props.brew.googleId + shareLink;
|
shareLink = this.props.brew.googleId + shareLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ const BrewItem = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderGoogleDriveIcon : function(){
|
||||||
if(!this.props.brew.gDrive) return;
|
if(!this.props.brew.googleId) return;
|
||||||
|
|
||||||
return <span>
|
return <span>
|
||||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||||
@@ -95,6 +95,9 @@ const BrewItem = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
const brew = this.props.brew;
|
const brew = this.props.brew;
|
||||||
|
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
||||||
|
brew.tags = brew.tags?.filter(tag => tag); //remove tags that are empty strings
|
||||||
|
}
|
||||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||||
|
|
||||||
return <div className='brewItem'>
|
return <div className='brewItem'>
|
||||||
@@ -104,8 +107,19 @@ const BrewItem = createClass({
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className='info'>
|
<div className='info'>
|
||||||
<span title={`Authors:\n${brew.authors.join('\n')}`}>
|
|
||||||
<i className='fas fa-user'/> {brew.authors.join(', ')}
|
{brew.tags?.length ? <>
|
||||||
|
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
|
||||||
|
<i className='fas fa-tags'/>
|
||||||
|
{brew.tags.map((tag, idx)=>{
|
||||||
|
let matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
|
return <span className={matches[1]}>{matches[2]}</span>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</> : <></>
|
||||||
|
}
|
||||||
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
|
<i className='fas fa-user'/> {brew.authors?.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
|
|||||||
@@ -29,13 +29,23 @@
|
|||||||
.info{
|
.info{
|
||||||
position: initial;
|
position: initial;
|
||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
font-family : ScalySans;
|
font-family : ScalySansRemake;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
&>span{
|
&>span{
|
||||||
margin-right : 12px;
|
margin-right : 12px;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.brewTags span {
|
||||||
|
background-color: #c8ac6e3b;
|
||||||
|
margin: 2px;
|
||||||
|
padding: 2px;
|
||||||
|
border: 1px solid #c8ac6e;
|
||||||
|
border-radius: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
.links{
|
.links{
|
||||||
opacity : 1;
|
opacity : 1;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./listPage.less');
|
require('./listPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
@@ -6,6 +7,11 @@ const moment = require('moment');
|
|||||||
|
|
||||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||||
|
|
||||||
|
const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
|
||||||
|
|
||||||
|
const DEFAULT_SORT_TYPE = 'alpha';
|
||||||
|
const DEFAULT_SORT_DIR = 'asc';
|
||||||
|
|
||||||
const ListPage = createClass({
|
const ListPage = createClass({
|
||||||
displayName : 'ListPage',
|
displayName : 'ListPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -21,13 +27,56 @@ const ListPage = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
|
// HIDE ALL GROUPS UNTIL LOADED
|
||||||
|
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||||
|
brewGroup.visible = false;
|
||||||
|
return brewGroup;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sortType : 'alpha',
|
filterString : this.props.query?.filter || '',
|
||||||
sortDir : 'asc',
|
sortType : this.props.query?.sort || null,
|
||||||
filterString : ''
|
sortDir : this.props.query?.dir || null,
|
||||||
|
query : this.props.query,
|
||||||
|
brewCollection : brewCollection
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
componentDidMount : function() {
|
||||||
|
// SAVE TO LOCAL STORAGE WHEN LEAVING PAGE
|
||||||
|
window.onbeforeunload = this.saveToLocalStorage;
|
||||||
|
|
||||||
|
// LOAD FROM LOCAL STORAGE
|
||||||
|
if(typeof window !== 'undefined') {
|
||||||
|
const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
|
||||||
|
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
|
||||||
|
this.updateUrl(this.state.filterString, newSortType, newSortDir);
|
||||||
|
|
||||||
|
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||||
|
brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
|
||||||
|
return brewGroup;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
brewCollection : brewCollection,
|
||||||
|
sortType : newSortType,
|
||||||
|
sortDir : newSortDir
|
||||||
|
});
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount : function() {
|
||||||
|
window.onbeforeunload = function(){};
|
||||||
|
},
|
||||||
|
|
||||||
|
saveToLocalStorage : function() {
|
||||||
|
this.state.brewCollection.map((brewGroup)=>{
|
||||||
|
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
|
||||||
|
});
|
||||||
|
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
|
||||||
|
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
|
||||||
|
},
|
||||||
|
|
||||||
renderBrews : function(brews){
|
renderBrews : function(brews){
|
||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
@@ -49,14 +98,18 @@ const ListPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleSortOptionChange : function(event){
|
handleSortOptionChange : function(event){
|
||||||
|
this.updateUrl(this.state.filterString, event.target.value, this.state.sortDir);
|
||||||
this.setState({
|
this.setState({
|
||||||
sortType : event.target.value
|
sortType : event.target.value
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSortDirChange : function(event){
|
handleSortDirChange : function(event){
|
||||||
|
const newDir = this.state.sortDir == 'asc' ? 'desc' : 'asc';
|
||||||
|
|
||||||
|
this.updateUrl(this.state.filterString, this.state.sortType, newDir);
|
||||||
this.setState({
|
this.setState({
|
||||||
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
sortDir : newDir
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -74,19 +127,37 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
handleFilterTextChange : function(e){
|
handleFilterTextChange : function(e){
|
||||||
this.setState({
|
this.setState({
|
||||||
filterString : e.target.value
|
filterString : e.target.value,
|
||||||
});
|
});
|
||||||
|
this.updateUrl(e.target.value, this.state.sortType, this.state.sortDir);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateUrl : function(filterTerm, sortType, sortDir){
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const urlParams = new URLSearchParams(url.search);
|
||||||
|
|
||||||
|
urlParams.set('sort', sortType);
|
||||||
|
urlParams.set('dir', sortDir);
|
||||||
|
|
||||||
|
if(!filterTerm)
|
||||||
|
urlParams.delete('filter');
|
||||||
|
else
|
||||||
|
urlParams.set('filter', filterTerm);
|
||||||
|
|
||||||
|
url.search = urlParams;
|
||||||
|
window.history.replaceState(null, null, url);
|
||||||
|
},
|
||||||
|
|
||||||
renderFilterOption : function(){
|
renderFilterOption : function(){
|
||||||
return <td>
|
return <td>
|
||||||
<label>
|
<label>
|
||||||
<i className='fas fa-search'></i>
|
<i className='fas fa-search'></i>
|
||||||
<input
|
<input
|
||||||
type='search'
|
type='search'
|
||||||
placeholder='search title/description'
|
placeholder='filter title/description'
|
||||||
onChange={this.handleFilterTextChange}
|
onChange={this.handleFilterTextChange}
|
||||||
|
value={this.state.filterString}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</td>;
|
</td>;
|
||||||
@@ -125,32 +196,48 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
getSortedBrews : function(brews){
|
getSortedBrews : function(brews){
|
||||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||||
brews = _.filter(brews, (brew)=>{
|
|
||||||
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
|
||||||
(_.deburr(brew.description).toLowerCase().includes(testString));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
brews = _.filter(brews, (brew)=>{
|
||||||
|
const brewStrings = _.deburr([
|
||||||
|
brew.title,
|
||||||
|
brew.description,
|
||||||
|
brew.tags].join('\n')
|
||||||
|
.toLowerCase());
|
||||||
|
|
||||||
|
return brewStrings.includes(testString);
|
||||||
|
});
|
||||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleBrewCollectionState : function(brewGroupClass) {
|
||||||
|
this.setState((prevState)=>({
|
||||||
|
brewCollection : prevState.brewCollection.map(
|
||||||
|
(brewGroup)=>brewGroup.class === brewGroupClass ? { ...brewGroup, visible: !brewGroup.visible } : brewGroup
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
renderBrewCollection : function(brewCollection){
|
renderBrewCollection : function(brewCollection){
|
||||||
|
if(brewCollection == []) return <div className='brewCollection'>
|
||||||
|
<h1>No Brews</h1>
|
||||||
|
</div>;
|
||||||
return _.map(brewCollection, (brewGroup, idx)=>{
|
return _.map(brewCollection, (brewGroup, idx)=>{
|
||||||
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
||||||
<h1>{brewGroup.title || 'No Title'}</h1>
|
<h1 className={brewGroup.visible ? 'active' : 'inactive'} onClick={()=>{this.toggleBrewCollectionState(brewGroup.class);}}>{brewGroup.title || 'No Title'}</h1>
|
||||||
{this.renderBrews(this.getSortedBrews(brewGroup.brews))}
|
{brewGroup.visible ? this.renderBrews(this.getSortedBrews(brewGroup.brews)) : <></>}
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='listPage sitePage'>
|
return <div className='listPage sitePage'>
|
||||||
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
<link href='/themes/5ePhb.style.css' rel='stylesheet'/>
|
||||||
{this.props.navItems}
|
{this.props.navItems}
|
||||||
|
|
||||||
<div className='content V3'>
|
<div className='content V3'>
|
||||||
<div className='phb'>
|
<div className='phb page'>
|
||||||
{this.renderSortOptions()}
|
{this.renderSortOptions()}
|
||||||
{this.renderBrewCollection(this.props.brewCollection)}
|
{this.renderBrewCollection(this.state.brewCollection)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
}
|
}
|
||||||
.listPage{
|
.listPage{
|
||||||
.content{
|
.content{
|
||||||
overflow-y : scroll;
|
overflow-y : overlay;
|
||||||
.phb{
|
.phb{
|
||||||
.noColumns();
|
.noColumns();
|
||||||
height : auto;
|
height : auto;
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
position : fixed;
|
position : fixed;
|
||||||
top : 35px;
|
top : 35px;
|
||||||
left : calc(50vw - 408px);
|
left : calc(50vw - 400px);
|
||||||
border : 2px solid #58180D;
|
border : 2px solid #58180D;
|
||||||
width : 800px;
|
width : 800px;
|
||||||
background-color : #EEE5CE;
|
background-color : #EEE5CE;
|
||||||
@@ -74,4 +74,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
h1 {
|
||||||
|
cursor: pointer;
|
||||||
|
&.active {
|
||||||
|
color: #58180D;
|
||||||
|
}
|
||||||
|
&.inactive {
|
||||||
|
color: #707070;
|
||||||
|
|
||||||
|
}
|
||||||
|
&.active::before, &.inactive::before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 0.6cm;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
&.active::before {
|
||||||
|
content: '\f107';
|
||||||
|
}
|
||||||
|
&.inactive::before {
|
||||||
|
content: '\f105';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const EditPage = createClass({
|
|||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { text: text }),
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
isPending : true,
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors
|
||||||
}), ()=>this.trySave());
|
}), ()=>this.trySave());
|
||||||
@@ -122,14 +122,17 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
handleStyleChange : function(style){
|
handleStyleChange : function(style){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { style: style }),
|
brew : { ...prevState.brew, style: style },
|
||||||
isPending : true
|
isPending : true
|
||||||
}), ()=>this.trySave());
|
}), ()=>this.trySave());
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata){
|
handleMetaChange : function(metadata){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, metadata),
|
brew : {
|
||||||
|
...prevState.brew,
|
||||||
|
...metadata
|
||||||
|
},
|
||||||
isPending : true,
|
isPending : true,
|
||||||
}), ()=>this.trySave());
|
}), ()=>this.trySave());
|
||||||
|
|
||||||
@@ -200,7 +203,7 @@ const EditPage = createClass({
|
|||||||
const brew = this.state.brew;
|
const brew = this.state.brew;
|
||||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
|
||||||
const params = `${transfer ? `?transfer${this.state.saveGoogle ? 'To' : 'From'}Google=true` : ''}`;
|
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||||
const res = await request
|
const res = await request
|
||||||
.put(`/api/update/${brew.editId}${params}`)
|
.put(`/api/update/${brew.editId}${params}`)
|
||||||
.send(brew)
|
.send(brew)
|
||||||
@@ -210,16 +213,14 @@ const EditPage = createClass({
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
this.savedBrew = res.body;
|
||||||
if(transfer) {
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.googleId ?? ''}${this.savedBrew.editId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, 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
|
||||||
}),
|
},
|
||||||
isPending : false,
|
isPending : false,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
}));
|
}));
|
||||||
@@ -276,26 +277,26 @@ const EditPage = createClass({
|
|||||||
console.log(errMsg);
|
console.log(errMsg);
|
||||||
} catch (e){}
|
} catch (e){}
|
||||||
|
|
||||||
if(this.state.errors.status == '401'){
|
// if(this.state.errors.status == '401'){
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
// Oops!
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
// <div className='errorContainer' onClick={this.clearErrors}>
|
||||||
You must be signed in to a Google account
|
// You must be signed in to a Google account
|
||||||
to save this to<br />Google Drive!<br />
|
// to save this to<br />Google Drive!<br />
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
// <a target='_blank' rel='noopener noreferrer'
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||||
<div className='confirm'>
|
// <div className='confirm'>
|
||||||
Sign In
|
// Sign In
|
||||||
</div>
|
// </div>
|
||||||
</a>
|
// </a>
|
||||||
<div className='deny'>
|
// <div className='deny'>
|
||||||
Not Now
|
// Not Now
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</Nav.item>;
|
// </Nav.item>;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if(this.state.errors.response.req.url.match(/^\/api\/.*Google.*$/m)){
|
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
<div className='errorContainer' onClick={this.clearErrors}>
|
||||||
@@ -340,7 +341,7 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
processShareId : function() {
|
processShareId : function() {
|
||||||
return this.state.brew.googleId ?
|
return this.state.brew.googleId && !this.state.brew.stubbed ?
|
||||||
this.state.brew.googleId + this.state.brew.shareId :
|
this.state.brew.googleId + this.state.brew.shareId :
|
||||||
this.state.brew.shareId;
|
this.state.brew.shareId;
|
||||||
},
|
},
|
||||||
@@ -352,7 +353,7 @@ const EditPage = createClass({
|
|||||||
const title = `${this.props.brew.title} ${systems}`;
|
const title = `${this.props.brew.title} ${systems}`;
|
||||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||||
|
|
||||||
**[Homebrewery Link](https://homebrewery.naturalcrit.com/share/${shareLink})**`;
|
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
||||||
},
|
},
|
||||||
@@ -387,7 +388,7 @@ const EditPage = createClass({
|
|||||||
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
||||||
view
|
view
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`https://homebrewery.naturalcrit.com/share/${shareLink}`);}}>
|
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.publicUrl}/share/${shareLink}`);}}>
|
||||||
copy url
|
copy url
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const HomePage = createClass({
|
|||||||
},
|
},
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { text: text })
|
brew : { ...prevState.brew, text: text }
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
|
|||||||
@@ -1,50 +1,68 @@
|
|||||||
# The Homebrewery
|
```css
|
||||||
|
.page #example + table td {
|
||||||
|
border:1px dashed #00000030;
|
||||||
|
}
|
||||||
|
.page {
|
||||||
|
padding-bottom : 1.1cm;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# The Homebrewery *V3*
|
||||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||||
|
|
||||||
### Homebrew D&D made easy
|
### Homebrew D&D made easy
|
||||||
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||||
|
|
||||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right.
|
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Editing and Sharing
|
### Editing and Sharing
|
||||||
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
|
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
||||||
|
|
||||||
|
Any changes you make while on the *edit url* will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew, so be careful about who you share it with.
|
||||||
|
|
||||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||||
|
|
||||||
## Helping out
|
{{note
|
||||||
|
##### PDF Creation
|
||||||
|
PDF Printing works best in Google Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
|
||||||
|
|
||||||
|
After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
|
||||||
|
* Set the **Destination** to "Save as PDF"
|
||||||
|
* Set **Paper Size** to "Letter"
|
||||||
|
* If you are printing on A4 paper, make sure to have the **PRINT → {{far,fa-file}} A4 Pagesize** snippet in your brew
|
||||||
|
* In **Options** make sure "Background Images" is selected.
|
||||||
|
* Hit print and enjoy! You're done!
|
||||||
|
|
||||||
|
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
||||||
|
}}
|
||||||
|
|
||||||
|
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||||
|
|
||||||
|
{{artist,bottom:160px,left:100px
|
||||||
|
##### Homebrew Mug
|
||||||
|
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{pageNumber 1}}
|
||||||
|
{{footnote PART 1 | FANCINESS}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
## New in V3.0.0
|
||||||
|
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
||||||
|
|
||||||
|
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
||||||
|
|
||||||
|
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
||||||
|
|
||||||
|
#### New Things All The Time!
|
||||||
|
Check out the latest updates in the full changelog [here](/changelog).
|
||||||
|
|
||||||
|
### Helping out
|
||||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
||||||
|
|
||||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
>##### PDF Exporting
|
|
||||||
> PDF Printing works best in Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
|
|
||||||
>
|
|
||||||
> After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
|
|
||||||
> * Set the **Destination** to "Save as PDF"
|
|
||||||
> * Set **Paper Size** to "Letter"
|
|
||||||
> * If you are printing on A4 paper, make sure to have the "A4 page size snippet" in your brew
|
|
||||||
> * In **Options** make sure "Background Images" is selected.
|
|
||||||
> * Hit print and enjoy! You're done!
|
|
||||||
>
|
|
||||||
> If you want to save ink or have a monochrome printer, add the **Ink Friendly** snippet to your brew before you print
|
|
||||||
|
|
||||||
|
|
||||||
```
|
|
||||||
```
|
|
||||||
|
|
||||||
## V3.0.0 Released!
|
|
||||||
With the latest major update to *The Homebrewery* we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like **div** and **span** in most cases. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
|
||||||
|
|
||||||
**You can enable V3 via the <span class="fa fa-info-circle" style="text-indent:0"></span> Properties button!**
|
|
||||||
|
|
||||||
## New Things All The Time!
|
|
||||||
What's new in the latest update? Check out the full changelog [here](/changelog)
|
|
||||||
|
|
||||||
### Bugs, Issues, Suggestions?
|
### Bugs, Issues, Suggestions?
|
||||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
||||||
|
|
||||||
@@ -53,56 +71,105 @@ Need help getting started or just the right look for your brew? Head to [r/Homeb
|
|||||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
||||||
|
|
||||||
### Legal Junk
|
### Legal Junk
|
||||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). This means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||||
|
|
||||||
### More Resources
|
#### Crediting Me
|
||||||
|
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||||
|
|
||||||
|
### More Homebrew Resources
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||||
|
|
||||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
|
{{position:absolute;top:20px;right:20px;width:auto
|
||||||
|
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||||
<div class='pageNumber'>1</div>
|
|
||||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
|
||||||
|
|
||||||
<div style='position: absolute; top: 20px; right: 20px;'>
|
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'><img src='/assets/discord.png' style='height:30px'/></a>
|
|
||||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||||
</div>
|
}}
|
||||||
|
|
||||||
\page
|
\page
|
||||||
|
|
||||||
# Appendix
|
## Markdown+
|
||||||
|
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||||
|
|
||||||
### Not quite Markdown
|
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
||||||
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
|
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
||||||
|
|
||||||
___
|
### Curly Brackets
|
||||||
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
|
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
||||||
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
|
|
||||||
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
|
#### Span
|
||||||
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
|
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
||||||
|
|
||||||
|
|
||||||
|
#### Block
|
||||||
|
{{purple,#book,text-align:center,background:#aa88aa55
|
||||||
|
My favorite book is Wheel of Time. This block has a class of `purple`, an id of `book`, and centered text with a colored background. The opening and closing brackets are on lines separate from the block contents.
|
||||||
|
}}
|
||||||
|
|
||||||
```
|
#### Injection
|
||||||
```
|
For any element not inside a span or block, you can *inject* attributes using the same syntax but with single brackets in a single line immediately after the element.
|
||||||
|
|
||||||
|
Inline elements like *italics* {color:#D35400} or images require the injection on the same line.
|
||||||
|
|
||||||
### Images
|
Block elements like headers require the injection to start on the line immediately following.
|
||||||
Images must be hosted online somewhere, like imgur. You use the address to that image to reference it in your brew. Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
|
|
||||||
|
|
||||||
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. I have added both a inflow image snippet and a background image snippet to give you exmaples of how to do it.
|
##### A Purple Header
|
||||||
|
{color:purple,text-align:center}
|
||||||
|
|
||||||
|
\* *this does not currently work for tables yet*
|
||||||
|
|
||||||
|
### Vertical Spacing
|
||||||
|
A blank line can be achieved with a run of one or more `:` alone on a line. More `:`'s will create more space.
|
||||||
|
|
||||||
### Crediting Me
|
::
|
||||||
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
|
||||||
|
|
||||||
|
Much nicer than `<br><br><br><br><br>`
|
||||||
|
|
||||||
|
### Definition Lists
|
||||||
|
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||||
|
|
||||||
<div class='pageNumber'>2</div>
|
### Column Breaks
|
||||||
<div class='footnote'>PART 2 | BORING STUFF</div>
|
Column and page breaks with `\column` and `\page`.
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||||
|
|
||||||
|
A cell can be spanned across columns by grouping multiple pipe `|` characters at the end of a cell.
|
||||||
|
|
||||||
|
Row spanning is achieved by adding a `^` at the end of a cell just before the `|`.
|
||||||
|
|
||||||
|
These can be combined to span a cell across both columns and rows. Cells must have the same colspan if they are to be rowspan'd.
|
||||||
|
|
||||||
|
##### Example
|
||||||
|
| Head A | Spanned Header ||
|
||||||
|
| Head B | Head C | Head D |
|
||||||
|
|:-------|:------:|:------:|
|
||||||
|
| 1A | 1B | 1C |
|
||||||
|
| 2A ^| 2B | 2C |
|
||||||
|
| 3A ^| 3B 3C ||
|
||||||
|
| 4A | 4B 4C^||
|
||||||
|
| 5A ^| 5B | 5C |
|
||||||
|
| 6A | 6B ^| 6C |
|
||||||
|
|
||||||
|
## Images
|
||||||
|
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*.
|
||||||
|
|
||||||
|
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
||||||
|
|
||||||
|
 {width:100px,border:"2px solid",border-radius:10px}
|
||||||
|
|
||||||
|
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
|
||||||
|
|
||||||
|
## Snippets
|
||||||
|
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
||||||
|
|
||||||
|
## Style Editor Panel
|
||||||
|
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||||
|
|
||||||
|
{{pageNumber 2}}
|
||||||
|
{{footnote PART 2 | BORING STUFF}}
|
||||||
|
|||||||
108
client/homebrew/pages/homePage/welcome_msg_legacy.md
Normal file
108
client/homebrew/pages/homePage/welcome_msg_legacy.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# The Homebrewery
|
||||||
|
|
||||||
|
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||||
|
|
||||||
|
### Homebrew D&D made easy
|
||||||
|
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||||
|
|
||||||
|
**Try it!** Simply edit the text on the left and watch it *update live* on the right.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Editing and Sharing
|
||||||
|
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
|
||||||
|
|
||||||
|
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||||
|
|
||||||
|
## Helping out
|
||||||
|
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
||||||
|
|
||||||
|
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
>##### PDF Exporting
|
||||||
|
> PDF Printing works best in Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
|
||||||
|
>
|
||||||
|
> After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
|
||||||
|
> * Set the **Destination** to "Save as PDF"
|
||||||
|
> * Set **Paper Size** to "Letter"
|
||||||
|
> * If you are printing on A4 paper, make sure to have the "A4 page size snippet" in your brew
|
||||||
|
> * In **Options** make sure "Background Images" is selected.
|
||||||
|
> * Hit print and enjoy! You're done!
|
||||||
|
>
|
||||||
|
> If you want to save ink or have a monochrome printer, add the **Ink Friendly** snippet to your brew before you print
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
## V3.0.0 Released!
|
||||||
|
With the latest major update to *The Homebrewery* we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like **div** and **span** in most cases. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||||
|
|
||||||
|
**You can enable V3 via the <span class="fa fa-info-circle" style="text-indent:0"></span> Properties button!**
|
||||||
|
|
||||||
|
## New Things All The Time!
|
||||||
|
What's new in the latest update? Check out the full changelog [here](/changelog)
|
||||||
|
|
||||||
|
### Bugs, Issues, Suggestions?
|
||||||
|
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
||||||
|
|
||||||
|
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
||||||
|
|
||||||
|
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
||||||
|
|
||||||
|
### Legal Junk
|
||||||
|
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). This means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
||||||
|
|
||||||
|
If you wish to sell or in some way gain profit for what you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||||
|
|
||||||
|
### More Resources
|
||||||
|
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||||
|
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||||
|
|
||||||
|
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
|
||||||
|
|
||||||
|
<div class='pageNumber'>1</div>
|
||||||
|
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||||
|
|
||||||
|
<div style='position: absolute; top: 20px; right: 20px;'>
|
||||||
|
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||||
|
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||||
|
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||||
|
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
# Appendix
|
||||||
|
|
||||||
|
### Not quite Markdown
|
||||||
|
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
|
||||||
|
|
||||||
|
___
|
||||||
|
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
|
||||||
|
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
|
||||||
|
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
|
||||||
|
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Images
|
||||||
|
Images must be hosted online somewhere, like imgur. You use the address to that image to reference it in your brew. Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
|
||||||
|
|
||||||
|
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. I have added both a inflow image snippet and a background image snippet to give you exmaples of how to do it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Crediting Me
|
||||||
|
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class='pageNumber'>2</div>
|
||||||
|
<div class='footnote'>PART 2 | BORING STUFF</div>
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
```css
|
|
||||||
.page #example + table td {
|
|
||||||
border:1px dashed #00000030;
|
|
||||||
}
|
|
||||||
.page {
|
|
||||||
padding-bottom : 1.1cm;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# The Homebrewery *V3*
|
|
||||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
|
||||||
|
|
||||||
### Homebrew D&D made easy
|
|
||||||
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
|
||||||
|
|
||||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
|
||||||
|
|
||||||
### Editing and Sharing
|
|
||||||
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
|
||||||
|
|
||||||
Any changes you make while on the *edit url* will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew, so be careful about who you share it with.
|
|
||||||
|
|
||||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
|
||||||
|
|
||||||
{{note
|
|
||||||
##### PDF Creation
|
|
||||||
PDF Printing works best in Google Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
|
|
||||||
|
|
||||||
After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
|
|
||||||
* Set the **Destination** to "Save as PDF"
|
|
||||||
* Set **Paper Size** to "Letter"
|
|
||||||
* If you are printing on A4 paper, make sure to have the **PRINT → {{far,fa-file}} A4 Pagesize** snippet in your brew
|
|
||||||
* In **Options** make sure "Background Images" is selected.
|
|
||||||
* Hit print and enjoy! You're done!
|
|
||||||
|
|
||||||
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
|
||||||
}}
|
|
||||||
|
|
||||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
|
||||||
|
|
||||||
{{artist,bottom:160px,left:100px
|
|
||||||
##### Homebrew Mug
|
|
||||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
|
||||||
}}
|
|
||||||
|
|
||||||
{{pageNumber 1}}
|
|
||||||
{{footnote PART 1 | FANCINESS}}
|
|
||||||
|
|
||||||
\column
|
|
||||||
|
|
||||||
## New in V3.0.0
|
|
||||||
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
|
||||||
|
|
||||||
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
|
||||||
|
|
||||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
|
||||||
|
|
||||||
#### New Things All The Time!
|
|
||||||
Check out the latest updates in the full changelog [here](/changelog).
|
|
||||||
|
|
||||||
### Helping out
|
|
||||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) to help me keep the servers running.
|
|
||||||
|
|
||||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
|
||||||
|
|
||||||
### Bugs, Issues, Suggestions?
|
|
||||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
|
||||||
|
|
||||||
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
|
||||||
|
|
||||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
|
||||||
|
|
||||||
### Legal Junk
|
|
||||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery codebase any way that you want, except for claiming that you made it yourself.
|
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
|
||||||
|
|
||||||
#### Crediting Me
|
|
||||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
|
||||||
|
|
||||||
### More Homebrew Resources
|
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
|
||||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
|
||||||
|
|
||||||
{{position:absolute;top:20px;right:20px;width:auto
|
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
|
||||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
|
||||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
|
||||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
|
||||||
}}
|
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
## Markdown+
|
|
||||||
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
|
||||||
|
|
||||||
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
|
||||||
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
|
||||||
|
|
||||||
### Curly Brackets
|
|
||||||
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
|
||||||
|
|
||||||
#### Span
|
|
||||||
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
|
||||||
|
|
||||||
|
|
||||||
#### Block
|
|
||||||
{{purple,#book,text-align:center,background:#aa88aa55
|
|
||||||
My favorite book is Wheel of Time. This block has a class of `purple`, an id of `book`, and centered text with a colored background. The opening and closing brackets are on lines separate from the block contents.
|
|
||||||
}}
|
|
||||||
|
|
||||||
#### Injection
|
|
||||||
For any element not inside a span or block, you can *inject* attributes using the same syntax but with single brackets in a single line immediately after the element.
|
|
||||||
|
|
||||||
Inline elements like *italics* {color:#D35400} or images require the injection on the same line.
|
|
||||||
|
|
||||||
Block elements like headers require the injection to start on the line immediately following.
|
|
||||||
|
|
||||||
##### A Purple Header
|
|
||||||
{color:purple,text-align:center}
|
|
||||||
|
|
||||||
\* *this does not currently work for tables yet*
|
|
||||||
|
|
||||||
### Vertical Spacing
|
|
||||||
A blank line can be achieved with a run of one or more `:` alone on a line. More `:`'s will create more space.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
Much nicer than `<br><br><br><br><br>`
|
|
||||||
|
|
||||||
### Definition Lists
|
|
||||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
|
||||||
|
|
||||||
### Column Breaks
|
|
||||||
Column and page breaks with `\column` and `\page`.
|
|
||||||
|
|
||||||
\column
|
|
||||||
|
|
||||||
### Tables
|
|
||||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
|
||||||
|
|
||||||
A cell can be spanned across columns by grouping multiple pipe `|` characters at the end of a cell.
|
|
||||||
|
|
||||||
Row spanning is achieved by adding a `^` at the end of a cell just before the `|`.
|
|
||||||
|
|
||||||
These can be combined to span a cell across both columns and rows. Cells must have the same colspan if they are to be rowspan'd.
|
|
||||||
|
|
||||||
##### Example
|
|
||||||
| Head A | Spanned Header ||
|
|
||||||
| Head B | Head C | Head D |
|
|
||||||
|:-------|:------:|:------:|
|
|
||||||
| 1A | 1B | 1C |
|
|
||||||
| 2A ^| 2B | 2C |
|
|
||||||
| 3A ^| 3B 3C ||
|
|
||||||
| 4A | 4B 4C^||
|
|
||||||
| 5A ^| 5B | 5C |
|
|
||||||
| 6A | 6B ^| 6C |
|
|
||||||
|
|
||||||
## Images
|
|
||||||
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*.
|
|
||||||
|
|
||||||
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
|
||||||
|
|
||||||
 {width:100px,border:"2px solid",border-radius:10px}
|
|
||||||
|
|
||||||
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
|
|
||||||
|
|
||||||
## Snippets
|
|
||||||
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
|
||||||
|
|
||||||
## Style Editor Panel
|
|
||||||
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
|
||||||
|
|
||||||
{{pageNumber 2}}
|
|
||||||
{{footnote PART 2 | BORING STUFF}}
|
|
||||||
@@ -27,55 +27,30 @@ const NewPage = createClass({
|
|||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
text : '',
|
text : '',
|
||||||
style : undefined,
|
style : undefined,
|
||||||
shareId : null,
|
|
||||||
editId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
gDrive : false,
|
|
||||||
|
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
tags : '',
|
renderer : 'V3'
|
||||||
published : false,
|
|
||||||
authors : [],
|
|
||||||
systems : []
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
const brew = this.props.brew;
|
let brew = this.props.brew;
|
||||||
|
|
||||||
if(typeof window !== 'undefined') { //Load from localStorage if in client browser
|
if(this.props.brew.shareId) {
|
||||||
const brewStorage = localStorage.getItem(BREWKEY);
|
brew = {
|
||||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
text : brew.text ?? '',
|
||||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
style : brew.style ?? undefined,
|
||||||
|
title : brew.title ?? '',
|
||||||
if(!brew.text || !brew.style){
|
description : brew.description ?? '',
|
||||||
brew.text = brew.text || (brewStorage ?? '');
|
renderer : brew.renderer ?? 'legacy'
|
||||||
brew.style = brew.style || (styleStorage ?? undefined);
|
};
|
||||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
|
||||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
|
||||||
brew.renderer = metaStorage?.renderer || brew.renderer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : brew,
|
||||||
text : brew.text || '',
|
|
||||||
style : brew.style || undefined,
|
|
||||||
gDrive : false,
|
|
||||||
title : brew.title || '',
|
|
||||||
description : brew.description || '',
|
|
||||||
tags : brew.tags || '',
|
|
||||||
published : false,
|
|
||||||
authors : [],
|
|
||||||
systems : brew.systems || [],
|
|
||||||
renderer : brew.renderer || 'legacy'
|
|
||||||
},
|
|
||||||
|
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
errors : null,
|
errors : null,
|
||||||
@@ -85,6 +60,28 @@ const NewPage = createClass({
|
|||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
|
|
||||||
|
const brew = this.state.brew;
|
||||||
|
|
||||||
|
if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||||
|
const brewStorage = localStorage.getItem(BREWKEY);
|
||||||
|
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||||
|
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||||
|
|
||||||
|
brew.text = brewStorage ?? brew.text;
|
||||||
|
brew.style = styleStorage ?? brew.style;
|
||||||
|
// brew.title = metaStorage?.title || this.state.brew.title;
|
||||||
|
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||||
|
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
brew : brew
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
|
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer }));
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
@@ -112,7 +109,7 @@ const NewPage = createClass({
|
|||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { text: text }),
|
brew : { ...prevState.brew, text: text },
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(BREWKEY, text);
|
localStorage.setItem(BREWKEY, text);
|
||||||
@@ -120,14 +117,14 @@ const NewPage = createClass({
|
|||||||
|
|
||||||
handleStyleChange : function(style){
|
handleStyleChange : function(style){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { style: style }),
|
brew : { ...prevState.brew, style: style },
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(STYLEKEY, style);
|
localStorage.setItem(STYLEKEY, style);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata){
|
handleMetaChange : function(metadata){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, 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,
|
||||||
@@ -162,7 +159,7 @@ const NewPage = createClass({
|
|||||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api${this.state.saveGoogle ? '?transferToGoogle=true' : ''}`)
|
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
@@ -174,7 +171,7 @@ const NewPage = createClass({
|
|||||||
localStorage.removeItem(BREWKEY);
|
localStorage.removeItem(BREWKEY);
|
||||||
localStorage.removeItem(STYLEKEY);
|
localStorage.removeItem(STYLEKEY);
|
||||||
localStorage.removeItem(METAKEY);
|
localStorage.removeItem(METAKEY);
|
||||||
window.location = `/edit/${brew.googleId ?? ''}${brew.editId}`;
|
window.location = `/edit/${brew.editId}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
@@ -187,26 +184,26 @@ const NewPage = createClass({
|
|||||||
console.log(errMsg);
|
console.log(errMsg);
|
||||||
} catch (e){}
|
} catch (e){}
|
||||||
|
|
||||||
if(this.state.errors.status == '401'){
|
// if(this.state.errors.status == '401'){
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
// Oops!
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
// <div className='errorContainer' onClick={this.clearErrors}>
|
||||||
You must be signed in to a Google account
|
// You must be signed in to a Google account
|
||||||
to save this to<br />Google Drive!<br />
|
// to save this to<br />Google Drive!<br />
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
// <a target='_blank' rel='noopener noreferrer'
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
||||||
<div className='confirm'>
|
// <div className='confirm'>
|
||||||
Sign In
|
// Sign In
|
||||||
</div>
|
// </div>
|
||||||
</a>
|
// </a>
|
||||||
<div className='deny'>
|
// <div className='deny'>
|
||||||
Not Now
|
// Not Now
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</Nav.item>;
|
// </Nav.item>;
|
||||||
}
|
// }
|
||||||
|
|
||||||
if(this.state.errors.response.req.url.match(/^\/api\/.*Google.*$/m)){
|
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
<div className='errorContainer' onClick={this.clearErrors}>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const PrintPage = createClass({
|
|||||||
brew : {
|
brew : {
|
||||||
text : brewStorage,
|
text : brewStorage,
|
||||||
style : styleStorage,
|
style : styleStorage,
|
||||||
renderer : metaStorage.renderer || 'legacy'
|
renderer : metaStorage?.renderer || 'legacy'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const SharePage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
processShareId : function() {
|
processShareId : function() {
|
||||||
return this.props.brew.googleId ?
|
return this.props.brew.googleId && !this.props.brew.stubbed ?
|
||||||
this.props.brew.googleId + this.props.brew.shareId :
|
this.props.brew.googleId + this.props.brew.shareId :
|
||||||
this.props.brew.shareId;
|
this.props.brew.shareId;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,10 +19,11 @@ const UserPage = createClass({
|
|||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : [],
|
brews : [],
|
||||||
|
query : ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `'` : `'s`);
|
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `’` : `’s`);
|
||||||
|
|
||||||
const brews = _.groupBy(this.props.brews, (brew)=>{
|
const brews = _.groupBy(this.props.brews, (brew)=>{
|
||||||
return (brew.published ? 'published' : 'private');
|
return (brew.published ? 'published' : 'private');
|
||||||
@@ -62,7 +63,7 @@ const UserPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()}></ListPage>;
|
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
BIN
client/homebrew/thumbnail.png
Normal file
BIN
client/homebrew/thumbnail.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
@@ -1,4 +1,6 @@
|
|||||||
module.exports = async(name, title = '', props = {})=>{
|
module.exports = async(name, title = '', props = {})=>{
|
||||||
|
const HOMEBREWERY_PUBLIC_URL=props.config.publicUrl;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -6,7 +8,14 @@ module.exports = async(name, title = '', props = {})=>{
|
|||||||
<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/homebrew/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||||
|
<meta property="og:title" content="${props.brew?.title || 'Homebrewery - Untitled Brew'}">
|
||||||
|
<meta property="og:url" content="${HOMEBREWERY_PUBLIC_URL}/${props.brew?.shareId ? `share/${props.brew.shareId}` : ''}">
|
||||||
|
<meta property="og:image" content="${props.brew?.thumbnail || `${HOMEBREWERY_PUBLIC_URL}/thumbnail.png`}">
|
||||||
|
<meta property="og:description" content="${props.brew?.description || 'No description.'}">
|
||||||
|
<meta property="og:site_name" content="The Homebrewery - Make your Homebrew content look legit!">
|
||||||
|
<meta property="og:type" content="article">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -3,5 +3,7 @@
|
|||||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||||
"secret" : "secret",
|
"secret" : "secret",
|
||||||
"web_port" : 8000,
|
"web_port" : 8000,
|
||||||
"enable_v3" : true
|
"enable_v3" : true,
|
||||||
|
"local_environments" : ["docker", "local"],
|
||||||
|
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
||||||
}
|
}
|
||||||
|
|||||||
7931
package-lock.json
generated
7931
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"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.0.8",
|
"version": "3.2.2",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.11.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"jest": {
|
"jest": {
|
||||||
"testTimeout" : 15000,
|
"testTimeout": 15000,
|
||||||
"modulePaths": [
|
"modulePaths": [
|
||||||
"mode_modules",
|
"mode_modules",
|
||||||
"shared",
|
"shared",
|
||||||
@@ -51,45 +51,44 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.17.8",
|
"@babel/core": "^7.19.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
"@babel/plugin-transform-runtime": "^7.18.10",
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@babel/preset-env": "^7.19.0",
|
||||||
"@babel/preset-react": "^7.16.7",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"body-parser": "^1.19.2",
|
"body-parser": "^1.20.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"codemirror": "^5.65.2",
|
"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.1",
|
||||||
"express": "^4.17.3",
|
"express": "^4.18.1",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.5",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "10.0.1",
|
"fs-extra": "10.1.0",
|
||||||
"googleapis": "98.0.0",
|
"googleapis": "107.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.0.12",
|
"marked": "4.1.0",
|
||||||
"marked-extended-tables": "^1.0.3",
|
"marked-extended-tables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.4",
|
||||||
"mongoose": "^6.2.8",
|
"mongoose": "^6.5.5",
|
||||||
"nanoid": "3.3.1",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.11.3",
|
"nconf": "^0.12.0",
|
||||||
"query-string": "7.1.1",
|
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"react-frame-component": "4.1.3",
|
"react-frame-component": "4.1.3",
|
||||||
"react-router-dom": "5.3.0",
|
"react-router-dom": "6.3.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.11.0",
|
"eslint": "^8.23.0",
|
||||||
"eslint-plugin-react": "^7.29.4",
|
"eslint-plugin-react": "^7.31.7",
|
||||||
"jest": "^27.5.1",
|
"jest": "^29.0.2",
|
||||||
"supertest": "^6.2.2"
|
"supertest": "^6.2.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const build = async ({ bundle, render, ssr })=>{
|
|||||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||||
await fs.copy('./themes/fonts', './build/fonts');
|
await fs.copy('./themes/fonts', './build/fonts');
|
||||||
await fs.copy('./themes/assets', './build/assets');
|
await fs.copy('./themes/assets', './build/assets');
|
||||||
|
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
|
||||||
let src = './themes/5ePhbLegacy.style.less';
|
let src = './themes/5ePhbLegacy.style.less';
|
||||||
//Parse brew theme files
|
//Parse brew theme files
|
||||||
less.render(fs.readFileSync(src).toString(), {
|
less.render(fs.readFileSync(src).toString(), {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"codemirror/addon/fold/foldcode.js",
|
"codemirror/addon/fold/foldcode.js",
|
||||||
"codemirror/addon/fold/foldgutter.js",
|
"codemirror/addon/fold/foldgutter.js",
|
||||||
"codemirror/addon/fold/xml-fold.js",
|
"codemirror/addon/fold/xml-fold.js",
|
||||||
|
"codemirror/addon/scroll/scrollpastend.js",
|
||||||
"codemirror/addon/search/search.js",
|
"codemirror/addon/search/search.js",
|
||||||
"codemirror/addon/search/searchcursor.js",
|
"codemirror/addon/search/searchcursor.js",
|
||||||
"codemirror/addon/search/jump-to-line.js",
|
"codemirror/addon/search/jump-to-line.js",
|
||||||
|
|||||||
232
server/app.js
232
server/app.js
@@ -9,47 +9,12 @@ const yaml = require('js-yaml');
|
|||||||
const app = express();
|
const app = express();
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
|
|
||||||
const homebrewApi = require('./homebrew.api.js');
|
const { homebrewApi, getBrew } = require('./homebrew.api.js');
|
||||||
const GoogleActions = require('./googleActions.js');
|
const GoogleActions = require('./googleActions.js');
|
||||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
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 brewAccessTypes = ['edit', 'share', 'raw'];
|
|
||||||
|
|
||||||
//Get the brew object from the HB database or Google Drive
|
|
||||||
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
|
||||||
if(!brewAccessTypes.includes(accessType))
|
|
||||||
throw ('Invalid Access Type when getting brew');
|
|
||||||
let brew;
|
|
||||||
if(id.length > 12) {
|
|
||||||
const googleId = id.slice(0, -12);
|
|
||||||
id = id.slice(-12);
|
|
||||||
brew = await GoogleActions.getGoogleBrew(googleId, id, accessType);
|
|
||||||
} else {
|
|
||||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
|
||||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
|
||||||
}
|
|
||||||
|
|
||||||
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
|
||||||
//Split brew.text into text and style
|
|
||||||
//unless the Access Type is RAW, in which case return immediately
|
|
||||||
if(accessType == 'raw') {
|
|
||||||
return brew;
|
|
||||||
}
|
|
||||||
splitTextStyleAndMetadata(brew);
|
|
||||||
return brew;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sanitizeBrew = (brew, full=false)=>{
|
|
||||||
delete brew._id;
|
|
||||||
delete brew.__v;
|
|
||||||
if(full){
|
|
||||||
delete brew.editId;
|
|
||||||
}
|
|
||||||
return brew;
|
|
||||||
};
|
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
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')) {
|
||||||
@@ -66,6 +31,15 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
|
brew._id = undefined;
|
||||||
|
brew.__v = undefined;
|
||||||
|
if(accessType !== 'edit'){
|
||||||
|
brew.editId = undefined;
|
||||||
|
}
|
||||||
|
return brew;
|
||||||
|
};
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
|
|
||||||
//app.use(express.static(`${__dirname}/build`));
|
//app.use(express.static(`${__dirname}/build`));
|
||||||
@@ -93,12 +67,12 @@ app.use((req, res, next)=>{
|
|||||||
app.use(homebrewApi);
|
app.use(homebrewApi);
|
||||||
app.use(require('./admin.api.js'));
|
app.use(require('./admin.api.js'));
|
||||||
|
|
||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||||
const welcomeTextV3 = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
const welcomeTextLegacy = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
||||||
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||||
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
|
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
|
||||||
const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
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);};
|
||||||
|
|
||||||
@@ -108,63 +82,60 @@ app.get('/robots.txt', (req, res)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
//Home page
|
//Home page
|
||||||
app.get('/', async (req, res, next)=>{
|
app.get('/', (req, res, next)=>{
|
||||||
const brew = {
|
req.brew = {
|
||||||
text : welcomeText
|
text : welcomeText,
|
||||||
|
renderer : 'V3'
|
||||||
};
|
};
|
||||||
req.brew = brew;
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Home page v3
|
//Home page v3
|
||||||
app.get('/v3_preview', async (req, res, next)=>{
|
app.get('/legacy', (req, res, next)=>{
|
||||||
const brew = {
|
req.brew = {
|
||||||
text : welcomeTextV3,
|
text : welcomeTextLegacy,
|
||||||
renderer : 'V3'
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
splitTextStyleAndMetadata(brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Legacy/Other Document -> v3 Migration Guide
|
//Legacy/Other Document -> v3 Migration Guide
|
||||||
app.get('/migrate', async (req, res, next)=>{
|
app.get('/migrate', (req, res, next)=>{
|
||||||
const brew = {
|
req.brew = {
|
||||||
text : migrateText,
|
text : migrateText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
};
|
};
|
||||||
splitTextStyleAndMetadata(brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Changelog page
|
//Changelog page
|
||||||
app.get('/changelog', async (req, res, next)=>{
|
app.get('/changelog', async (req, res, next)=>{
|
||||||
const brew = {
|
req.brew = {
|
||||||
title : 'Changelog',
|
title : 'Changelog',
|
||||||
text : changelogText,
|
text : changelogText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
};
|
};
|
||||||
splitTextStyleAndMetadata(brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//FAQ page
|
//FAQ page
|
||||||
app.get('/faq', async (req, res, next)=>{
|
app.get('/faq', async (req, res, next)=>{
|
||||||
const brew = {
|
req.brew = {
|
||||||
title : 'FAQ',
|
title : 'FAQ',
|
||||||
text : faqText,
|
text : faqText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
};
|
};
|
||||||
splitTextStyleAndMetadata(brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Source page
|
//Source page
|
||||||
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
app.get('/source/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
const { brew } = req;
|
||||||
|
|
||||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||||
let text = brew.text;
|
let text = brew.text;
|
||||||
@@ -173,11 +144,12 @@ app.get('/source/:id', asyncHandler(async (req, res)=>{
|
|||||||
}
|
}
|
||||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||||
res.status(200).send(text);
|
res.status(200).send(text);
|
||||||
}));
|
});
|
||||||
|
|
||||||
//Download brew source page
|
//Download brew source page
|
||||||
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
||||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
const { brew } = req;
|
||||||
|
sanitizeBrew(brew, 'share');
|
||||||
const prefix = 'HB - ';
|
const prefix = 'HB - ';
|
||||||
|
|
||||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||||
@@ -188,13 +160,29 @@ app.get('/download/:id', asyncHandler(async (req, res)=>{
|
|||||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
||||||
});
|
});
|
||||||
res.status(200).send(brew.text);
|
res.status(200).send(brew.text);
|
||||||
}));
|
});
|
||||||
|
|
||||||
//User Page
|
//User Page
|
||||||
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);
|
||||||
|
|
||||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
const fields = [
|
||||||
|
'googleId',
|
||||||
|
'title',
|
||||||
|
'pageCount',
|
||||||
|
'description',
|
||||||
|
'authors',
|
||||||
|
'published',
|
||||||
|
'views',
|
||||||
|
'shareId',
|
||||||
|
'editId',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'lastViewed',
|
||||||
|
'tags'
|
||||||
|
];
|
||||||
|
|
||||||
|
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
@@ -206,62 +194,97 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(googleBrews) {
|
if(googleBrews && googleBrews.length > 0) {
|
||||||
|
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
||||||
|
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
||||||
|
if(match !== -1) {
|
||||||
|
brew.googleId = googleBrews[match].googleId;
|
||||||
|
brew.stubbed = true;
|
||||||
|
brew.pageCount = googleBrews[match].pageCount;
|
||||||
|
brew.renderer = googleBrews[match].renderer;
|
||||||
|
brew.version = googleBrews[match].version;
|
||||||
|
googleBrews.splice(match, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||||
brews = _.concat(brews, googleBrews);
|
brews = _.concat(brews, googleBrews);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
req.brews = _.map(brews, (brew)=>{
|
req.brews = _.map(brews, (brew)=>{
|
||||||
return sanitizeBrew(brew, !ownAccount);
|
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
|
||||||
});
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||||
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
|
sanitizeBrew(req.brew, 'edit');
|
||||||
|
splitTextStyleAndMetadata(req.brew);
|
||||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
|
||||||
req.brew = brew;
|
|
||||||
return next();
|
return next();
|
||||||
}));
|
});
|
||||||
|
|
||||||
//New Page
|
//New Page
|
||||||
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||||
const brew = await getBrewFromId(req.params.id, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
brew.title = `CLONE - ${brew.title}`;
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew = brew;
|
req.brew.title = `CLONE - ${req.brew.title}`;
|
||||||
return next();
|
return next();
|
||||||
}));
|
});
|
||||||
|
|
||||||
//Share Page
|
//Share Page
|
||||||
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
const brew = await getBrewFromId(req.params.id, 'share');
|
const { brew } = req;
|
||||||
|
|
||||||
if(req.params.id.length > 12) {
|
if(req.params.id.length > 12 && !brew._id) {
|
||||||
const googleId = req.params.id.slice(0, -12);
|
const googleId = req.params.id.slice(0, -12);
|
||||||
const shareId = req.params.id.slice(-12);
|
const shareId = req.params.id.slice(-12);
|
||||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||||
.catch((err)=>{next(err);});
|
.catch((err)=>{next(err);});
|
||||||
} else {
|
} else {
|
||||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||||
}
|
}
|
||||||
|
sanitizeBrew(req.brew, 'share');
|
||||||
req.brew = brew;
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
//Print Page
|
//Print Page
|
||||||
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||||
const brew = await getBrewFromId(req.params.id, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
req.brew = brew;
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
next();
|
||||||
}));
|
});
|
||||||
|
|
||||||
|
const nodeEnv = config.get('node_env');
|
||||||
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
|
// Local only
|
||||||
|
if(isLocalEnvironment){
|
||||||
|
// Login
|
||||||
|
app.post('/local/login', (req, res)=>{
|
||||||
|
const username = req.body.username;
|
||||||
|
if(!username) return;
|
||||||
|
|
||||||
|
const payload = jwt.encode({ username: username, issued: new Date }, config.get('secret'));
|
||||||
|
return res.json(payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Render the page
|
//Render the page
|
||||||
const templateFn = require('./../client/template.js');
|
const templateFn = require('./../client/template.js');
|
||||||
app.use((req, res)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
|
// Create configuration object
|
||||||
|
const configuration = {
|
||||||
|
local : isLocalEnvironment,
|
||||||
|
publicUrl : config.get('publicUrl') ?? '',
|
||||||
|
environment : nodeEnv
|
||||||
|
};
|
||||||
const props = {
|
const props = {
|
||||||
version : require('./../package.json').version,
|
version : require('./../package.json').version,
|
||||||
url : req.originalUrl,
|
url : req.originalUrl,
|
||||||
@@ -269,16 +292,18 @@ app.use((req, res)=>{
|
|||||||
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'),
|
||||||
|
config : configuration
|
||||||
};
|
};
|
||||||
const title = req.brew ? req.brew.title : '';
|
const title = req.brew ? req.brew.title : '';
|
||||||
templateFn('homebrew', title, props)
|
const page = await templateFn('homebrew', title, props)
|
||||||
.then((page)=>{ res.send(page); })
|
.catch((err)=>{
|
||||||
.catch((err)=>{
|
console.log(err);
|
||||||
console.log(err);
|
return res.sendStatus(500);
|
||||||
return res.sendStatus(500);
|
});
|
||||||
});
|
if(!page) return;
|
||||||
});
|
res.send(page);
|
||||||
|
}));
|
||||||
|
|
||||||
//v=====----- Error-Handling Middleware -----=====v//
|
//v=====----- Error-Handling Middleware -----=====v//
|
||||||
//Format Errors so all fields will be sent
|
//Format Errors so all fields will be sent
|
||||||
@@ -302,6 +327,13 @@ app.use((err, req, res, next)=>{
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(status).send(getPureError(err));
|
res.status(status).send(getPureError(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use((req, res)=>{
|
||||||
|
if(!res.headersSent) {
|
||||||
|
console.error('Headers have not been sent, responding with a server error.', req.url);
|
||||||
|
res.status(500).send('An error occurred and the server did not send a response. The error has been logged, please note the time this occurred and report this issue.');
|
||||||
|
}
|
||||||
|
});
|
||||||
//^=====--------------------------------------=====^//
|
//^=====--------------------------------------=====^//
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@@ -124,9 +124,9 @@ const GoogleActions = {
|
|||||||
title : file.properties.title,
|
title : file.properties.title,
|
||||||
description : file.description,
|
description : file.description,
|
||||||
views : parseInt(file.properties.views),
|
views : parseInt(file.properties.views),
|
||||||
tags : '',
|
|
||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
systems : []
|
systems : [],
|
||||||
|
thumbnail : file.properties.thumbnail
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return brews;
|
return brews;
|
||||||
@@ -142,12 +142,12 @@ const GoogleActions = {
|
|||||||
description : `${brew.description}`,
|
description : `${brew.description}`,
|
||||||
properties : {
|
properties : {
|
||||||
title : brew.title,
|
title : brew.title,
|
||||||
published : brew.published,
|
shareId : brew.shareId || nanoid(12),
|
||||||
version : brew.version,
|
editId : brew.editId || nanoid(12),
|
||||||
renderer : brew.renderer,
|
|
||||||
tags : brew.tags,
|
|
||||||
pageCount : brew.pageCount,
|
pageCount : brew.pageCount,
|
||||||
systems : brew.systems.join()
|
renderer : brew.renderer || 'legacy',
|
||||||
|
isStubbed : true,
|
||||||
|
thumbnail : brew.thumbnail
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
media : {
|
media : {
|
||||||
@@ -159,10 +159,9 @@ const GoogleActions = {
|
|||||||
console.log('Error saving to google');
|
console.log('Error saving to google');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw (err);
|
throw (err);
|
||||||
//return res.status(500).send('Error while saving');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return (brew);
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
newGoogleBrew : async (auth, brew)=>{
|
newGoogleBrew : async (auth, brew)=>{
|
||||||
@@ -176,16 +175,18 @@ const GoogleActions = {
|
|||||||
const folderId = await GoogleActions.getGoogleFolder(auth);
|
const folderId = await GoogleActions.getGoogleFolder(auth);
|
||||||
|
|
||||||
const fileMetadata = {
|
const fileMetadata = {
|
||||||
'name' : `${brew.title}.txt`,
|
name : `${brew.title}.txt`,
|
||||||
'description' : `${brew.description}`,
|
description : `${brew.description}`,
|
||||||
'parents' : [folderId],
|
parents : [folderId],
|
||||||
'properties' : { //AppProperties is not accessible
|
properties : { //AppProperties is not accessible
|
||||||
'shareId' : brew.shareId || nanoid(12),
|
shareId : brew.shareId || nanoid(12),
|
||||||
'editId' : brew.editId || nanoid(12),
|
editId : brew.editId || nanoid(12),
|
||||||
'title' : brew.title,
|
title : brew.title,
|
||||||
'views' : '0',
|
pageCount : brew.pageCount,
|
||||||
'pageCount' : brew.pageCount,
|
renderer : brew.renderer || 'legacy',
|
||||||
'renderer' : brew.renderer || 'legacy'
|
isStubbed : true,
|
||||||
|
version : 1,
|
||||||
|
thumbnail : brew.thumbnail || ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,26 +213,7 @@ const GoogleActions = {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
const newHomebrew = {
|
return obj.data.id;
|
||||||
text : brew.text,
|
|
||||||
shareId : fileMetadata.properties.shareId,
|
|
||||||
editId : fileMetadata.properties.editId,
|
|
||||||
createdAt : new Date(),
|
|
||||||
updatedAt : new Date(),
|
|
||||||
gDrive : true,
|
|
||||||
googleId : obj.data.id,
|
|
||||||
pageCount : fileMetadata.properties.pageCount,
|
|
||||||
|
|
||||||
title : brew.title,
|
|
||||||
description : brew.description,
|
|
||||||
tags : '',
|
|
||||||
published : brew.published,
|
|
||||||
renderer : brew.renderer,
|
|
||||||
authors : [],
|
|
||||||
systems : []
|
|
||||||
};
|
|
||||||
|
|
||||||
return newHomebrew;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||||
@@ -244,7 +226,6 @@ const GoogleActions = {
|
|||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error loading from Google');
|
console.log('Error loading from Google');
|
||||||
throw (err);
|
throw (err);
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(obj) {
|
if(obj) {
|
||||||
@@ -254,9 +235,7 @@ const GoogleActions = {
|
|||||||
throw ('Share ID does not match');
|
throw ('Share ID does not match');
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceDrive = google.drive({ version: 'v3' });
|
const file = await drive.files.get({
|
||||||
|
|
||||||
const file = await serviceDrive.files.get({
|
|
||||||
fileId : id,
|
fileId : id,
|
||||||
fields : 'description, properties',
|
fields : 'description, properties',
|
||||||
alt : 'media'
|
alt : 'media'
|
||||||
@@ -273,7 +252,7 @@ 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 : '',
|
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 : [],
|
||||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||||
@@ -286,8 +265,8 @@ const GoogleActions = {
|
|||||||
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
views : parseInt(obj.data.properties.views) || 0, //brews with no view parameter will return undefined
|
||||||
version : parseInt(obj.data.properties.version) || 0,
|
version : parseInt(obj.data.properties.version) || 0,
|
||||||
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
renderer : obj.data.properties.renderer ? obj.data.properties.renderer : 'legacy',
|
||||||
|
thumbnail : obj.data.properties.thumbnail || '',
|
||||||
|
|
||||||
gDrive : true,
|
|
||||||
googleId : id
|
googleId : id
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -295,14 +274,11 @@ const GoogleActions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteGoogleBrew : async (auth, id)=>{
|
deleteGoogleBrew : async (auth, id, accessId)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const googleId = id.slice(0, -12);
|
|
||||||
const accessId = id.slice(-12);
|
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : googleId,
|
fileId : id,
|
||||||
fields : 'properties'
|
fields : 'properties'
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
@@ -311,11 +287,11 @@ const GoogleActions = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if(obj && obj.data.properties.editId != accessId) {
|
if(obj && obj.data.properties.editId != accessId) {
|
||||||
throw ('Not authorized to delete this Google brew');
|
throw { status: 403, message: 'Not authorized to delete this Google brew' };
|
||||||
}
|
}
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
fileId : googleId,
|
fileId : id,
|
||||||
resource : { trashed: true }
|
resource : { trashed: true }
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const HomebrewModel = require('./homebrew.model.js').model;
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
const router = require('express').Router();
|
const router = require('express').Router();
|
||||||
@@ -5,6 +6,8 @@ const zlib = require('zlib');
|
|||||||
const GoogleActions = require('./googleActions.js');
|
const GoogleActions = require('./googleActions.js');
|
||||||
const Markdown = require('../shared/naturalcrit/markdown.js');
|
const Markdown = require('../shared/naturalcrit/markdown.js');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
|
const asyncHandler = require('express-async-handler');
|
||||||
|
const { nanoid } = require('nanoid');
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -12,6 +15,63 @@ const yaml = require('js-yaml');
|
|||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
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)=>{
|
const mergeBrewText = (brew)=>{
|
||||||
let text = brew.text;
|
let text = brew.text;
|
||||||
if(brew.style !== undefined) {
|
if(brew.style !== undefined) {
|
||||||
@@ -32,15 +92,33 @@ const MAX_TITLE_LENGTH = 100;
|
|||||||
|
|
||||||
const getGoodBrewTitle = (text)=>{
|
const getGoodBrewTitle = (text)=>{
|
||||||
const tokens = Markdown.marked.lexer(text);
|
const tokens = Markdown.marked.lexer(text);
|
||||||
return (tokens.find((token)=>token.type == 'heading' || token.type == 'paragraph')?.text || 'No Title')
|
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||||
.slice(0, MAX_TITLE_LENGTH);
|
.slice(0, MAX_TITLE_LENGTH);
|
||||||
};
|
};
|
||||||
|
|
||||||
const excludePropsFromUpdate = (brew)=>{
|
const excludePropsFromUpdate = (brew)=>{
|
||||||
// Remove undesired properties
|
// Remove undesired properties
|
||||||
const propsToExclude = ['views', 'lastViewed'];
|
const modified = _.clone(brew);
|
||||||
|
const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
delete brew[prop];
|
delete modified[prop];
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
};
|
||||||
|
|
||||||
|
const excludeGoogleProps = (brew)=>{
|
||||||
|
const modified = _.clone(brew);
|
||||||
|
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
delete modified[prop];
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
};
|
||||||
|
|
||||||
|
const excludeStubProps = (brew)=>{
|
||||||
|
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
brew[prop] = undefined;
|
||||||
}
|
}
|
||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
@@ -54,33 +132,17 @@ const beforeNewSave = (account, brew)=>{
|
|||||||
brew.text = mergeBrewText(brew);
|
brew.text = mergeBrewText(brew);
|
||||||
};
|
};
|
||||||
|
|
||||||
const newLocalBrew = async (brew)=>{
|
|
||||||
const newHomebrew = new HomebrewModel(brew);
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
let saved = await newHomebrew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err, err.toString(), err.stack);
|
|
||||||
throw `Error while creating new brew, ${err.toString()}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
saved = saved.toObject();
|
|
||||||
saved.gDrive = false;
|
|
||||||
return saved;
|
|
||||||
};
|
|
||||||
|
|
||||||
const newGoogleBrew = async (account, brew, res)=>{
|
const newGoogleBrew = async (account, brew, res)=>{
|
||||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
|
|
||||||
return await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
const newBrew = excludeGoogleProps(brew);
|
||||||
|
|
||||||
|
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||||
};
|
};
|
||||||
|
|
||||||
const newBrew = async (req, res)=>{
|
const newBrew = async (req, res)=>{
|
||||||
const brew = req.body;
|
const brew = req.body;
|
||||||
const { transferToGoogle } = req.query;
|
const { saveToGoogle } = req.query;
|
||||||
|
|
||||||
delete brew.editId;
|
delete brew.editId;
|
||||||
delete brew.shareId;
|
delete brew.shareId;
|
||||||
@@ -88,148 +150,189 @@ const newBrew = async (req, res)=>{
|
|||||||
|
|
||||||
beforeNewSave(req.account, brew);
|
beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
let saved;
|
const newHomebrew = new HomebrewModel(brew);
|
||||||
if(transferToGoogle) {
|
newHomebrew.editId = nanoid(12);
|
||||||
saved = await newGoogleBrew(req.account, brew, res)
|
newHomebrew.shareId = nanoid(12);
|
||||||
|
|
||||||
|
let googleId, saved;
|
||||||
|
if(saveToGoogle) {
|
||||||
|
googleId = await newGoogleBrew(req.account, newHomebrew, res)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
res.status(err.status || err.response.status).send(err.message || 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 {
|
} else {
|
||||||
saved = await newLocalBrew(brew)
|
// Compress brew text to binary before saving
|
||||||
.catch((err)=>{
|
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||||
res.status(500).send(err);
|
// 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;
|
if(!saved) return;
|
||||||
return res.status(200).send(saved);
|
saved = saved.toObject();
|
||||||
|
|
||||||
|
res.status(200).send(saved);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateBrew = async (req, res)=>{
|
const updateBrew = async (req, res)=>{
|
||||||
let brew = excludePropsFromUpdate(req.body);
|
// 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
|
||||||
const { transferToGoogle, transferFromGoogle } = req.query;
|
let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
|
||||||
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
|
const googleId = brew.googleId;
|
||||||
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
let saved;
|
brew.text = mergeBrewText(brew);
|
||||||
if(brew.googleId && transferFromGoogle) {
|
|
||||||
beforeNewSave(req.account, brew);
|
|
||||||
|
|
||||||
saved = await newLocalBrew(brew)
|
if(brew.googleId && removeFromGoogle) {
|
||||||
.catch((err)=>{
|
// 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
|
||||||
console.error(err);
|
afterSave = async ()=>{
|
||||||
res.status(500).send(err);
|
return await deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||||
});
|
.catch((err)=>{
|
||||||
if(!saved) return;
|
console.error(err);
|
||||||
|
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
await deleteGoogleBrew(req.account, `${brew.googleId}${brew.editId}`, res)
|
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)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || err.response.status).send(err.message || err);
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
});
|
});
|
||||||
} else if(!brew.googleId && transferToGoogle) {
|
if(!brew.googleId) return;
|
||||||
saved = await newGoogleBrew(req.account, brew, res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || err.response.status).send(err.message || err);
|
|
||||||
});
|
|
||||||
if(!saved) return;
|
|
||||||
|
|
||||||
await deleteLocalBrew(req.account, brew.editId)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status).send(err.message);
|
|
||||||
});
|
|
||||||
} else if(brew.googleId) {
|
} else if(brew.googleId) {
|
||||||
brew.text = mergeBrewText(brew);
|
// If the google id exists and no other actions are being performed, update the google brew
|
||||||
|
const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew))
|
||||||
saved = await GoogleActions.updateGoogleBrew(brew)
|
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.response?.status || 500).send(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 {
|
} else {
|
||||||
const dbBrew = await HomebrewModel.get({ editId: req.params.id })
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
return res.status(500).send('Error while saving');
|
|
||||||
});
|
|
||||||
|
|
||||||
brew = _.merge(dbBrew, brew);
|
|
||||||
brew.text = mergeBrewText(brew);
|
|
||||||
|
|
||||||
// Compress brew text to binary before saving
|
// Compress brew text to binary before saving
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
brew.text = undefined;
|
brew.text = undefined;
|
||||||
brew.updatedAt = new Date();
|
|
||||||
|
|
||||||
if(req.account) {
|
|
||||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.markModified('authors');
|
|
||||||
brew.markModified('systems');
|
|
||||||
|
|
||||||
saved = await brew.save();
|
|
||||||
}
|
}
|
||||||
|
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;
|
if(!saved) return;
|
||||||
|
// Call and wait for afterSave to complete
|
||||||
|
const after = await afterSave();
|
||||||
|
if(!after) return;
|
||||||
|
|
||||||
if(!res.headersSent) return res.status(200).send(saved);
|
res.status(200).send(saved);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteBrew = async (req, res)=>{
|
const deleteGoogleBrew = async (account, id, editId, res)=>{
|
||||||
if(req.params.id.length > 12) {
|
|
||||||
const deleted = await deleteGoogleBrew(req.account, req.params.id, res)
|
|
||||||
.catch((err)=>{
|
|
||||||
res.status(500).send(err);
|
|
||||||
});
|
|
||||||
if(deleted) return res.status(200).send();
|
|
||||||
} else {
|
|
||||||
const deleted = await deleteLocalBrew(req.account, req.params.id)
|
|
||||||
.catch((err)=>{
|
|
||||||
res.status(err.status).send(err.message);
|
|
||||||
});
|
|
||||||
if(deleted) return res.status(200).send(deleted);
|
|
||||||
return res.status(200).send();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteLocalBrew = async (account, id)=>{
|
|
||||||
const brew = await HomebrewModel.findOne({ editId: id });
|
|
||||||
if(!brew) {
|
|
||||||
throw { status: 404, message: 'Can not find homebrew with that id' };
|
|
||||||
}
|
|
||||||
|
|
||||||
if(account) {
|
|
||||||
// Remove current user as author
|
|
||||||
brew.authors = _.pull(brew.authors, account.username);
|
|
||||||
brew.markModified('authors');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(brew.authors.length === 0) {
|
|
||||||
// Delete brew if there are no authors left
|
|
||||||
await brew.remove()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
throw { status: 500, message: 'Error while removing' };
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Otherwise, save the brew with updated author list
|
|
||||||
return await brew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
throw { status: 500, message: err };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteGoogleBrew = async (account, id, res)=>{
|
|
||||||
const auth = await GoogleActions.authCheck(account, res);
|
const auth = await GoogleActions.authCheck(account, res);
|
||||||
await GoogleActions.deleteGoogleBrew(auth, id);
|
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', newBrew);
|
const deleteBrew = async (req, res, next)=>{
|
||||||
router.put('/api/:id', updateBrew);
|
// Delete an orphaned stub if its Google brew doesn't exist
|
||||||
router.put('/api/update/:id', updateBrew);
|
try {
|
||||||
router.delete('/api/:id', deleteBrew);
|
await getBrew('edit')(req, res, ()=>{});
|
||||||
router.get('/api/remove/:id', deleteBrew);
|
} 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();
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
let brew = req.brew;
|
||||||
|
const { googleId, editId } = brew;
|
||||||
|
const account = req.account;
|
||||||
|
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
|
||||||
|
// If the user is the owner and the file is saved to google, mark the google brew for deletion
|
||||||
|
const shouldDeleteGoogleBrew = googleId && isOwner;
|
||||||
|
|
||||||
|
if(brew._id) {
|
||||||
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
if(account) {
|
||||||
|
// Remove current user as author
|
||||||
|
brew.authors = _.pull(brew.authors, account.username);
|
||||||
|
brew.markModified('authors');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(brew.authors.length === 0) {
|
||||||
|
// Delete brew if there are no authors left
|
||||||
|
await brew.remove()
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
throw { status: 500, message: 'Error while removing' };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if(shouldDeleteGoogleBrew) {
|
||||||
|
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
||||||
|
brew.googleId = undefined;
|
||||||
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
|
brew.text = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, save the brew with updated author list
|
||||||
|
await brew.save()
|
||||||
|
.catch((err)=>{
|
||||||
|
throw { status: 500, message: err };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(shouldDeleteGoogleBrew) {
|
||||||
|
const deleted = await deleteGoogleBrew(account, googleId, editId, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
if(!deleted) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/api', asyncHandler(newBrew));
|
||||||
|
router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
||||||
|
router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
||||||
|
router.delete('/api/:id', asyncHandler(deleteBrew));
|
||||||
|
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
homebrewApi : router,
|
||||||
|
getBrew
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,17 +6,19 @@ const zlib = require('zlib');
|
|||||||
const HomebrewSchema = mongoose.Schema({
|
const HomebrewSchema = mongoose.Schema({
|
||||||
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||||
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||||
|
googleId : { type: String },
|
||||||
title : { type: String, default: '' },
|
title : { type: String, default: '' },
|
||||||
text : { type: String, default: '' },
|
text : { type: String, default: '' },
|
||||||
textBin : { type: Buffer },
|
textBin : { type: Buffer },
|
||||||
pageCount : { type: Number, default: 1 },
|
pageCount : { type: Number, default: 1 },
|
||||||
|
|
||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : { type: String, default: '' },
|
tags : [String],
|
||||||
systems : [String],
|
systems : [String],
|
||||||
renderer : { type: String, default: '' },
|
renderer : { type: String, default: '' },
|
||||||
authors : [String],
|
authors : [String],
|
||||||
published : { type: Boolean, default: false },
|
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 },
|
||||||
@@ -36,9 +38,9 @@ HomebrewSchema.statics.increaseView = async function(query) {
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.get = function(query){
|
HomebrewSchema.statics.get = function(query, fields=null){
|
||||||
return new Promise((resolve, reject)=>{
|
return new Promise((resolve, reject)=>{
|
||||||
Homebrew.find(query, (err, brews)=>{
|
Homebrew.find(query, fields, null, (err, brews)=>{
|
||||||
if(err || !brews.length) return reject('Can not find brew');
|
if(err || !brews.length) return reject('Can not find brew');
|
||||||
if(!_.isNil(brews[0].textBin)) { // Uncompress zipped text field
|
if(!_.isNil(brews[0].textBin)) { // Uncompress zipped text field
|
||||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||||
@@ -51,13 +53,13 @@ HomebrewSchema.statics.get = function(query){
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
|
HomebrewSchema.statics.getByUser = function(username, allowAccess=false, fields=null){
|
||||||
return new Promise((resolve, reject)=>{
|
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;
|
||||||
}
|
}
|
||||||
Homebrew.find(query).lean().exec((err, brews)=>{ //lean() converts results to JSObjects
|
Homebrew.find(query, fields).lean().exec((err, brews)=>{ //lean() converts results to JSObjects
|
||||||
if(err) return reject('Can not find brew');
|
if(err) return reject('Can not find brew');
|
||||||
return resolve(brews);
|
return resolve(brews);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ if(typeof navigator !== 'undefined'){
|
|||||||
// require('codemirror/addon/edit/trailingspace.js');
|
// require('codemirror/addon/edit/trailingspace.js');
|
||||||
//Active line highlighting
|
//Active line highlighting
|
||||||
// require('codemirror/addon/selection/active-line.js');
|
// require('codemirror/addon/selection/active-line.js');
|
||||||
|
//Scroll past last line
|
||||||
|
require('codemirror/addon/scroll/scrollpastend.js');
|
||||||
//Auto-closing
|
//Auto-closing
|
||||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
//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/fold/xml-fold.js');
|
||||||
@@ -98,6 +100,7 @@ const CodeEditor = createClass({
|
|||||||
indentWithTabs : true,
|
indentWithTabs : true,
|
||||||
tabSize : 2,
|
tabSize : 2,
|
||||||
historyEventDelay : 250,
|
historyEventDelay : 250,
|
||||||
|
scrollPastEnd : true,
|
||||||
extraKeys : {
|
extraKeys : {
|
||||||
'Ctrl-B' : this.makeBold,
|
'Ctrl-B' : this.makeBold,
|
||||||
'Cmd-B' : this.makeBold,
|
'Cmd-B' : this.makeBold,
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
@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';
|
||||||
|
|
||||||
|
@keyframes sourceMoveAnimation {
|
||||||
|
50% {background-color: red; color: white;}
|
||||||
|
100% {background-color: unset; color: unset;}
|
||||||
|
}
|
||||||
|
|
||||||
.codeEditor{
|
.codeEditor{
|
||||||
.CodeMirror-foldmarker {
|
.CodeMirror-foldmarker {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -10,6 +15,11 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sourceMoveFlash .CodeMirror-line{
|
||||||
|
animation-name: sourceMoveAnimation;
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
//.cm-tab {
|
//.cm-tab {
|
||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
//}
|
//}
|
||||||
@@ -19,4 +29,4 @@
|
|||||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ nav{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navItem{
|
.navItem{
|
||||||
#backgroundColors;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
padding : 8px 12px;
|
padding : 8px 12px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ const SplitPane = createClass({
|
|||||||
return {
|
return {
|
||||||
currentDividerPos : null,
|
currentDividerPos : null,
|
||||||
windowWidth : 0,
|
windowWidth : 0,
|
||||||
isDragging : false
|
isDragging : false,
|
||||||
|
moveSource : false,
|
||||||
|
moveBrew : false,
|
||||||
|
showMoveArrows : true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -29,6 +32,11 @@ const SplitPane = createClass({
|
|||||||
userSetDividerPos : dividerPos,
|
userSetDividerPos : dividerPos,
|
||||||
windowWidth : window.innerWidth
|
windowWidth : window.innerWidth
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
currentDividerPos : window.innerWidth / 2,
|
||||||
|
userSetDividerPos : window.innerWidth / 2
|
||||||
|
});
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', this.handleWindowResize);
|
window.addEventListener('resize', this.handleWindowResize);
|
||||||
},
|
},
|
||||||
@@ -83,20 +91,58 @@ const SplitPane = createClass({
|
|||||||
window.getSelection().removeAllRanges();
|
window.getSelection().removeAllRanges();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
setMoveArrows : function(newState) {
|
||||||
|
if(this.state.showMoveArrows != newState){
|
||||||
|
this.setState({
|
||||||
|
showMoveArrows : newState
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMoveArrows : function(){
|
||||||
|
if(this.state.showMoveArrows) {
|
||||||
|
return <>
|
||||||
|
<div className='arrow left'
|
||||||
|
style={{ left: this.state.currentDividerPos-4 }}
|
||||||
|
onClick={()=>this.setState({ moveSource: !this.state.moveSource })} >
|
||||||
|
<i className='fas fa-arrow-left' />
|
||||||
|
</div>
|
||||||
|
<div className='arrow right'
|
||||||
|
style={{ left: this.state.currentDividerPos-4 }}
|
||||||
|
onClick={()=>this.setState({ moveBrew: !this.state.moveBrew })} >
|
||||||
|
<i className='fas fa-arrow-right' />
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
renderDivider : function(){
|
renderDivider : function(){
|
||||||
return <div className='divider' onMouseDown={this.handleDown} >
|
return <>
|
||||||
<div className='dots'>
|
{this.renderMoveArrows()}
|
||||||
<i className='fas fa-circle' />
|
<div className='divider' onMouseDown={this.handleDown} >
|
||||||
<i className='fas fa-circle' />
|
<div className='dots'>
|
||||||
<i className='fas fa-circle' />
|
<i className='fas fa-circle' />
|
||||||
|
<i className='fas fa-circle' />
|
||||||
|
<i className='fas fa-circle' />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
||||||
<Pane ref='pane1' width={this.state.currentDividerPos}>{this.props.children[0]}</Pane>
|
<Pane
|
||||||
|
ref='pane1'
|
||||||
|
width={this.state.currentDividerPos}
|
||||||
|
>
|
||||||
|
{React.cloneElement(this.props.children[0], {
|
||||||
|
moveBrew : this.state.moveBrew,
|
||||||
|
moveSource : this.state.moveSource,
|
||||||
|
setMoveArrows : this.setMoveArrows
|
||||||
|
})}
|
||||||
|
</Pane>
|
||||||
{this.renderDivider()}
|
{this.renderDivider()}
|
||||||
<Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
|
<Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
|
||||||
</div>;
|
</div>;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
.divider{
|
.divider{
|
||||||
display : table;
|
display : table;
|
||||||
height : 100%;
|
height : 100%;
|
||||||
width : 12px;
|
width : 15px;
|
||||||
cursor : ew-resize;
|
cursor : ew-resize;
|
||||||
background-color : #bbb;
|
background-color : #bbb;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
@@ -32,4 +32,28 @@
|
|||||||
background-color: #999;
|
background-color: #999;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.arrow{
|
||||||
|
position : absolute;
|
||||||
|
width : 25px;
|
||||||
|
height : 25px;
|
||||||
|
border : 2px solid #bbb;
|
||||||
|
border-radius : 15px;
|
||||||
|
text-align : center;
|
||||||
|
font-size : 1.2em;
|
||||||
|
cursor : pointer;
|
||||||
|
background-color : #ddd;
|
||||||
|
z-index : 999;
|
||||||
|
box-shadow : 0 4px 5px #0000007f;
|
||||||
|
&.left{
|
||||||
|
.tooltipLeft('Jump to location in Editor');
|
||||||
|
top : 30px;
|
||||||
|
}
|
||||||
|
&.right{
|
||||||
|
.tooltipRight('Jump to location in Preview');
|
||||||
|
top : 60px;
|
||||||
|
}
|
||||||
|
&:hover{
|
||||||
|
background-color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,29 @@
|
|||||||
@grey : #7F8C8D;
|
@grey : #7F8C8D;
|
||||||
|
|
||||||
#backgroundColors {
|
#backgroundColors {
|
||||||
|
&.tealLight{ background-color : @tealLight };
|
||||||
|
&.teal{ background-color : @teal };
|
||||||
|
&.greenLight{ background-color : @greenLight };
|
||||||
|
&.green{ background-color : @green };
|
||||||
|
&.blueLight{ background-color : @blueLight };
|
||||||
|
&.blue{ background-color : @blue };
|
||||||
|
&.purpleLight{ background-color : @purpleLight };
|
||||||
|
&.purple{ background-color : @purple };
|
||||||
|
&.steelLight{ background-color : @steelLight };
|
||||||
|
&.steel{ background-color : @steel };
|
||||||
|
&.yellowLight{ background-color : @yellowLight };
|
||||||
|
&.yellow{ background-color : @yellow };
|
||||||
|
&.orangeLight{ background-color : @orangeLight };
|
||||||
|
&.orange{ background-color : @orange };
|
||||||
|
&.redLight{ background-color : @redLight };
|
||||||
|
&.red{ background-color : @red };
|
||||||
|
&.silverLight{ background-color : @silverLight };
|
||||||
|
&.silver{ background-color : @silver };
|
||||||
|
&.greyLight{ background-color : @greyLight };
|
||||||
|
&.grey{ background-color : @grey };
|
||||||
|
}
|
||||||
|
|
||||||
|
#backgroundColorsHover {
|
||||||
&.tealLight:hover{ background-color : @tealLight };
|
&.tealLight:hover{ background-color : @tealLight };
|
||||||
&.teal:hover{ background-color : @teal };
|
&.teal:hover{ background-color : @teal };
|
||||||
&.greenLight:hover{ background-color : @greenLight };
|
&.greenLight:hover{ background-color : @greenLight };
|
||||||
|
|||||||
@@ -545,7 +545,6 @@ body {
|
|||||||
-webkit-column-break-after : always;
|
-webkit-column-break-after : always;
|
||||||
break-after : always;
|
break-after : always;
|
||||||
-moz-column-break-after : always;
|
-moz-column-break-after : always;
|
||||||
break-before : column;
|
|
||||||
}
|
}
|
||||||
//Avoid breaking up
|
//Avoid breaking up
|
||||||
blockquote,table{
|
blockquote,table{
|
||||||
@@ -604,8 +603,8 @@ body {
|
|||||||
white-space : nowrap;
|
white-space : nowrap;
|
||||||
}
|
}
|
||||||
&.frame {
|
&.frame {
|
||||||
margin-top : 0.66cm;
|
margin-top : 0.7cm;
|
||||||
margin-bottom : 1.05cm;
|
margin-bottom : 0.9cm;
|
||||||
margin-left : -0.1cm;
|
margin-left : -0.1cm;
|
||||||
margin-right : -0.1cm;
|
margin-right : -0.1cm;
|
||||||
width : calc(100% + 0.2cm);
|
width : calc(100% + 0.2cm);
|
||||||
@@ -613,34 +612,38 @@ body {
|
|||||||
background-color : white;
|
background-color : white;
|
||||||
border : initial;
|
border : initial;
|
||||||
border-style : solid;
|
border-style : solid;
|
||||||
border-image-outset : 0.55cm 0.3cm;
|
border-image-outset : 0.4cm 0.3cm;
|
||||||
border-image-repeat : stretch;
|
border-image-repeat : stretch;
|
||||||
border-image-slice : 200;
|
border-image-slice : 200;
|
||||||
border-image-source : @frameBorderImage;
|
border-image-source : @frameBorderImage;
|
||||||
border-image-width : 47px;
|
border-image-width : 47px;
|
||||||
|
&.wide:first-child {
|
||||||
|
margin-top: 0.12cm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.decoration {
|
&.decoration {
|
||||||
transform-style : preserve-3d;
|
|
||||||
z-index: -1;
|
|
||||||
position:relative;
|
position:relative;
|
||||||
}
|
}
|
||||||
&.decoration::before {
|
&.decoration::before {
|
||||||
content :'';
|
content :'';
|
||||||
position : absolute;
|
position : absolute;
|
||||||
background-image : @classTableDecoration;
|
background-image : @classTableDecoration,
|
||||||
background-size : contain;
|
@classTableDecoration;
|
||||||
background-repeat : space;
|
background-size : contain, contain;
|
||||||
width : 7.75cm;
|
background-repeat : no-repeat, no-repeat;
|
||||||
height : calc(100% + 3.3cm);
|
background-position : top, bottom;
|
||||||
top : 50%;
|
width : 7.75cm;
|
||||||
left : 50%;
|
height : calc(100% + 3.3cm);
|
||||||
transform : translateY(-50%) translateX(-50%) translateZ(-1px);
|
top : 50%;
|
||||||
filter : drop-shadow(0px 0px 1px #C8C5C080)
|
left : 50%;
|
||||||
|
transform : translateY(-50%) translateX(-50%);
|
||||||
|
filter : drop-shadow(0px 0px 1px #C8C5C080);
|
||||||
|
z-index : -1;
|
||||||
}
|
}
|
||||||
&.decoration.wide::before {
|
&.decoration.wide::before {
|
||||||
width : calc(100% + 3.3cm);
|
width : calc(100% + 3.3cm);
|
||||||
height : 7.75cm;
|
height : 7.75cm;
|
||||||
top : calc(50% + 0.4cm);
|
background-position : left, right;
|
||||||
}
|
}
|
||||||
h5 + table{
|
h5 + table{
|
||||||
margin-top : 0.2cm;
|
margin-top : 0.2cm;
|
||||||
|
|||||||
BIN
themes/fonts/5e/Nodesto Caps Condensed Bold Italic.woff2
Normal file
BIN
themes/fonts/5e/Nodesto Caps Condensed Bold Italic.woff2
Normal file
Binary file not shown.
BIN
themes/fonts/5e/Nodesto Caps Condensed Bold.woff2
Normal file
BIN
themes/fonts/5e/Nodesto Caps Condensed Bold.woff2
Normal file
Binary file not shown.
BIN
themes/fonts/5e/Nodesto Caps Condensed Italic.woff2
Normal file
BIN
themes/fonts/5e/Nodesto Caps Condensed Italic.woff2
Normal file
Binary file not shown.
BIN
themes/fonts/5e/Nodesto Caps Condensed.woff2
Normal file
BIN
themes/fonts/5e/Nodesto Caps Condensed.woff2
Normal file
Binary file not shown.
@@ -77,3 +77,32 @@
|
|||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cover Page */
|
||||||
|
@font-face {
|
||||||
|
font-family: NodestoCapsCondensed;
|
||||||
|
src: url('../fonts/5e/Nodesto Caps Condensed.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: NodestoCapsCondensed;
|
||||||
|
src: url('../fonts/5e/Nodesto Caps Condensed Bold.woff2');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: NodestoCapsCondensed;
|
||||||
|
src: url('../fonts/5e/Nodesto Caps Condensed Italic.woff2');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: NodestoCapsCondensed;
|
||||||
|
src: url('../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user