mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-26 13:53:02 +00:00
Compare commits
443 Commits
recolorCSS
...
3.1.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1045867d73 | ||
|
|
0061e44339 | ||
|
|
29eb2fe39e | ||
|
|
f9af27b159 | ||
|
|
86f0abbfd1 | ||
|
|
e2a46c4883 | ||
|
|
389bcf1bbd | ||
|
|
b3ccbc183d | ||
|
|
362cb4f728 | ||
|
|
7bc2e5a71e | ||
|
|
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 | ||
|
|
69ea9a8f94 | ||
|
|
7eaec80d96 | ||
|
|
3ccd1ebb7b | ||
|
|
db5e4fc36c | ||
|
|
160cfcce4d | ||
|
|
ca8bd448a2 | ||
|
|
fdaf56b91a | ||
|
|
53367579f4 | ||
|
|
9ebe5fa989 | ||
|
|
3f852ccff3 | ||
|
|
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 | ||
|
|
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 | ||
|
|
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 | ||
|
|
153802cd5b | ||
|
|
b0b39e638a | ||
|
|
39e5f4aaf4 | ||
|
|
2860b8d50a | ||
|
|
a9b2e098ac | ||
|
|
394171ebb7 | ||
|
|
0a05f37774 | ||
|
|
9f69763572 | ||
|
|
9565f1d0ba | ||
|
|
172d2a8bd6 | ||
|
|
c70857af6f | ||
|
|
a98b23411c | ||
|
|
a734a7da25 | ||
|
|
6672ec0149 | ||
|
|
6c7eb339f4 | ||
|
|
468ccd748d | ||
|
|
613b9c4405 | ||
|
|
6318b1f84c | ||
|
|
8800808925 | ||
|
|
12670d4d9b | ||
|
|
41c0b7fef9 | ||
|
|
9a4c7da375 | ||
|
|
34b4556e5c | ||
|
|
bb935f4727 | ||
|
|
2719bb8280 | ||
|
|
ebfb0d8fcf | ||
|
|
8a0d74d323 | ||
|
|
84d0010702 | ||
|
|
e610c12b2a | ||
|
|
eeef9dcc1b | ||
|
|
c17db043ba | ||
|
|
89dbd19556 | ||
|
|
e9cf7be488 | ||
|
|
48c05081a4 | ||
|
|
36fcce7f1e | ||
|
|
6c85d0f35a | ||
|
|
331fcf0714 | ||
|
|
0ec9e8932e | ||
|
|
67eb7fdbd4 | ||
|
|
c0b2fb8ed9 | ||
|
|
c8f6dea1e1 | ||
|
|
a0a02f5375 | ||
|
|
a1e7da8d84 | ||
|
|
396a49a16d | ||
|
|
3628fb690a | ||
|
|
58428fbcc2 | ||
|
|
f53b0ec9af | ||
|
|
b6933406ed | ||
|
|
57d0e8eea3 | ||
|
|
7403ef60c1 | ||
|
|
42ee461f56 | ||
|
|
47a4c8829c | ||
|
|
4b6fb9f595 | ||
|
|
77e8952e8a | ||
|
|
2be365c839 | ||
|
|
c0c08b3354 | ||
|
|
b0d8462a60 | ||
|
|
f246c96b28 | ||
|
|
aa7b3d985f | ||
|
|
5ed6e9842c | ||
|
|
eaf8b02aa9 | ||
|
|
7aa0aed7c9 | ||
|
|
17dd738ac1 | ||
|
|
32a5e71015 | ||
|
|
1c641e3aff | ||
|
|
562ba42b1b | ||
|
|
c081234021 | ||
|
|
bba0208361 | ||
|
|
b19efcebb9 | ||
|
|
4d6ce6b917 | ||
|
|
92d8027640 | ||
|
|
9c511a6c64 | ||
|
|
c9935fa45c | ||
|
|
a140deae54 | ||
|
|
9e5cc57441 | ||
|
|
8bab346cbb | ||
|
|
0a52cafefe | ||
|
|
83b3fdff21 | ||
|
|
97dfbe9e35 | ||
|
|
e4e6b5426e | ||
|
|
58815a3910 | ||
|
|
b17f173e03 | ||
|
|
9535fea964 | ||
|
|
4134e43f6e | ||
|
|
274b3bcb7e | ||
|
|
8800397ba4 | ||
|
|
0a90218d2a | ||
|
|
06598e0665 | ||
|
|
03e5d86b73 | ||
|
|
8533240407 | ||
|
|
2753005386 | ||
|
|
9178d061ff | ||
|
|
934c77cee9 | ||
|
|
0d8ad50e2a | ||
|
|
2ec2239124 | ||
|
|
257262e3cc | ||
|
|
57c5f28938 | ||
|
|
e85975308f | ||
|
|
323ccf3b25 | ||
|
|
be892516d5 | ||
|
|
a6e956472f | ||
|
|
392ce35efa | ||
|
|
a11e93ef54 | ||
|
|
a163e611be | ||
|
|
ef35991a8c | ||
|
|
1c2992c887 | ||
|
|
1eaeebf2fe | ||
|
|
ffc027a309 | ||
|
|
6821d84f9b | ||
|
|
dcb25d8a40 | ||
|
|
2f20eeb016 | ||
|
|
98e40e2b49 | ||
|
|
2d10394690 | ||
|
|
222ad3e73e | ||
|
|
ebe76aacf3 | ||
|
|
b144f0c1d7 | ||
|
|
8ec8b2c66d | ||
|
|
5bb5af2b5e | ||
|
|
2229686057 | ||
|
|
1a419f7e28 | ||
|
|
b8973d63c0 | ||
|
|
b1932dc8e4 | ||
|
|
de54bd4817 | ||
|
|
424bc9fa6e | ||
|
|
5d5d0633ab | ||
|
|
9282bdc09d | ||
|
|
2cb34c6535 | ||
|
|
5329f21896 | ||
|
|
6d73f2eb9f | ||
|
|
52a777aae6 | ||
|
|
e44bbae07a | ||
|
|
f8abca6053 | ||
|
|
156e697042 | ||
|
|
39d338e5bf | ||
|
|
8fb25646bd | ||
|
|
535291a91a | ||
|
|
7c2663fa56 | ||
|
|
04cd53397a | ||
|
|
c16588578b | ||
|
|
fc000af68c | ||
|
|
b3414b23ce | ||
|
|
3143c4e51c | ||
|
|
bf7d43768b | ||
|
|
ea5a96f87f | ||
|
|
50f7dec026 | ||
|
|
98de9f1d7f | ||
|
|
4dc3d5dcf7 | ||
|
|
49566756cd | ||
|
|
5de89949b3 | ||
|
|
7b49f66ab7 | ||
|
|
412193f1d7 | ||
|
|
4c52c1b188 | ||
|
|
66152c52ca | ||
|
|
ca7b758dd4 | ||
|
|
8ea2780a44 | ||
|
|
a679c615ed | ||
|
|
7cc7bd4786 | ||
|
|
99761f0a93 | ||
|
|
b87f57cd25 | ||
|
|
14ff9aeae5 | ||
|
|
6ce37db3dc | ||
|
|
90dcbdfd02 | ||
|
|
ba7976c5c6 | ||
|
|
6520d3fd76 | ||
|
|
aafe9724d4 | ||
|
|
72207f9222 | ||
|
|
f1be8c88f2 | ||
|
|
927345b131 | ||
|
|
bb68421474 | ||
|
|
7699e1e79a | ||
|
|
59d08a7414 | ||
|
|
41c2d2a3d7 | ||
|
|
0c0be58e65 | ||
|
|
42afbd3e70 | ||
|
|
da9c0712a8 | ||
|
|
143d0f294a | ||
|
|
e197ab7bc3 | ||
|
|
d3bb075c47 | ||
|
|
afeb797c78 | ||
|
|
9ab14a9fd8 | ||
|
|
55a5546f25 | ||
|
|
938f0a028b | ||
|
|
c2b9a19c12 | ||
|
|
29c32f03ae | ||
|
|
950e03e321 | ||
|
|
a1876f16da | ||
|
|
c4b0dd5aa6 | ||
|
|
fa60258edc | ||
|
|
6846d5c6f0 | ||
|
|
30867960ce | ||
|
|
ee201ae6d8 | ||
|
|
6e5b4ca6e0 | ||
|
|
cbc3c36dc3 | ||
|
|
9675b1cf0b | ||
|
|
31967428ca | ||
|
|
9f60fe49ab | ||
|
|
d51340649b | ||
|
|
6907ec3a2e | ||
|
|
30b1aef8ba | ||
|
|
e1bbd76208 | ||
|
|
d38bf3b450 | ||
|
|
63e1849854 | ||
|
|
0b7fee0cc5 | ||
|
|
402301f201 | ||
|
|
c41141fe10 | ||
|
|
eccf5e15b1 | ||
|
|
6299e87569 | ||
|
|
0611db1bdf | ||
|
|
660004e348 | ||
|
|
ac5ce90eba | ||
|
|
0a41e7a4af | ||
|
|
5170b991b4 | ||
|
|
cd27933f98 | ||
|
|
6985f69caa | ||
|
|
aaf36a29a7 | ||
|
|
790420b320 | ||
|
|
2f011ebb24 | ||
|
|
191adf0a7c | ||
|
|
2ab95d908b | ||
|
|
62f505f982 | ||
|
|
eaafce0517 | ||
|
|
10f529c6b6 | ||
|
|
8424e51592 | ||
|
|
45e391b273 | ||
|
|
78e042cb9a | ||
|
|
0c41fdee6f | ||
|
|
11bbf1b8fa | ||
|
|
f89f686097 | ||
|
|
d93e4c7458 | ||
|
|
6bf4fc6cf8 | ||
|
|
35a8f7dd98 | ||
|
|
d0ec8ba22f | ||
|
|
57d3db5322 | ||
|
|
e43ee7ddba | ||
|
|
7a7bffab24 | ||
|
|
1001e57249 | ||
|
|
ccbeca2cad | ||
|
|
d7aa4afa60 | ||
|
|
e4c2ce6a8c | ||
|
|
de115c5113 | ||
|
|
4e0ab4b393 | ||
|
|
e614fbc5a1 | ||
|
|
7f001ee391 | ||
|
|
9432304be5 | ||
|
|
38c0527d35 | ||
|
|
588bcebc87 | ||
|
|
605ea2aa62 | ||
|
|
896d9ae2c7 | ||
|
|
0beabc6c0c | ||
|
|
834a4c13a7 | ||
|
|
eca12aae82 | ||
|
|
00158c1894 | ||
|
|
77f5e3e835 | ||
|
|
48a5c12ab7 | ||
|
|
603cf2c0ab | ||
|
|
0bc27e83ed | ||
|
|
25c1d03cca | ||
|
|
e2b4151ab4 | ||
|
|
889d307372 | ||
|
|
fd23396b95 | ||
|
|
f1c4910993 | ||
|
|
b817148d1c | ||
|
|
f422b22af1 | ||
|
|
6cd56dfd62 | ||
|
|
fe708e0a0b | ||
|
|
4fc0bbc9d7 | ||
|
|
fd0eb4ca7d |
@@ -2,17 +2,23 @@
|
|||||||
#
|
#
|
||||||
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
|
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
|
||||||
#
|
#
|
||||||
version: 2
|
version: 2.1
|
||||||
|
|
||||||
|
orbs:
|
||||||
|
node: circleci/node@3.0.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/node:16.10.0
|
- image: cimg/node:16.11.0
|
||||||
- image: circleci/mongo:4.4
|
- image: mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/repo
|
working_directory: ~/homebrewery
|
||||||
|
executor: node/default
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout:
|
||||||
|
path: ~/homebrewery
|
||||||
|
|
||||||
# Download and cache dependencies
|
# Download and cache dependencies
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
@@ -21,12 +27,48 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- run: npm install
|
- node/install-npm
|
||||||
|
- node/install-packages:
|
||||||
|
app-dir: ~/homebrewery
|
||||||
|
cache-path: node_modules
|
||||||
|
override-ci-command: npm i
|
||||||
|
|
||||||
- save_cache:
|
- save_cache:
|
||||||
paths:
|
paths:
|
||||||
- node_modules
|
- node_modules
|
||||||
key: v1-dependencies-{{ checksum "package.json" }}
|
key: v1-dependencies-{{ checksum "package.json" }}
|
||||||
|
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- .
|
||||||
|
|
||||||
|
test:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:16.11.0
|
||||||
|
|
||||||
|
working_directory: ~/homebrewery
|
||||||
|
parallelism: 4
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
|
||||||
# run tests!
|
# run tests!
|
||||||
- run: npm run circleci
|
- run:
|
||||||
|
name: Test - Basic
|
||||||
|
command: npm run test:basic
|
||||||
|
- run:
|
||||||
|
name: Test - Mustache Spans
|
||||||
|
command: npm run test:mustache-span
|
||||||
|
- run:
|
||||||
|
name: Test - Routes
|
||||||
|
command: npm run test:route
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
build_and_test:
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
- test:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
57
README.md
57
README.md
@@ -9,37 +9,37 @@ using [Markdown][markdown-url]. It is distributed under the terms of the [MIT Li
|
|||||||
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
|
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
The easiest way to get started using the Homebrewery is to use it
|
The easiest way to get started using The Homebrewery is to use it
|
||||||
[on our website][homebrewery-url]. The code is open source, so feel free to
|
[on our website][homebrewery-url]. The code is open source, so feel free to
|
||||||
clone it, tinker with it. If you want to make changes to the code, you can run
|
clone it and tinker with it. If you want to make changes to the code, you can run
|
||||||
your own local version for testing by following the installation instructions
|
your own local version for testing by following the installation instructions
|
||||||
below.
|
below.
|
||||||
|
|
||||||
[homebrewery-url]: https://homebrewery.naturalcrit.com
|
[homebrewery-url]: https://homebrewery.naturalcrit.com
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
First, install three programs that the Homebrewery requires to run and retrieve
|
First, install three programs that The Homebrewery requires to run and retrieve
|
||||||
updates:
|
updates:
|
||||||
|
|
||||||
1. install [node](https://nodejs.org/en/)
|
1. install [node](https://nodejs.org/en/)
|
||||||
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
||||||
|
|
||||||
For easiest installation, follow these steps:
|
For the easiest installation, follow these steps:
|
||||||
1. In the installer, uncheck the option to run as a service
|
1. In the installer, uncheck the option to run as a service.
|
||||||
1. You can install MongoDB Compass if you want a GUI to view your database documents
|
1. You can install MongoDB Compass if you want a GUI to view your database documents.
|
||||||
1. Go to the C drive and create a folder called "data"
|
1. Go to the C:\ drive and create a folder called "data".
|
||||||
1. Inside the "data" folder, create a new folder called "db"
|
1. Inside the "data" folder, create a new folder called "db".
|
||||||
1. Open a command prompt or other terminal and navigate to your mongodb install folder (c:program files\mongo\server\4.4\bin)
|
1. Open a command prompt or other terminal and navigate to your MongoDB install folder (C:\Program Files\Mongo\Server\4.4\bin).
|
||||||
1. In the command prompt, run "mongod", which will start up your local database server
|
1. In the command prompt, run "mongod", which will start up your local database server.
|
||||||
1. While MongoD is running, open a second command prompt and navigate to the mongodb install folder
|
1. While MongoD is running, open a second command prompt and navigate to the MongoDB install folder.
|
||||||
1. In the second command prompt, run "mongo", which allows you to edit the database
|
1. In the second command prompt, run "mongo", which allows you to edit the database.
|
||||||
1. Type `use homebrewery` to create the homebrewery database. You should see `switched to db homebrewery`
|
1. Type `use homebrewery` to create The Homebrewery database. You should see `switched to db homebrewery`.
|
||||||
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`
|
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`.
|
||||||
1. Search in Windows for "Advanced system settings" and open it
|
1. Search in Windows for "Advanced system settings" and open it.
|
||||||
1. Click "Environment variables", find the "path" variable, and double-click to open it
|
1. Click "Environment variables", find the "path" variable, and double-click to open it.
|
||||||
1. Click "New" and paste in the path to the mongodb "bin" folder
|
1. Click "New" and paste in the path to the MongoDB "bin" folder.
|
||||||
1. Click "OK", "OK", "OK" to close all the windows
|
1. Click "OK" three times to close all the windows.
|
||||||
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt)
|
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt).
|
||||||
|
|
||||||
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
||||||
```
|
```
|
||||||
@@ -54,7 +54,7 @@ the project to run locally.
|
|||||||
You can set this temporarily in your shell of choice:
|
You can set this temporarily in your shell of choice:
|
||||||
* Windows Powershell: `$env:NODE_ENV="local"`
|
* Windows Powershell: `$env:NODE_ENV="local"`
|
||||||
* Windows CMD: `set NODE_ENV=local`
|
* Windows CMD: `set NODE_ENV=local`
|
||||||
* Linux / OSX: `export NODE_ENV=local`
|
* Linux / macOS: `export NODE_ENV=local`
|
||||||
|
|
||||||
Third, you will need to install the Node dependencies, compile the app, and run
|
Third, you will need to install the Node dependencies, compile the app, and run
|
||||||
it using the two commands:
|
it using the two commands:
|
||||||
@@ -63,7 +63,7 @@ it using the two commands:
|
|||||||
1. `npm start`
|
1. `npm start`
|
||||||
|
|
||||||
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
||||||
in your browser and use the Homebrewery offline.
|
in your browser and use The Homebrewery offline.
|
||||||
|
|
||||||
### Running the application via Docker
|
### Running the application via Docker
|
||||||
|
|
||||||
@@ -95,11 +95,11 @@ You can check out the [changelog](./changelog.md).
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the [MIT license](./license). Which means you
|
This project is licensed under the [MIT license](./license), which means you
|
||||||
are free to use The Homebrewery in any way that you want, except for claiming
|
are free to use The Homebrewery in any way that you want, except for claiming
|
||||||
that you made it yourself.
|
that you made it yourself.
|
||||||
|
|
||||||
If you wish to sell or in some way gain profit for what's created on this site,
|
If you wish to sell, or in some way gain profit for, what's created on this site,
|
||||||
it's your responsibility to ensure you have the proper licenses/rights for any
|
it's your responsibility to ensure you have the proper licenses/rights for any
|
||||||
images or resources used.
|
images or resources used.
|
||||||
|
|
||||||
@@ -108,13 +108,12 @@ images or resources used.
|
|||||||
You are welcome to contribute to the development and maintenance of the
|
You are welcome to contribute to the development and maintenance of the
|
||||||
project! There are several ways of doing that:
|
project! There are several ways of doing that:
|
||||||
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some
|
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some
|
||||||
of them are outdated, duplicates or doesn't contain any useful info. In order
|
of them are outdated, duplicates, or don't contain any useful info. To help, you can [mark duplicates][github-mark-duplicate-url], try to
|
||||||
to help you can [mark duplicates][github-mark-duplicate-url], try to
|
reproduce some complex or weird issues, try finding a workaround for a
|
||||||
reproduce some complex or weird issues, try with finding a workaround for a
|
reported bug, or just mention our issue managers team to let them know about
|
||||||
reported bug or just mention issue managers team to let them know about
|
outdated issues via `@naturalcrit/issue-managers`.
|
||||||
outdated issue via `@naturalcrit/issue-managers`.
|
|
||||||
- Our [subreddit][subreddit-url] is constantly growing and there are number of
|
- Our [subreddit][subreddit-url] is constantly growing and there are number of
|
||||||
bug reports: any help with sorting them out is very welcome.
|
bug reports. Any help with sorting them out is very welcome.
|
||||||
- And of course you can contribute by fixing a bug or implementing a new
|
- And of course you can contribute by fixing a bug or implementing a new
|
||||||
feature by yourself, we are waiting for your
|
feature by yourself, we are waiting for your
|
||||||
[pull requests][github-pr-docs-url]!
|
[pull requests][github-pr-docs-url]!
|
||||||
|
|||||||
101
changelog.md
101
changelog.md
@@ -29,21 +29,114 @@ pre {
|
|||||||
.page p + pre {
|
.page p + pre {
|
||||||
margin-top : 0.1cm;
|
margin-top : 0.1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page .openSans {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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).
|
||||||
|
|
||||||
|
### 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
|
||||||
|
{{taskList
|
||||||
|
* [x] Style updates to user page.
|
||||||
|
|
||||||
|
* [x] Added a logout button (finally)! You can find it under {{openSans **USERNAME {{fa,fa-user}} → LOGOUT {{fas,fa-power-off}}**}}
|
||||||
|
|
||||||
|
Fixes issues: [#303](https://github.com/naturalcrit/homebrewery/issues/303)
|
||||||
|
|
||||||
|
* [x] Clarified the default text when submitting an issue via Reddit post.
|
||||||
|
|
||||||
|
* [x] Fixed broken Table of Contents links in PDFs. (Thanks lucastucious!)
|
||||||
|
|
||||||
|
Fixes issues: [#1749](https://github.com/naturalcrit/homebrewery/issues/1749)
|
||||||
|
|
||||||
|
* [x] Fixed window resizing causing the edit page divider to get lost off of the edge of the page.
|
||||||
|
|
||||||
|
Fixes issues: [#2053](https://github.com/naturalcrit/homebrewery/issues/2053)
|
||||||
|
|
||||||
|
* [x] Fixed Class Table decorations overlapping main text.
|
||||||
|
|
||||||
|
Fixes issues: [#1985](https://github.com/naturalcrit/homebrewery/issues/1985)
|
||||||
|
|
||||||
|
* [x] Updated {{openSans **STYLE EDITOR {{fa,fa-pencil-alt}} → REMOVE DROP CAP {{fas,fa-remove-format}}**}} snippet to also remove small-caps first line font.
|
||||||
|
|
||||||
|
* [x] Background work in preparation for brew themes.
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Wednesday 02/02/2022 - v3.0.7
|
||||||
|
{{taskList
|
||||||
|
* [x] Revert active line highlighting.
|
||||||
|
|
||||||
|
Fixes issues: [#1913](https://github.com/naturalcrit/homebrewery/issues/1913)
|
||||||
|
|
||||||
|
* [x] Added install steps for Ubuntu. [HERE](https://github.com/naturalcrit/homebrewery/blob/master/install/README.UBUNTU.md)
|
||||||
|
|
||||||
|
Fixes issues: [#1900](https://github.com/naturalcrit/homebrewery/issues/1900)
|
||||||
|
|
||||||
|
* [x] Added social media links to home page.
|
||||||
|
|
||||||
|
* [x] Increase brews visible on the user page to 1,000.
|
||||||
|
|
||||||
|
Fixes issues: [#1943](https://github.com/naturalcrit/homebrewery/issues/1943)
|
||||||
|
|
||||||
|
* [x] Added a Legacy to V3 migration guide under {{openSans **NEED HELP? {{fa,fa-question-circle}} → MIGRATE {{fas,fa-file-import}}**}}
|
||||||
|
|
||||||
|
* [x] Background refactoring and unit tests.
|
||||||
|
}}
|
||||||
|
|
||||||
### Saturday 18/12/2021 - v3.0.6
|
### Saturday 18/12/2021 - v3.0.6
|
||||||
{{taskList
|
{{taskList
|
||||||
* [x] Fixed text wrapping for long strings in code blocks.
|
* [x] Fixed text wrapping for long strings in code blocks.
|
||||||
|
|
||||||
Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736)
|
Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736)
|
||||||
|
|
||||||
* [x] Code search/replace `CTRL F / CTRL SHIFT F`
|
* [x] Code search/replace PC: `CTRL F / CTRL SHIFT F` / Mac: `CMD F / OPTION CMD F`
|
||||||
|
|
||||||
Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201)
|
Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201)
|
||||||
|
|
||||||
* [x] Auto-closing HTML tags and curly braces `{{ }}`
|
* [x] Auto-closing HTML tags and curly braces `{{ }}`
|
||||||
* [x] Highlight current active line
|
* [x] Highlight current active line
|
||||||
|
|
||||||
@@ -56,7 +149,7 @@ For a full record of development, visit our [Github Page](https://github.com/nat
|
|||||||
* [x] Make columns even in V3 Table of Contents.
|
* [x] Make columns even in V3 Table of Contents.
|
||||||
|
|
||||||
Fixes issues: [#1671](https://github.com/naturalcrit/homebrewery/issues/1671)
|
Fixes issues: [#1671](https://github.com/naturalcrit/homebrewery/issues/1671)
|
||||||
|
|
||||||
* [x] Fix `CTRL P` failing to print from `/new` pages.
|
* [x] Fix `CTRL P` failing to print from `/new` pages.
|
||||||
|
|
||||||
Fixes issues: [#1815](https://github.com/naturalcrit/homebrewery/issues/1815)
|
Fixes issues: [#1815](https://github.com/naturalcrit/homebrewery/issues/1815)
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const request = require('superagent');
|
|||||||
|
|
||||||
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() {
|
||||||
@@ -25,6 +27,23 @@ 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(_.merge({}, this.props.metadata, {
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
@@ -59,7 +78,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.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 = '/';
|
||||||
@@ -162,6 +181,18 @@ 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'>
|
||||||
|
<label>thumbnail</label>
|
||||||
|
<input type='text'
|
||||||
|
value={this.props.metadata.thumbnail}
|
||||||
|
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 className='field tags'>
|
<div className='field tags'>
|
||||||
<label>tags</label>
|
<label>tags</label>
|
||||||
|
|||||||
@@ -24,6 +24,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;
|
||||||
|
|||||||
@@ -97,7 +97,11 @@ module.exports = [
|
|||||||
gen : dedent`/* Removes Drop Caps */
|
gen : dedent`/* Removes Drop Caps */
|
||||||
.page h1+p:first-letter {
|
.page h1+p:first-letter {
|
||||||
all: unset;
|
all: unset;
|
||||||
}\n\n`
|
}\n\n
|
||||||
|
/* Removes Small-Caps in first line */
|
||||||
|
.page h1+p:first-line {
|
||||||
|
all: unset;
|
||||||
|
}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Tweak Drop Cap',
|
name : 'Tweak Drop Cap',
|
||||||
|
|||||||
@@ -32,11 +32,16 @@ const Homebrew = createClass({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
componentWillMount : function() {
|
|
||||||
|
getInitialState : function() {
|
||||||
global.account = this.props.account;
|
global.account = this.props.account;
|
||||||
global.version = this.props.version;
|
global.version = this.props.version;
|
||||||
global.enable_v3 = this.props.enable_v3;
|
global.enable_v3 = this.props.enable_v3;
|
||||||
|
global.config = this.props.config;
|
||||||
|
|
||||||
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function (){
|
render : function (){
|
||||||
return (
|
return (
|
||||||
<Router location={this.props.url}>
|
<Router location={this.props.url}>
|
||||||
@@ -46,7 +51,7 @@ const Homebrew = createClass({
|
|||||||
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||||
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||||
<Route path='/new' exact component={(routeProps)=><NewPage />}/>
|
<Route path='/new' exact component={(routeProps)=><NewPage />}/>
|
||||||
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
|
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} query={queryString.parse(routeProps.location.search)}/>}/>
|
||||||
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
|
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
|
||||||
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
|
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
|
||||||
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -18,13 +19,84 @@ const Account = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleLogout : function(){
|
||||||
|
if(confirm('Are you sure you want to log out?')) {
|
||||||
|
// Reset divider position
|
||||||
|
window.localStorage.removeItem('naturalcrit-pane-split');
|
||||||
|
// Clear login cookie
|
||||||
|
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 = '/';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
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.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
|
return <Nav.dropdown>
|
||||||
{global.account.username}
|
<Nav.item
|
||||||
</Nav.item>;
|
className='account'
|
||||||
|
color='orange'
|
||||||
|
icon='fas fa-user'
|
||||||
|
>
|
||||||
|
{global.account.username}
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
href={`/user/${global.account.username}`}
|
||||||
|
color='yellow'
|
||||||
|
icon='fas fa-beer'
|
||||||
|
>
|
||||||
|
brews
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
className='logout'
|
||||||
|
color='red'
|
||||||
|
icon='fas fa-power-off'
|
||||||
|
onClick={this.handleLogout}
|
||||||
|
>
|
||||||
|
logout
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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>;
|
||||||
|
|||||||
30
client/homebrew/navbar/help.navitem.jsx
Normal file
30
client/homebrew/navbar/help.navitem.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
module.exports = function(props){
|
||||||
|
return <Nav.dropdown>
|
||||||
|
<Nav.item color='grey' icon='fas fa-question-circle'>
|
||||||
|
need help?
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='red' icon='fas fa-fw fa-bug'
|
||||||
|
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent`
|
||||||
|
**Browser(s)** :
|
||||||
|
**Operating System** :
|
||||||
|
**Legacy or v3 Renderer** :
|
||||||
|
**Issue** : `)}`}
|
||||||
|
newTab={true}
|
||||||
|
rel='noopener noreferrer'>
|
||||||
|
report issue
|
||||||
|
</Nav.item>
|
||||||
|
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
||||||
|
href='/migrate'
|
||||||
|
newTab={true}
|
||||||
|
rel='noopener noreferrer'>
|
||||||
|
migrate
|
||||||
|
</Nav.item>
|
||||||
|
</Nav.dropdown>;
|
||||||
|
};
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
|
|
||||||
module.exports = function(props){
|
|
||||||
return <Nav.item
|
|
||||||
newTab={true}
|
|
||||||
color='red'
|
|
||||||
icon='fas fa-bug'
|
|
||||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
|
||||||
report issue
|
|
||||||
</Nav.item>;
|
|
||||||
};
|
|
||||||
@@ -14,12 +14,10 @@ const Navbar = createClass({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
getInitialState : function() {
|
||||||
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
return {
|
||||||
this.setState({
|
ver : global.version
|
||||||
//showNonChromeWarning : !isChrome,
|
};
|
||||||
ver : window.version
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
@import 'naturalcrit/styles/colors.less';
|
||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
@keyframes coloring {
|
@keyframes pinkColoring {
|
||||||
//from {color: white;}
|
//from {color: white;}
|
||||||
//to {color: red;}
|
//to {color: red;}
|
||||||
0% {color: pink;}
|
0% {color: pink;}
|
||||||
@@ -62,19 +63,21 @@
|
|||||||
}
|
}
|
||||||
i{
|
i{
|
||||||
.animate(color);
|
.animate(color);
|
||||||
animation-name: coloring;
|
animation-name: pinkColoring;
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
color: pink;
|
color: pink;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.recent.navItem{
|
.recent.navItem {
|
||||||
position : relative;
|
position : relative;
|
||||||
.dropdown{
|
.dropdown{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 28px;
|
top : 28px;
|
||||||
left : 0px;
|
left : 0px;
|
||||||
z-index : 10000;
|
z-index : 10000;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
overflow : hidden auto;
|
||||||
|
max-height : ~"calc(100vh - 28px)";
|
||||||
h4{
|
h4{
|
||||||
display : block;
|
display : block;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
@@ -88,11 +91,12 @@
|
|||||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
||||||
}
|
}
|
||||||
.item{
|
.item{
|
||||||
|
#backgroundColors;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : relative;
|
position : relative;
|
||||||
display : block;
|
display : block;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
padding : 13px 5px;
|
padding : 8px 5px 13px;
|
||||||
background-color : #333;
|
background-color : #333;
|
||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
@@ -138,4 +142,7 @@
|
|||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.account.navItem{
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ const RecentItems = createClass({
|
|||||||
if(!this.state.showDropdown) return null;
|
if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
const makeItems = (brews)=>{
|
const makeItems = (brews)=>{
|
||||||
return _.map(brews, (brew)=>{
|
return _.map(brews, (brew, i)=>{
|
||||||
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||||
</a>;
|
</a>;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const cx = require('classnames');
|
|||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../googleDrive.png');
|
const googleDriveIcon = require('../../../../googleDrive.png');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
@@ -31,19 +31,11 @@ const BrewItem = createClass({
|
|||||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.props.brew.googleId) {
|
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
||||||
request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`)
|
.send()
|
||||||
.send()
|
.end(function(err, res){
|
||||||
.end(function(err, res){
|
location.reload();
|
||||||
location.reload();
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
request.delete(`/api/${this.props.brew.editId}`)
|
|
||||||
.send()
|
|
||||||
.end(function(err, res){
|
|
||||||
location.reload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderDeleteBrewLink : function(){
|
renderDeleteBrewLink : function(){
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
-webkit-column-break-inside : avoid;
|
-webkit-column-break-inside : avoid;
|
||||||
page-break-inside : avoid;
|
page-break-inside : avoid;
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
|
box-shadow : 0px 4px 5px 0px #333;
|
||||||
|
background-color : #cab2802e;
|
||||||
.text {
|
.text {
|
||||||
min-height : 54px;
|
min-height : 54px;
|
||||||
h4{
|
h4{
|
||||||
177
client/homebrew/pages/basePages/listPage/listPage.jsx
Normal file
177
client/homebrew/pages/basePages/listPage/listPage.jsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
require('./listPage.less');
|
||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||||
|
|
||||||
|
const ListPage = createClass({
|
||||||
|
displayName : 'ListPage',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
brewCollection : [
|
||||||
|
{
|
||||||
|
title : '',
|
||||||
|
class : '',
|
||||||
|
brews : []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
navItems : <></>
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
sortType : 'alpha',
|
||||||
|
sortDir : 'asc',
|
||||||
|
filterString : this.props.query?.filter || '',
|
||||||
|
query : this.props.query
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderBrews : function(brews){
|
||||||
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
|
return _.map(brews, (brew, idx)=>{
|
||||||
|
return <BrewItem brew={brew} key={idx}/>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
sortBrewOrder : function(brew){
|
||||||
|
if(!brew.title){brew.title = 'No Title';}
|
||||||
|
const mapping = {
|
||||||
|
'alpha' : _.deburr(brew.title.toLowerCase()),
|
||||||
|
'created' : moment(brew.createdAt).format(),
|
||||||
|
'updated' : moment(brew.updatedAt).format(),
|
||||||
|
'views' : brew.views,
|
||||||
|
'latest' : moment(brew.lastViewed).format()
|
||||||
|
};
|
||||||
|
return mapping[this.state.sortType];
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSortOptionChange : function(event){
|
||||||
|
this.setState({
|
||||||
|
sortType : event.target.value
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSortDirChange : function(event){
|
||||||
|
this.setState({
|
||||||
|
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSortOption : function(sortTitle, sortValue){
|
||||||
|
return <td>
|
||||||
|
<button
|
||||||
|
value={`${sortValue}`}
|
||||||
|
onClick={this.handleSortOptionChange}
|
||||||
|
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||||
|
>
|
||||||
|
{`${sortTitle}`}
|
||||||
|
</button>
|
||||||
|
</td>;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFilterTextChange : function(e){
|
||||||
|
this.setState({
|
||||||
|
filterString : e.target.value,
|
||||||
|
});
|
||||||
|
this.updateUrl(e.target.value);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateUrl : function(filterTerm){
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const urlParams = new URLSearchParams(url.search);
|
||||||
|
if(urlParams.get('filter') == filterTerm)
|
||||||
|
return;
|
||||||
|
if(!filterTerm)
|
||||||
|
urlParams.delete('filter');
|
||||||
|
else
|
||||||
|
urlParams.set('filter', filterTerm);
|
||||||
|
url.search = urlParams;
|
||||||
|
window.history.replaceState(null, null, url);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderFilterOption : function(){
|
||||||
|
return <td>
|
||||||
|
<label>
|
||||||
|
<i className='fas fa-search'></i>
|
||||||
|
<input
|
||||||
|
type='search'
|
||||||
|
autoFocus={true}
|
||||||
|
placeholder='filter title/description'
|
||||||
|
onChange={this.handleFilterTextChange}
|
||||||
|
value={this.state.filterString}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</td>;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSortOptions : function(){
|
||||||
|
return <div className='sort-container'>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h6>Sort by :</h6>
|
||||||
|
</td>
|
||||||
|
{this.renderSortOption('Title', 'alpha')}
|
||||||
|
{this.renderSortOption('Created Date', 'created')}
|
||||||
|
{this.renderSortOption('Updated Date', 'updated')}
|
||||||
|
{this.renderSortOption('Views', 'views')}
|
||||||
|
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||||
|
<td>
|
||||||
|
<h6>Direction :</h6>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
onClick={this.handleSortDirChange}
|
||||||
|
className='sortDir'
|
||||||
|
>
|
||||||
|
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
{this.renderFilterOption()}
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSortedBrews : function(brews){
|
||||||
|
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||||
|
brews = _.filter(brews, (brew)=>{
|
||||||
|
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
||||||
|
(_.deburr(brew.description).toLowerCase().includes(testString));
|
||||||
|
});
|
||||||
|
|
||||||
|
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderBrewCollection : function(brewCollection){
|
||||||
|
return _.map(brewCollection, (brewGroup, idx)=>{
|
||||||
|
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
||||||
|
<h1>{brewGroup.title || 'No Title'}</h1>
|
||||||
|
{this.renderBrews(this.getSortedBrews(brewGroup.brews))}
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <div className='listPage sitePage'>
|
||||||
|
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
||||||
|
{this.props.navItems}
|
||||||
|
|
||||||
|
<div className='content V3'>
|
||||||
|
<div className='phb'>
|
||||||
|
{this.renderSortOptions()}
|
||||||
|
{this.renderBrewCollection(this.props.brewCollection)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = ListPage;
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
-webkit-column-gap : auto;
|
-webkit-column-gap : auto;
|
||||||
-moz-column-gap : auto;
|
-moz-column-gap : auto;
|
||||||
}
|
}
|
||||||
.userPage{
|
.listPage{
|
||||||
.content{
|
.content{
|
||||||
overflow-y : scroll;
|
overflow-y : scroll;
|
||||||
.phb{
|
.phb{
|
||||||
@@ -10,7 +10,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
@@ -200,73 +200,18 @@ 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;
|
||||||
|
|
||||||
if(this.state.saveGoogle) {
|
const params = `${transfer ? `?transfer${this.state.saveGoogle ? 'To' : 'From'}Google=true` : ''}`;
|
||||||
if(transfer) {
|
const res = await request
|
||||||
const res = await request
|
.put(`/api/update/${brew.editId}${params}`)
|
||||||
.post('/api/newGoogle/')
|
.send(brew)
|
||||||
.send(brew)
|
.catch((err)=>{
|
||||||
.catch((err)=>{
|
console.log('Error Updating Local Brew');
|
||||||
console.log(err.status === 401
|
this.setState({ errors: err });
|
||||||
? 'Not signed in!'
|
});
|
||||||
: 'Error Transferring to Google!');
|
|
||||||
this.setState({ errors: err, saveGoogle: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
if(!res) { return; }
|
this.savedBrew = res.body;
|
||||||
|
if(transfer) {
|
||||||
console.log('Deleting Local Copy');
|
history.replaceState(null, null, `/edit/${this.savedBrew.googleId ?? ''}${this.savedBrew.editId}`);
|
||||||
await request.delete(`/api/${brew.editId}`)
|
|
||||||
.send()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error deleting Local Copy');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
|
|
||||||
} else {
|
|
||||||
const res = await request
|
|
||||||
.put(`/api/updateGoogle/${brew.editId}`)
|
|
||||||
.send(brew)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log(err.status === 401
|
|
||||||
? 'Not signed in!'
|
|
||||||
: 'Error Saving to Google!');
|
|
||||||
this.setState({ errors: err });
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if(transfer) {
|
|
||||||
const res = await request.post('/api')
|
|
||||||
.send(brew)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error creating Local Copy');
|
|
||||||
this.setState({ errors: err });
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
await request.get(`/api/removeGoogle/${brew.googleId}${brew.editId}`)
|
|
||||||
.send()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error Deleting Google Brew');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
|
|
||||||
} else {
|
|
||||||
const res = await request
|
|
||||||
.put(`/api/update/${brew.editId}`)
|
|
||||||
.send(brew)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.log('Error Updating Local Brew');
|
|
||||||
this.setState({ errors: err });
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
@@ -331,26 +276,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}>
|
||||||
@@ -407,7 +352,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)}`;
|
||||||
},
|
},
|
||||||
@@ -434,7 +379,7 @@ const EditPage = createClass({
|
|||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
{this.renderSaveButton()}
|
{this.renderSaveButton()}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<ReportIssue />
|
<HelpNavItem/>
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||||
share
|
share
|
||||||
@@ -442,7 +387,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'>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ const cx = require('classnames');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const ErrorPage = createClass({
|
|||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<PatreonNavItem />
|
<PatreonNavItem />
|
||||||
<IssueNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { Meta } = require('vitreum/headtags');
|
|||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ const HomePage = createClass({
|
|||||||
return <Navbar ver={this.props.ver}>
|
return <Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
<IssueNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
202
client/homebrew/pages/homePage/migrate.md
Normal file
202
client/homebrew/pages/homePage/migrate.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# How to Convert a Legacy Document to v3
|
||||||
|
Here you will find a number of steps to guide you through converting a Legacy document into a Homebrewery v3 document.
|
||||||
|
|
||||||
|
**The first thing you'll want to do is switch the editor's rendering engine from `Legacy` to `v3`.** This will be the renderer we design features for moving forward.
|
||||||
|
|
||||||
|
There are some examples of Legacy code in the code pane if you need more context behind some of the changes.
|
||||||
|
|
||||||
|
**This document will evolve as users like yourself inform us of issues with it, or areas of conversion that it does not cover. _Please_ reach out if you have any suggestions for this document.**
|
||||||
|
|
||||||
|
## Simple Replacements
|
||||||
|
To make your life a little easier with this section, a text editor like [VSCode](https://code.visualstudio.com/) or Notepad will help a lot.
|
||||||
|
|
||||||
|
The following table describes Legacy and other document elements and their Homebrewery counterparts. A simple find/replace should get these in working order.
|
||||||
|
|
||||||
|
| Legacy / Other | Homebrewery |
|
||||||
|
|:----------------|:-----------------------------|
|
||||||
|
| `\pagebreak` | `\page` |
|
||||||
|
| `======` | `\page` |
|
||||||
|
| `\pagebreaknum` | `{{pageNumber,auto}}\n\page` |
|
||||||
|
| `@=====` | `{{pageNumber,auto}}\n\page` |
|
||||||
|
| `\columnbreak` | `\column` |
|
||||||
|
| `.phb` | `.page` |
|
||||||
|
|
||||||
|
## Classed or Styled Divs
|
||||||
|
Anything that relies on the following syntax can be changed to the new Homebrewery v3 curly brace syntax:
|
||||||
|
|
||||||
|
```
|
||||||
|
<div class="classTable wide">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
:
|
||||||
|
The above example is equivalent to the following in v3 syntax.
|
||||||
|
|
||||||
|
```
|
||||||
|
{{classTable,wide
|
||||||
|
...
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
:
|
||||||
|
Some examples of this include class tables (as shown above), descriptive blocks, notes, and spell lists.
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
## Margins and Padding
|
||||||
|
Any manual margins and padding to push text down the page will likely need to be updated. Colons can be used on lines by themselves to push things down the page vertically if you'd rather not set pixel-perfect margins or padding.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
In Legacy, notes are denoted using markdown blockquote syntax. In Homebrewery v3, this is replaced by the curly brace syntax.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
> ##### Catchy Title
|
||||||
|
> Useful Information
|
||||||
|
-->
|
||||||
|
|
||||||
|
{{note
|
||||||
|
##### Title
|
||||||
|
Information
|
||||||
|
}}
|
||||||
|
|
||||||
|
## Split Tables
|
||||||
|
Split tables also use the curly brace syntax, as the new renderer can handle style values separately from class names.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<div style='column-count:2'>
|
||||||
|
|
||||||
|
| d8 | Loot |
|
||||||
|
|:---:|:-----------:|
|
||||||
|
| 1 | 100gp |
|
||||||
|
| 2 | 200gp |
|
||||||
|
| 3 | 300gp |
|
||||||
|
| 4 | 400gp |
|
||||||
|
|
||||||
|
| d8 | Loot |
|
||||||
|
|:---:|:-----------:|
|
||||||
|
| 5 | 500gp |
|
||||||
|
| 6 | 600gp |
|
||||||
|
| 7 | 700gp |
|
||||||
|
| 8 | 1000gp |
|
||||||
|
|
||||||
|
</div>
|
||||||
|
-->
|
||||||
|
|
||||||
|
##### Typical Difficulty Classes
|
||||||
|
{{column-count:2
|
||||||
|
| Task Difficulty | DC |
|
||||||
|
|:----------------|:--:|
|
||||||
|
| Very easy | 5 |
|
||||||
|
| Easy | 10 |
|
||||||
|
| Medium | 15 |
|
||||||
|
|
||||||
|
| Task Difficulty | DC |
|
||||||
|
|:------------------|:--:|
|
||||||
|
| Hard | 20 |
|
||||||
|
| Very hard | 25 |
|
||||||
|
| Nearly impossible | 30 |
|
||||||
|
}}
|
||||||
|
|
||||||
|
## Blockquotes
|
||||||
|
Blockquotes are denoted by the `>` character at the beginning of the line. In Homebrewery's v3 renderer, they hold virtually no meaning and have no CSS styling. You are free to use blockquotes when styling your document or creating themes without needing to worry about your CSS affecting other parts of the document.
|
||||||
|
|
||||||
|
{{pageNumber,auto}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
## Stat Blocks
|
||||||
|
|
||||||
|
There are pretty significant differences between stat blocks on the Legacy renderer and Homebrewery v3. This section contains a list of changes that will need to be made to update the stat block.
|
||||||
|
|
||||||
|
### Initial Changes
|
||||||
|
You will want to **remove all leading** `___` that started the stat block in Legacy, and replace that with `{{monster` before the stat block, and `}}` after it.
|
||||||
|
|
||||||
|
**If you want a frame** around the stat block, you can add `,frame` to the curly brace definition.
|
||||||
|
|
||||||
|
**If the stat block was wide**, make sure to add `,wide` to the curly brace definition.
|
||||||
|
|
||||||
|
### Blockquotes
|
||||||
|
The key difference is the lack of blockquotes. Legacy documents use the `>` symbol at the start of the line for each line in the stat block, and the v3 renderer does not. **You will want to remove all `>` characters at the beginning of all lines, and delete any leading spaces.**
|
||||||
|
|
||||||
|
### Lists
|
||||||
|
The basic characteristics and advanced characteristics sections are not list elements in Homebrewery. You will want to **remove all `-` or `*` characters from the beginning of lines.**
|
||||||
|
|
||||||
|
### Spacing
|
||||||
|
In order to have the correct spacing after removing the list elements, you will want to **add two colons between the name of each basic/advanced characteristic and its value.** _(see example in the code pane)_
|
||||||
|
|
||||||
|
Additionally, in the special traits and actions sections, you will want to add a colon at the beginning of each line that separates a trait/action from another, as seen below. **Any empty lines between special traits and actions should contain only a colon.** _(see example in the code pane)_
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
{{margin-top:102px}}
|
||||||
|
|
||||||
|
<!--
|
||||||
|
### Legacy/Other Document Example:
|
||||||
|
___
|
||||||
|
> ## Centaur
|
||||||
|
> *Large Monstrosity, neutral good*
|
||||||
|
>___
|
||||||
|
> - **Armor Class** 12
|
||||||
|
> - **Hit Points** 45(6d10 + 12)
|
||||||
|
> - **Speed** 50ft.
|
||||||
|
>___
|
||||||
|
>|STR|DEX|CON|INT|WIS|CHA|
|
||||||
|
>|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
|
>|18 (+4)|14 (+2)|14 (+2)|9 (-1)|13 (+1)|11 (+0)|
|
||||||
|
>___
|
||||||
|
> - **Skills** Athletics +6, Perception +3, Survival +3
|
||||||
|
> - **Senses** passive Perception 13
|
||||||
|
> - **Languages** Elvish, Sylvan
|
||||||
|
> - **Challenge** 2 (450 XP)
|
||||||
|
> ___
|
||||||
|
> ***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||||
|
>
|
||||||
|
> ***Second Thing*** More details.
|
||||||
|
>
|
||||||
|
> ### Actions
|
||||||
|
> ***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||||
|
>
|
||||||
|
> ***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||||
|
>
|
||||||
|
> ***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||||
|
>
|
||||||
|
> ***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||||
|
-->
|
||||||
|
|
||||||
|
### Homebrewery v3 Example:
|
||||||
|
|
||||||
|
{{monster
|
||||||
|
## Centaur
|
||||||
|
*Large monstrosity, neutral good*
|
||||||
|
___
|
||||||
|
**Armor Class** :: 12
|
||||||
|
**Hit Points** :: 45(6d10 + 12)
|
||||||
|
**Speed** :: 50ft.
|
||||||
|
___
|
||||||
|
| STR | DEX | CON | INT | WIS | CHA |
|
||||||
|
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||||
|
|18 (+4)|14 (+2)|14 (+2)|9 (-1) |13 (+1)|11 (+0)|
|
||||||
|
___
|
||||||
|
**Skills** :: Athletics +6, Perception +3, Survival +3
|
||||||
|
**Senses** :: passive Perception 13
|
||||||
|
**Languages** :: Elvish, Sylvan
|
||||||
|
**Challenge** :: 2 (450 XP)
|
||||||
|
___
|
||||||
|
***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||||
|
:
|
||||||
|
***Second Thing*** More details.
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||||
|
:
|
||||||
|
***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||||
|
:
|
||||||
|
***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||||
|
:
|
||||||
|
***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{pageNumber,auto}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -59,18 +59,18 @@ If you wish to sell or in some way gain profit for what you make on this site, i
|
|||||||
|
|
||||||
### More Resources
|
### 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>
|
<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 Discord of Many Things 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' />
|
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
|
||||||
|
|
||||||
<div class='pageNumber'>1</div>
|
<div class='pageNumber'>1</div>
|
||||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||||
|
|
||||||
<div style='position: absolute; top: 40px; right: 60px;'>
|
<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' height='36px'/></a><span style='position: absolute; left: -4px; top: 40px'>Discord</span>
|
<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: 10px;'><img src='/assets/github.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 48px'>Github</span>
|
<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: 10px;'><img src='/assets/patreon.png' height='36px'/></a><span style='position: absolute; top: 40px; right: 46px'>Patreon</span>
|
<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: 10px;'><img src='/assets/reddit.png' height='36px'/></a><span style='position: absolute; top: 40px; right: 0px;'>Reddit</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
\page
|
\page
|
||||||
|
|||||||
@@ -43,13 +43,6 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
|
|||||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
{{position:absolute;top:40px;right:-580px
|
|
||||||
<a href='https://discord.gg/by3deKx' target='_blank' title='discord' style='color: black;'><img src='/assets/discord.png' height='36px'/></a><span style='position: absolute; left: -6px; top: 40px'>Discord</span>
|
|
||||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='github' style='color: black; padding-left: 10px;'><img src='/assets/github.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 47px'>Github</span>
|
|
||||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='patreon' style='color: black; padding-left: 10px;'><img src='/assets/patreon.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 93px'>Patreon</span>
|
|
||||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='reddit' style='color: black; padding-left: 10px;'><img src='/assets/reddit.png' height='36px'/></a><span style='position: absolute; top: 40px; left: 147px;'>Reddit</span>
|
|
||||||
}}
|
|
||||||
|
|
||||||
{{pageNumber 1}}
|
{{pageNumber 1}}
|
||||||
{{footnote PART 1 | FANCINESS}}
|
{{footnote PART 1 | FANCINESS}}
|
||||||
|
|
||||||
@@ -89,6 +82,13 @@ If you'd like to credit me in your brew, I'd be flattered! Just reference that y
|
|||||||
<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.
|
||||||
|
|
||||||
|
{{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
|
\page
|
||||||
|
|
||||||
## Markdown+
|
## Markdown+
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
|||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
@@ -157,45 +157,24 @@ const NewPage = createClass({
|
|||||||
const index = brew.text.indexOf('```\n\n');
|
const index = brew.text.indexOf('```\n\n');
|
||||||
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
};
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
if(this.state.saveGoogle) {
|
const res = await request
|
||||||
const res = await request
|
.post(`/api${this.state.saveGoogle ? '?transferToGoogle=true' : ''}`)
|
||||||
.post('/api/newGoogle/')
|
|
||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err.status === 401
|
console.log(err);
|
||||||
? 'Not signed in!'
|
|
||||||
: 'Error Creating New Google Brew!');
|
|
||||||
this.setState({ isSaving: false, errors: err });
|
this.setState({ isSaving: false, errors: err });
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
if(!res) return;
|
||||||
|
|
||||||
brew = res.body;
|
brew = res.body;
|
||||||
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.googleId ?? ''}${brew.editId}`;
|
||||||
} else {
|
|
||||||
request.post('/api')
|
|
||||||
.send(brew)
|
|
||||||
.end((err, res)=>{
|
|
||||||
if(err){
|
|
||||||
this.setState({
|
|
||||||
isSaving : false
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
window.onbeforeunload = function(){};
|
|
||||||
brew = res.body;
|
|
||||||
localStorage.removeItem(BREWKEY);
|
|
||||||
localStorage.removeItem(STYLEKEY);
|
|
||||||
localStorage.removeItem(METAKEY);
|
|
||||||
window.location = `/edit/${brew.editId}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
@@ -208,26 +187,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}>
|
||||||
@@ -291,7 +270,7 @@ const NewPage = createClass({
|
|||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderSaveButton()}
|
{this.renderSaveButton()}
|
||||||
{this.renderLocalPrintButton()}
|
{this.renderLocalPrintButton()}
|
||||||
<IssueNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
require('./userPage.less');
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
const moment = require('moment');
|
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
@@ -12,16 +11,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
|
||||||
|
|
||||||
// const brew = {
|
|
||||||
// title : 'SUPER Long title woah now',
|
|
||||||
// authors : []
|
|
||||||
// };
|
|
||||||
|
|
||||||
//const BREWS = _.times(25, ()=>{ return brew;});
|
|
||||||
|
|
||||||
|
|
||||||
const UserPage = createClass({
|
const UserPage = createClass({
|
||||||
displayName : 'UserPage',
|
displayName : 'UserPage',
|
||||||
@@ -29,163 +19,51 @@ const UserPage = createClass({
|
|||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : [],
|
brews : [],
|
||||||
|
query : ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `'` : `'s`);
|
||||||
sortType : 'alpha',
|
|
||||||
sortDir : 'asc',
|
|
||||||
filterString : ''
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getUsernameWithS : function() {
|
|
||||||
if(this.props.username.endsWith('s'))
|
|
||||||
return `${this.props.username}'`;
|
|
||||||
return `${this.props.username}'s`;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderBrews : function(brews){
|
const brews = _.groupBy(this.props.brews, (brew)=>{
|
||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
|
||||||
|
|
||||||
const sortedBrews = this.sortBrews(brews);
|
|
||||||
|
|
||||||
return _.map(sortedBrews, (brew, idx)=>{
|
|
||||||
return <BrewItem brew={brew} key={idx}/>;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
sortBrewOrder : function(brew){
|
|
||||||
if(!brew.title){brew.title = 'No Title';}
|
|
||||||
const mapping = {
|
|
||||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
|
||||||
'created' : moment(brew.createdAt).format(),
|
|
||||||
'updated' : moment(brew.updatedAt).format(),
|
|
||||||
'views' : brew.views,
|
|
||||||
'latest' : moment(brew.lastViewed).format()
|
|
||||||
};
|
|
||||||
return mapping[this.state.sortType];
|
|
||||||
},
|
|
||||||
|
|
||||||
sortBrews : function(brews){
|
|
||||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSortOptionChange : function(event){
|
|
||||||
this.setState({
|
|
||||||
sortType : event.target.value
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSortDirChange : function(event){
|
|
||||||
this.setState({
|
|
||||||
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderSortOption : function(sortTitle, sortValue){
|
|
||||||
return <td>
|
|
||||||
<button
|
|
||||||
value={`${sortValue}`}
|
|
||||||
onClick={this.handleSortOptionChange}
|
|
||||||
className={`sortOption ${(this.state.sortType == sortValue ? 'active' : '')}`}
|
|
||||||
>
|
|
||||||
{`${sortTitle}`}
|
|
||||||
</button>
|
|
||||||
</td>;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleFilterTextChange : function(e){
|
|
||||||
this.setState({
|
|
||||||
filterString : e.target.value
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderFilterOption : function(){
|
|
||||||
return <td>
|
|
||||||
<label className='filterOption'>
|
|
||||||
<i className='fas fa-search'></i>
|
|
||||||
<input
|
|
||||||
type='search'
|
|
||||||
placeholder='search title/description'
|
|
||||||
onChange={this.handleFilterTextChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderSortOptions : function(){
|
|
||||||
return <div className='sort-container'>
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<h6>Sort by :</h6>
|
|
||||||
</td>
|
|
||||||
{this.renderSortOption('Title', 'alpha')}
|
|
||||||
{this.renderSortOption('Created Date', 'created')}
|
|
||||||
{this.renderSortOption('Updated Date', 'updated')}
|
|
||||||
{this.renderSortOption('Views', 'views')}
|
|
||||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
|
||||||
<td>
|
|
||||||
<h6>Direction :</h6>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button
|
|
||||||
onClick={this.handleSortDirChange}
|
|
||||||
className='sortDir'
|
|
||||||
>
|
|
||||||
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
{this.renderFilterOption()}
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
getSortedBrews : function(){
|
|
||||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
|
||||||
const brewCollection = this.state.filterString ? _.filter(this.props.brews, (brew)=>{
|
|
||||||
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
|
||||||
(_.deburr(brew.description).toLowerCase().includes(testString));
|
|
||||||
}) : this.props.brews;
|
|
||||||
return _.groupBy(brewCollection, (brew)=>{
|
|
||||||
return (brew.published ? 'published' : 'private');
|
return (brew.published ? 'published' : 'private');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const brewCollection = [
|
||||||
|
{
|
||||||
|
title : `${usernameWithS} published brews`,
|
||||||
|
class : 'published',
|
||||||
|
brews : brews.published
|
||||||
|
}
|
||||||
|
];
|
||||||
|
if(this.props.username == global.account?.username){
|
||||||
|
brewCollection.push(
|
||||||
|
{
|
||||||
|
title : `${usernameWithS} unpublished brews`,
|
||||||
|
class : 'unpublished',
|
||||||
|
brews : brews.private
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
brewCollection : brewCollection
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
navItems : function() {
|
||||||
|
return <Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
const brews = this.getSortedBrews();
|
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
|
||||||
|
|
||||||
return <div className='userPage sitePage'>
|
|
||||||
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
|
||||||
<Navbar>
|
|
||||||
<Nav.section>
|
|
||||||
<NewBrew />
|
|
||||||
<ReportIssue />
|
|
||||||
<RecentNavItem />
|
|
||||||
<Account />
|
|
||||||
</Nav.section>
|
|
||||||
</Navbar>
|
|
||||||
|
|
||||||
<div className='content V3'>
|
|
||||||
<div className='phb'>
|
|
||||||
{this.renderSortOptions()}
|
|
||||||
<div className='published'>
|
|
||||||
<h1>{this.getUsernameWithS()} published brews</h1>
|
|
||||||
{this.renderBrews(brews.published)}
|
|
||||||
</div>
|
|
||||||
{this.props.username == global.account?.username &&
|
|
||||||
<div className='unpublished'>
|
|
||||||
<h1>{this.getUsernameWithS()} unpublished brews</h1>
|
|
||||||
{this.renderBrews(brews.private)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
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>
|
||||||
@@ -7,6 +9,13 @@ module.exports = async(name, title = '', props = {})=>{
|
|||||||
<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/homebrew/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"
|
||||||
}
|
}
|
||||||
|
|||||||
6506
package-lock.json
generated
6506
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
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.6",
|
"version": "3.1.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.11.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
@@ -20,6 +20,9 @@
|
|||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
|
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
||||||
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"phb": "node scripts/phb.js",
|
"phb": "node scripts/phb.js",
|
||||||
"prod": "set NODE_ENV=production && npm run build",
|
"prod": "set NODE_ENV=production && npm run build",
|
||||||
"postinstall": "npm run buildall",
|
"postinstall": "npm run buildall",
|
||||||
@@ -31,6 +34,7 @@
|
|||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"jest": {
|
"jest": {
|
||||||
|
"testTimeout": 15000,
|
||||||
"modulePaths": [
|
"modulePaths": [
|
||||||
"mode_modules",
|
"mode_modules",
|
||||||
"shared",
|
"shared",
|
||||||
@@ -47,33 +51,33 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.16.12",
|
"@babel/core": "^7.18.2",
|
||||||
"@babel/plugin-transform-runtime": "^7.16.10",
|
"@babel/plugin-transform-runtime": "^7.18.2",
|
||||||
"@babel/preset-env": "^7.16.11",
|
"@babel/preset-env": "^7.18.2",
|
||||||
"@babel/preset-react": "^7.16.7",
|
"@babel/preset-react": "^7.17.12",
|
||||||
"body-parser": "^1.19.1",
|
"body-parser": "^1.20.0",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"codemirror": "^5.65.1",
|
"codemirror": "^5.65.5",
|
||||||
"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.2",
|
"express": "^4.18.1",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.1",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "10.0.0",
|
"fs-extra": "10.1.0",
|
||||||
"googleapis": "92.0.0",
|
"googleapis": "101.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.0.16",
|
||||||
"marked-extended-tables": "^1.0.3",
|
"marked-extended-tables": "^1.0.3",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.3",
|
||||||
"mongoose": "^6.1.8",
|
"mongoose": "^6.3.6",
|
||||||
"nanoid": "3.2.0",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.11.3",
|
"nconf": "^0.12.0",
|
||||||
"query-string": "7.1.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",
|
||||||
@@ -83,9 +87,9 @@
|
|||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.7.0",
|
"eslint": "^8.17.0",
|
||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.30.0",
|
||||||
"jest": "^27.4.5",
|
"jest": "^28.1.1",
|
||||||
"supertest": "^6.2.2"
|
"supertest": "^6.2.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
const DB = require('./server/db.js');
|
const DB = require('./server/db.js');
|
||||||
const server = require('./server/app.js');
|
const server = require('./server/app.js');
|
||||||
|
const config = require('./server/config.js');
|
||||||
const config = require('nconf')
|
|
||||||
.argv()
|
|
||||||
.env({ lowerCase: true })
|
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
|
||||||
.file('defaults', { file: 'config/default.json' });
|
|
||||||
|
|
||||||
DB.connect(config).then(()=>{
|
DB.connect(config).then(()=>{
|
||||||
// Ensure that we have successfully connected to the database
|
// Ensure that we have successfully connected to the database
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
|
// Set working directory to project root
|
||||||
|
process.chdir(`${__dirname}/..`);
|
||||||
|
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const jwt = require('jwt-simple');
|
const jwt = require('jwt-simple');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const yaml = require('js-yaml');
|
const yaml = require('js-yaml');
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const config = require('./config.js');
|
||||||
|
|
||||||
const homebrewApi = require('./homebrew.api.js');
|
const homebrewApi = require('./homebrew.api.js');
|
||||||
const GoogleActions = require('./googleActions.js');
|
const GoogleActions = require('./googleActions.js');
|
||||||
@@ -21,7 +25,7 @@ const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
|||||||
if(id.length > 12) {
|
if(id.length > 12) {
|
||||||
const googleId = id.slice(0, -12);
|
const googleId = id.slice(0, -12);
|
||||||
id = id.slice(-12);
|
id = id.slice(-12);
|
||||||
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
|
brew = await GoogleActions.getGoogleBrew(googleId, id, accessType);
|
||||||
} else {
|
} else {
|
||||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
||||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
||||||
@@ -62,22 +66,13 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`${__dirname}/../build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
|
|
||||||
process.chdir(__dirname);
|
|
||||||
|
|
||||||
//app.use(express.static(`${__dirname}/build`));
|
//app.use(express.static(`${__dirname}/build`));
|
||||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||||
app.use(require('cookie-parser')());
|
app.use(require('cookie-parser')());
|
||||||
app.use(require('./forcessl.mw.js'));
|
app.use(require('./forcessl.mw.js'));
|
||||||
|
|
||||||
// FIXME: the config should be passed as an argument for the app
|
|
||||||
const config = require('nconf')
|
|
||||||
.argv()
|
|
||||||
.env({ lowerCase: true })
|
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
|
||||||
.file('defaults', { file: 'config/default.json' });
|
|
||||||
|
|
||||||
//Account Middleware
|
//Account Middleware
|
||||||
app.use((req, res, next)=>{
|
app.use((req, res, next)=>{
|
||||||
if(req.cookies && req.cookies.nc_session){
|
if(req.cookies && req.cookies.nc_session){
|
||||||
@@ -99,16 +94,17 @@ 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 welcomeTextV3 = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||||
const changelogText = require('fs').readFileSync('./../changelog.md', 'utf8');
|
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||||
const faqText = require('fs').readFileSync('./../faq.md', 'utf8');
|
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
|
||||||
|
const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
||||||
|
|
||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
//Robots.txt
|
//Robots.txt
|
||||||
app.get('/robots.txt', (req, res)=>{
|
app.get('/robots.txt', (req, res)=>{
|
||||||
return res.sendFile(`${__dirname}/robots.txt`);
|
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||||
});
|
});
|
||||||
|
|
||||||
//Home page
|
//Home page
|
||||||
@@ -131,6 +127,17 @@ app.get('/v3_preview', async (req, res, next)=>{
|
|||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Legacy/Other Document -> v3 Migration Guide
|
||||||
|
app.get('/migrate', async (req, res, next)=>{
|
||||||
|
const brew = {
|
||||||
|
text : migrateText,
|
||||||
|
renderer : 'V3'
|
||||||
|
};
|
||||||
|
splitTextStyleAndMetadata(brew);
|
||||||
|
req.brew = brew;
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
|
||||||
//Changelog page
|
//Changelog page
|
||||||
app.get('/changelog', async (req, res, next)=>{
|
app.get('/changelog', async (req, res, next)=>{
|
||||||
const brew = {
|
const brew = {
|
||||||
@@ -187,19 +194,36 @@ app.get('/download/:id', asyncHandler(async (req, res)=>{
|
|||||||
app.get('/user/:username', async (req, res, next)=>{
|
app.get('/user/:username', async (req, res, next)=>{
|
||||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||||
|
|
||||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
const fields = [
|
||||||
|
'title',
|
||||||
|
'pageCount',
|
||||||
|
'description',
|
||||||
|
'authors',
|
||||||
|
'published',
|
||||||
|
'views',
|
||||||
|
'shareId',
|
||||||
|
'editId',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'lastViewed'
|
||||||
|
];
|
||||||
|
|
||||||
|
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
if(ownAccount && req?.account?.googleId){
|
||||||
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
.catch((err)=>{
|
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||||
console.error(err);
|
.catch((err)=>{
|
||||||
});
|
console.error(err);
|
||||||
|
});
|
||||||
|
|
||||||
if(googleBrews)
|
if(googleBrews) {
|
||||||
|
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)=>{
|
||||||
@@ -249,9 +273,31 @@ app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
|||||||
return next();
|
return 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((req, res)=>{
|
||||||
|
// 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,
|
||||||
@@ -259,7 +305,8 @@ 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)
|
templateFn('homebrew', title, props)
|
||||||
|
|||||||
5
server/config.js
Normal file
5
server/config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = require('nconf')
|
||||||
|
.argv()
|
||||||
|
.env({ lowerCase: true })
|
||||||
|
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||||
|
.file('defaults', { file: 'config/default.json' });
|
||||||
@@ -3,13 +3,22 @@ const _ = require('lodash');
|
|||||||
const { google } = require('googleapis');
|
const { google } = require('googleapis');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const token = require('./token.js');
|
const token = require('./token.js');
|
||||||
const config = require('nconf')
|
const config = require('./config.js');
|
||||||
.argv()
|
|
||||||
.env({ lowerCase: true }) // Load environment variables
|
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
|
||||||
.file('defaults', { file: 'config/default.json' });
|
|
||||||
|
|
||||||
//let oAuth2Client;
|
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||||
|
JSON.parse(config.get('service_account')) :
|
||||||
|
config.get('service_account');
|
||||||
|
let serviceAuth;
|
||||||
|
try {
|
||||||
|
serviceAuth = google.auth.fromJSON(keys);
|
||||||
|
serviceAuth.scopes = [
|
||||||
|
'https://www.googleapis.com/auth/drive'
|
||||||
|
];
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
|
||||||
|
}
|
||||||
|
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
||||||
|
|
||||||
const GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
@@ -47,7 +56,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getGoogleFolder : async (auth)=>{
|
getGoogleFolder : async (auth)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
fileMetadata = {
|
fileMetadata = {
|
||||||
'name' : 'Homebrewery',
|
'name' : 'Homebrewery',
|
||||||
@@ -83,17 +92,8 @@ const GoogleActions = {
|
|||||||
return folderId;
|
return folderId;
|
||||||
},
|
},
|
||||||
|
|
||||||
listGoogleBrews : async (req, res)=>{
|
listGoogleBrews : async (auth)=>{
|
||||||
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
|
||||||
|
|
||||||
//TODO: Change to service account to allow non-owners to view published files.
|
|
||||||
// Requires a driveId parameter in the drive.files.list command
|
|
||||||
// const keys = JSON.parse(config.get('service_account'));
|
|
||||||
// const auth = google.auth.fromJSON(keys);
|
|
||||||
// auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
|
||||||
|
|
||||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
|
||||||
|
|
||||||
const obj = await drive.files.list({
|
const obj = await drive.files.list({
|
||||||
pageSize : 1000,
|
pageSize : 1000,
|
||||||
@@ -101,18 +101,18 @@ const GoogleActions = {
|
|||||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(`Error Listing Google Brews`);
|
console.log(`Error Listing Google Brews`);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw (err);
|
throw (err);
|
||||||
//TODO: Should break out here, but continues on for some reason.
|
//TODO: Should break out here, but continues on for some reason.
|
||||||
});
|
});
|
||||||
|
|
||||||
if(!obj.data.files.length) {
|
if(!obj.data.files.length) {
|
||||||
console.log('No files found.');
|
console.log('No files found.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const brews = obj.data.files.map((file)=>{
|
const brews = obj.data.files.map((file)=>{
|
||||||
return {
|
return {
|
||||||
text : '',
|
text : '',
|
||||||
shareId : file.properties.shareId,
|
shareId : file.properties.shareId,
|
||||||
editId : file.properties.editId,
|
editId : file.properties.editId,
|
||||||
@@ -126,65 +126,49 @@ const GoogleActions = {
|
|||||||
views : parseInt(file.properties.views),
|
views : parseInt(file.properties.views),
|
||||||
tags : '',
|
tags : '',
|
||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
authors : [req.account.username], //TODO: properly save and load authors to google drive
|
systems : [],
|
||||||
systems : []
|
thumbnail : file.properties.thumbnail
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return brews;
|
return brews;
|
||||||
},
|
},
|
||||||
|
|
||||||
existsGoogleBrew : async (auth, id)=>{
|
updateGoogleBrew : async (brew)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
const drive = google.drive({ version: 'v3' });
|
||||||
|
|
||||||
const result = await drive.files.get({ fileId: id })
|
await drive.files.update({
|
||||||
.catch((err)=>{
|
fileId : brew.googleId,
|
||||||
console.log('error checking file exists...');
|
resource : {
|
||||||
console.error(err);
|
name : `${brew.title}.txt`,
|
||||||
return false;
|
description : `${brew.description}`,
|
||||||
});
|
properties : {
|
||||||
|
title : brew.title,
|
||||||
if(result){return true;}
|
published : brew.published,
|
||||||
|
version : brew.version,
|
||||||
return false;
|
renderer : brew.renderer,
|
||||||
},
|
tags : brew.tags,
|
||||||
|
pageCount : brew.pageCount,
|
||||||
updateGoogleBrew : async (auth, brew)=>{
|
systems : brew.systems.join(),
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
thumbnail : brew.thumbnail
|
||||||
|
|
||||||
if(await GoogleActions.existsGoogleBrew(auth, brew.googleId) == true) {
|
|
||||||
await drive.files.update({
|
|
||||||
fileId : brew.googleId,
|
|
||||||
resource : {
|
|
||||||
name : `${brew.title}.txt`,
|
|
||||||
description : `${brew.description}`,
|
|
||||||
properties : {
|
|
||||||
title : brew.title,
|
|
||||||
published : brew.published,
|
|
||||||
version : brew.version,
|
|
||||||
renderer : brew.renderer,
|
|
||||||
tags : brew.tags,
|
|
||||||
pageCount : brew.pageCount,
|
|
||||||
systems : brew.systems.join()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
media : {
|
|
||||||
mimeType : 'text/plain',
|
|
||||||
body : brew.text
|
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
.catch((err)=>{
|
media : {
|
||||||
console.log('Error saving to google');
|
mimeType : 'text/plain',
|
||||||
console.error(err);
|
body : brew.text
|
||||||
throw (err);
|
}
|
||||||
//return res.status(500).send('Error while saving');
|
})
|
||||||
});
|
.catch((err)=>{
|
||||||
}
|
console.log('Error saving to google');
|
||||||
|
console.error(err);
|
||||||
|
throw (err);
|
||||||
|
//return res.status(500).send('Error while saving');
|
||||||
|
});
|
||||||
|
|
||||||
return (brew);
|
return (brew);
|
||||||
},
|
},
|
||||||
|
|
||||||
newGoogleBrew : async (auth, brew)=>{
|
newGoogleBrew : async (auth, brew)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const media = {
|
const media = {
|
||||||
mimeType : 'text/plain',
|
mimeType : 'text/plain',
|
||||||
@@ -198,12 +182,13 @@ const GoogleActions = {
|
|||||||
'description' : `${brew.description}`,
|
'description' : `${brew.description}`,
|
||||||
'parents' : [folderId],
|
'parents' : [folderId],
|
||||||
'properties' : { //AppProperties is not accessible
|
'properties' : { //AppProperties is not accessible
|
||||||
'shareId' : nanoid(12),
|
'shareId' : brew.shareId || nanoid(12),
|
||||||
'editId' : nanoid(12),
|
'editId' : brew.editId || nanoid(12),
|
||||||
'title' : brew.title,
|
'title' : brew.title,
|
||||||
'views' : '0',
|
'views' : '0',
|
||||||
'pageCount' : brew.pageCount,
|
'pageCount' : brew.pageCount,
|
||||||
'renderer' : brew.renderer || 'legacy'
|
'renderer' : brew.renderer || 'legacy',
|
||||||
|
'thumbnail' : brew.thumbnail || ''
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -252,9 +237,8 @@ const GoogleActions = {
|
|||||||
return newHomebrew;
|
return newHomebrew;
|
||||||
},
|
},
|
||||||
|
|
||||||
readFileMetadata : async (auth, id, accessId, accessType)=>{
|
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||||
|
const drive = google.drive({ version: 'v3' });
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -273,16 +257,7 @@ const GoogleActions = {
|
|||||||
throw ('Share ID does not match');
|
throw ('Share ID does not match');
|
||||||
}
|
}
|
||||||
|
|
||||||
//Access file using service account. Using API key only causes "automated query" lockouts after a while.
|
const serviceDrive = google.drive({ version: 'v3' });
|
||||||
|
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
|
||||||
JSON.parse(config.get('service_account')) :
|
|
||||||
config.get('service_account');
|
|
||||||
|
|
||||||
const serviceAuth = google.auth.fromJSON(keys);
|
|
||||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
|
||||||
|
|
||||||
const serviceDrive = google.drive({ version: 'v3', auth: serviceAuth });
|
|
||||||
|
|
||||||
const file = await serviceDrive.files.get({
|
const file = await serviceDrive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -314,6 +289,7 @@ 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,
|
gDrive : true,
|
||||||
googleId : id
|
googleId : id
|
||||||
@@ -323,10 +299,8 @@ const GoogleActions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteGoogleBrew : async (req, res, id)=>{
|
deleteGoogleBrew : async (auth, id)=>{
|
||||||
|
const drive = google.drive({ version: 'v3', auth });
|
||||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
|
||||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
|
||||||
|
|
||||||
const googleId = id.slice(0, -12);
|
const googleId = id.slice(0, -12);
|
||||||
const accessId = id.slice(-12);
|
const accessId = id.slice(-12);
|
||||||
@@ -338,7 +312,6 @@ const GoogleActions = {
|
|||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error loading from Google');
|
console.log('Error loading from Google');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(obj && obj.data.properties.editId != accessId) {
|
if(obj && obj.data.properties.editId != accessId) {
|
||||||
@@ -353,21 +326,10 @@ const GoogleActions = {
|
|||||||
console.log('Can\'t delete Google file');
|
console.log('Can\'t delete Google file');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
increaseView : async (id, accessId, accessType, brew)=>{
|
increaseView : async (id, accessId, accessType, brew)=>{
|
||||||
//service account because this is modifying another user's file properties
|
const drive = google.drive({ version: 'v3' });
|
||||||
//so we need extended scope
|
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
|
||||||
JSON.parse(config.get('service_account')) :
|
|
||||||
config.get('service_account');
|
|
||||||
|
|
||||||
const auth = google.auth.fromJSON(keys);
|
|
||||||
auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
|
||||||
|
|
||||||
const drive = google.drive({ version: 'v3', auth: auth });
|
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
fileId : brew.googleId,
|
fileId : brew.googleId,
|
||||||
@@ -384,8 +346,6 @@ const GoogleActions = {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
//return res.status(500).send('Error while saving');
|
//return res.status(500).send('Error while saving');
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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 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) {
|
||||||
@@ -41,154 +42,195 @@ const excludePropsFromUpdate = (brew)=>{
|
|||||||
const propsToExclude = ['views', 'lastViewed'];
|
const propsToExclude = ['views', 'lastViewed'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
delete brew[prop];
|
delete brew[prop];
|
||||||
};
|
}
|
||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
const newBrew = (req, res)=>{
|
const beforeNewSave = (account, brew)=>{
|
||||||
const brew = req.body;
|
|
||||||
|
|
||||||
if(!brew.title) {
|
if(!brew.title) {
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
brew.title = getGoodBrewTitle(brew.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
brew.authors = (req.account) ? [req.account.username] : [];
|
brew.authors = (account) ? [account.username] : [];
|
||||||
brew.text = mergeBrewText(brew);
|
brew.text = mergeBrewText(brew);
|
||||||
|
};
|
||||||
|
|
||||||
delete brew.editId;
|
const newLocalBrew = async (brew)=>{
|
||||||
delete brew.shareId;
|
|
||||||
delete brew.googleId;
|
|
||||||
|
|
||||||
const newHomebrew = new HomebrewModel(brew);
|
const newHomebrew = new HomebrewModel(brew);
|
||||||
// Compress brew text to binary before saving
|
// Compress brew text to binary before saving
|
||||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
newHomebrew.text = undefined;
|
newHomebrew.text = undefined;
|
||||||
|
|
||||||
newHomebrew.save((err, obj)=>{
|
let saved = await newHomebrew.save()
|
||||||
if(err) {
|
|
||||||
console.error(err, err.toString(), err.stack);
|
|
||||||
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
obj = obj.toObject();
|
|
||||||
obj.gDrive = false;
|
|
||||||
return res.status(200).send(obj);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBrew = (req, res)=>{
|
|
||||||
HomebrewModel.get({ editId: req.params.id })
|
|
||||||
.then((brew)=>{
|
|
||||||
const updateBrew = excludePropsFromUpdate(req.body);
|
|
||||||
brew = _.merge(brew, updateBrew);
|
|
||||||
brew.text = mergeBrewText(brew);
|
|
||||||
|
|
||||||
// Compress brew text to binary before saving
|
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
|
||||||
brew.text = undefined;
|
|
||||||
brew.updatedAt = new Date();
|
|
||||||
|
|
||||||
if(req.account) {
|
|
||||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.markModified('authors');
|
|
||||||
brew.markModified('systems');
|
|
||||||
|
|
||||||
brew.save((err, obj)=>{
|
|
||||||
if(err) throw err;
|
|
||||||
return res.status(200).send(obj);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err, err.toString(), err.stack);
|
||||||
return res.status(500).send('Error while saving');
|
throw `Error while creating new brew, ${err.toString()}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
saved = saved.toObject();
|
||||||
|
saved.gDrive = false;
|
||||||
|
return saved;
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteBrew = (req, res)=>{
|
const newGoogleBrew = async (account, brew, res)=>{
|
||||||
HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
if(!objs.length || err) {
|
|
||||||
return res.status(404).send('Can not find homebrew with that id');
|
|
||||||
}
|
|
||||||
|
|
||||||
const brew = objs[0];
|
return await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||||
|
|
||||||
if(req.account) {
|
|
||||||
// Remove current user as author
|
|
||||||
brew.authors = _.pull(brew.authors, req.account.username);
|
|
||||||
brew.markModified('authors');
|
|
||||||
}
|
|
||||||
|
|
||||||
if(brew.authors.length === 0) {
|
|
||||||
// Delete brew if there are no authors left
|
|
||||||
brew.remove((err)=>{
|
|
||||||
if(err) return res.status(500).send('Error while removing');
|
|
||||||
return res.status(200).send();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Otherwise, save the brew with updated author list
|
|
||||||
brew.save((err, savedBrew)=>{
|
|
||||||
if(err) throw err;
|
|
||||||
return res.status(200).send(savedBrew);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const newGoogleBrew = async (req, res, next)=>{
|
const newBrew = async (req, res)=>{
|
||||||
let oAuth2Client;
|
|
||||||
|
|
||||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
|
||||||
|
|
||||||
const brew = req.body;
|
const brew = req.body;
|
||||||
|
const { transferToGoogle } = req.query;
|
||||||
if(!brew.title) {
|
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.authors = (req.account) ? [req.account.username] : [];
|
|
||||||
brew.text = mergeBrewText(brew);
|
|
||||||
|
|
||||||
delete brew.editId;
|
delete brew.editId;
|
||||||
delete brew.shareId;
|
delete brew.shareId;
|
||||||
delete brew.googleId;
|
delete brew.googleId;
|
||||||
|
|
||||||
req.body = brew;
|
beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
try {
|
let saved;
|
||||||
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
if(transferToGoogle) {
|
||||||
return res.status(200).send(newBrew);
|
saved = await newGoogleBrew(req.account, brew, res)
|
||||||
} catch (err) {
|
.catch((err)=>{
|
||||||
return res.status(err.response.status).send(err);
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
saved = await newLocalBrew(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(!saved) return;
|
||||||
|
return res.status(200).send(saved);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBrew = async (req, res)=>{
|
||||||
|
let brew = excludePropsFromUpdate(req.body);
|
||||||
|
const { transferToGoogle, transferFromGoogle } = req.query;
|
||||||
|
|
||||||
|
let saved;
|
||||||
|
if(brew.googleId && transferFromGoogle) {
|
||||||
|
beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
|
saved = await newLocalBrew(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
if(!saved) return;
|
||||||
|
|
||||||
|
await deleteGoogleBrew(req.account, `${brew.googleId}${brew.editId}`, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
} else if(!brew.googleId && transferToGoogle) {
|
||||||
|
saved = await newGoogleBrew(req.account, brew, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
if(!saved) return;
|
||||||
|
|
||||||
|
await deleteLocalBrew(req.account, brew.editId)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status).send(err.message);
|
||||||
|
});
|
||||||
|
} else if(brew.googleId) {
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
|
saved = await GoogleActions.updateGoogleBrew(brew)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.response?.status || 500).send(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const dbBrew = await HomebrewModel.get({ editId: req.params.id })
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).send('Error while saving');
|
||||||
|
});
|
||||||
|
|
||||||
|
brew = _.merge(dbBrew, brew);
|
||||||
|
brew.text = mergeBrewText(brew);
|
||||||
|
|
||||||
|
// Compress brew text to binary before saving
|
||||||
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
|
brew.text = undefined;
|
||||||
|
brew.updatedAt = new Date();
|
||||||
|
|
||||||
|
if(req.account) {
|
||||||
|
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||||
|
}
|
||||||
|
|
||||||
|
brew.markModified('authors');
|
||||||
|
brew.markModified('systems');
|
||||||
|
|
||||||
|
saved = await brew.save();
|
||||||
|
}
|
||||||
|
if(!saved) return;
|
||||||
|
|
||||||
|
if(!res.headersSent) return res.status(200).send(saved);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteBrew = async (req, res)=>{
|
||||||
|
if(req.params.id.length > 12) {
|
||||||
|
const deleted = await deleteGoogleBrew(req.account, req.params.id, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
if(deleted) return res.status(200).send();
|
||||||
|
} else {
|
||||||
|
const deleted = await deleteLocalBrew(req.account, req.params.id)
|
||||||
|
.catch((err)=>{
|
||||||
|
res.status(err.status).send(err.message);
|
||||||
|
});
|
||||||
|
if(deleted) return res.status(200).send(deleted);
|
||||||
|
return res.status(200).send();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateGoogleBrew = async (req, res, next)=>{
|
const deleteLocalBrew = async (account, id)=>{
|
||||||
let oAuth2Client;
|
const brew = await HomebrewModel.findOne({ editId: id });
|
||||||
|
if(!brew) {
|
||||||
|
throw { status: 404, message: 'Can not find homebrew with that id' };
|
||||||
|
}
|
||||||
|
|
||||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
if(account) {
|
||||||
|
// Remove current user as author
|
||||||
|
brew.authors = _.pull(brew.authors, account.username);
|
||||||
|
brew.markModified('authors');
|
||||||
|
}
|
||||||
|
|
||||||
const brew = excludePropsFromUpdate(req.body);
|
if(brew.authors.length === 0) {
|
||||||
brew.text = mergeBrewText(brew);
|
// Delete brew if there are no authors left
|
||||||
|
await brew.remove()
|
||||||
try {
|
.catch((err)=>{
|
||||||
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
|
console.error(err);
|
||||||
return res.status(200).send(updatedBrew);
|
throw { status: 500, message: 'Error while removing' };
|
||||||
} catch (err) {
|
});
|
||||||
return res.status(err.response?.status || 500).send(err);
|
} else {
|
||||||
|
// Otherwise, save the brew with updated author list
|
||||||
|
return await brew.save()
|
||||||
|
.catch((err)=>{
|
||||||
|
throw { status: 500, message: err };
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', newBrew);
|
const deleteGoogleBrew = async (account, id, res)=>{
|
||||||
router.post('/api/newGoogle/', newGoogleBrew);
|
const auth = await GoogleActions.authCheck(account, res);
|
||||||
router.put('/api/:id', updateBrew);
|
await GoogleActions.deleteGoogleBrew(auth, id);
|
||||||
router.put('/api/update/:id', updateBrew);
|
return true;
|
||||||
router.put('/api/updateGoogle/:id', updateGoogleBrew);
|
};
|
||||||
router.delete('/api/:id', deleteBrew);
|
|
||||||
router.get('/api/remove/:id', deleteBrew);
|
router.post('/api', asyncHandler(newBrew));
|
||||||
router.get('/api/removeGoogle/:id', (req, res)=>{GoogleActions.deleteGoogleBrew(req, res, req.params.id);});
|
router.put('/api/:id', asyncHandler(updateBrew));
|
||||||
|
router.put('/api/update/:id', asyncHandler(updateBrew));
|
||||||
|
router.delete('/api/:id', asyncHandler(deleteBrew));
|
||||||
|
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
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 +37,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 +52,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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
const jwt = require('jwt-simple');
|
const jwt = require('jwt-simple');
|
||||||
|
|
||||||
// Load configuration values
|
// Load configuration values
|
||||||
const config = require('nconf')
|
const config = require('./config.js');
|
||||||
.argv()
|
|
||||||
.env({ lowerCase: true }) // Load environment variables
|
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
|
||||||
.file('defaults', { file: 'config/default.json' });
|
|
||||||
|
|
||||||
// Generate an Access Token for the given User ID
|
// Generate an Access Token for the given User ID
|
||||||
const generateAccessToken = (account)=>{
|
const generateAccessToken = (account)=>{
|
||||||
|
|||||||
@@ -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;
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@import '../styles/colors';
|
||||||
@keyframes glideDropDown {
|
@keyframes glideDropDown {
|
||||||
0% {transform : translate(0px, -100%);
|
0% {transform : translate(0px, -100%);
|
||||||
opacity : 0;
|
opacity : 0;
|
||||||
@@ -49,6 +50,7 @@ nav{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navItem{
|
.navItem{
|
||||||
|
#backgroundColors;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
padding : 8px 12px;
|
padding : 8px 12px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
@@ -58,30 +60,12 @@ nav{
|
|||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
|
line-height : 13px;
|
||||||
i{
|
i{
|
||||||
margin-left : 5px;
|
margin-left : 5px;
|
||||||
font-size : 13px;
|
font-size : 13px;
|
||||||
|
float : right;
|
||||||
}
|
}
|
||||||
&.tealLight:hover{ background-color : @tealLight };
|
|
||||||
&.teal:hover{ background-color : @teal };
|
|
||||||
&.greenLight:hover{ background-color : @greenLight };
|
|
||||||
&.green:hover{ background-color : @green };
|
|
||||||
&.blueLight:hover{ background-color : @blueLight };
|
|
||||||
&.blue:hover{ background-color : @blue };
|
|
||||||
&.purpleLight:hover{ background-color : @purpleLight };
|
|
||||||
&.purple:hover{ background-color : @purple };
|
|
||||||
&.steelLight:hover{ background-color : @steelLight };
|
|
||||||
&.steel:hover{ background-color : @steel };
|
|
||||||
&.yellowLight:hover{ background-color : @yellowLight };
|
|
||||||
&.yellow:hover{ background-color : @yellow };
|
|
||||||
&.orangeLight:hover{ background-color : @orangeLight };
|
|
||||||
&.orange:hover{ background-color : @orange };
|
|
||||||
&.redLight:hover{ background-color : @redLight };
|
|
||||||
&.red:hover{ background-color : @red };
|
|
||||||
&.silverLight:hover{ background-color : @silverLight };
|
|
||||||
&.silver:hover{ background-color : @silver };
|
|
||||||
&.greyLight:hover{ background-color : @greyLight };
|
|
||||||
&.grey:hover{ background-color : @grey };
|
|
||||||
}
|
}
|
||||||
.navSection:last-child .navItem{
|
.navSection:last-child .navItem{
|
||||||
border-left : 1px solid #666;
|
border-left : 1px solid #666;
|
||||||
|
|||||||
@@ -10,43 +10,77 @@ const SplitPane = createClass({
|
|||||||
return {
|
return {
|
||||||
storageKey : 'naturalcrit-pane-split',
|
storageKey : 'naturalcrit-pane-split',
|
||||||
onDragFinish : function(){} //fires when dragging
|
onDragFinish : function(){} //fires when dragging
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
size : null,
|
currentDividerPos : null,
|
||||||
isDragging : false
|
windowWidth : 0,
|
||||||
|
isDragging : false,
|
||||||
|
moveSource : false,
|
||||||
|
moveBrew : false,
|
||||||
|
showMoveArrows : true
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
const paneSize = window.localStorage.getItem(this.props.storageKey);
|
const dividerPos = window.localStorage.getItem(this.props.storageKey);
|
||||||
if(paneSize){
|
if(dividerPos){
|
||||||
this.setState({
|
this.setState({
|
||||||
size : paneSize
|
currentDividerPos : this.limitPosition(dividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13)),
|
||||||
|
userSetDividerPos : dividerPos,
|
||||||
|
windowWidth : window.innerWidth
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
currentDividerPos : window.innerWidth / 2,
|
||||||
|
userSetDividerPos : window.innerWidth / 2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
window.addEventListener('resize', this.handleWindowResize);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount : function() {
|
||||||
|
window.removeEventListener('resize', this.handleWindowResize);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleWindowResize : function() {
|
||||||
|
// Allow divider to increase in size to last user-set position
|
||||||
|
// Limit current position to between 10% and 90% of visible space
|
||||||
|
const newLoc = this.limitPosition(this.state.userSetDividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
currentDividerPos : newLoc,
|
||||||
|
windowWidth : window.innerWidth
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
limitPosition : function(x, min = 1, max = window.innerWidth - 13) {
|
||||||
|
const result = Math.round(Math.min(max, Math.max(min, x)));
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleUp : function(){
|
handleUp : function(){
|
||||||
if(this.state.isDragging){
|
if(this.state.isDragging){
|
||||||
this.props.onDragFinish(this.state.size);
|
this.props.onDragFinish(this.state.currentDividerPos);
|
||||||
window.localStorage.setItem(this.props.storageKey, this.state.size);
|
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||||
}
|
}
|
||||||
this.setState({ isDragging: false });
|
this.setState({ isDragging: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDown : function(){
|
handleDown : function(){
|
||||||
this.setState({ isDragging: true });
|
this.setState({ isDragging: true });
|
||||||
//this.unFocus()
|
//this.unFocus()
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMove : function(e){
|
handleMove : function(e){
|
||||||
if(!this.state.isDragging) return;
|
if(!this.state.isDragging) return;
|
||||||
|
|
||||||
const minWidth = 1;
|
const newSize = this.limitPosition(e.pageX);
|
||||||
const maxWidth = window.innerWidth - 13;
|
|
||||||
const newSize = Math.min(maxWidth, Math.max(minWidth, e.pageX));
|
|
||||||
this.setState({
|
this.setState({
|
||||||
size : newSize
|
currentDividerPos : newSize,
|
||||||
|
userSetDividerPos : newSize
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
@@ -57,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.size}>{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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,27 @@
|
|||||||
@silverLight : #ECF0F1;
|
@silverLight : #ECF0F1;
|
||||||
@silver : #BDC3C7;
|
@silver : #BDC3C7;
|
||||||
@greyLight : #95A5A6;
|
@greyLight : #95A5A6;
|
||||||
@grey : #7F8C8D;
|
@grey : #7F8C8D;
|
||||||
|
|
||||||
|
#backgroundColors {
|
||||||
|
&.tealLight:hover{ background-color : @tealLight };
|
||||||
|
&.teal:hover{ background-color : @teal };
|
||||||
|
&.greenLight:hover{ background-color : @greenLight };
|
||||||
|
&.green:hover{ background-color : @green };
|
||||||
|
&.blueLight:hover{ background-color : @blueLight };
|
||||||
|
&.blue:hover{ background-color : @blue };
|
||||||
|
&.purpleLight:hover{ background-color : @purpleLight };
|
||||||
|
&.purple:hover{ background-color : @purple };
|
||||||
|
&.steelLight:hover{ background-color : @steelLight };
|
||||||
|
&.steel:hover{ background-color : @steel };
|
||||||
|
&.yellowLight:hover{ background-color : @yellowLight };
|
||||||
|
&.yellow:hover{ background-color : @yellow };
|
||||||
|
&.orangeLight:hover{ background-color : @orangeLight };
|
||||||
|
&.orange:hover{ background-color : @orange };
|
||||||
|
&.redLight:hover{ background-color : @redLight };
|
||||||
|
&.red:hover{ background-color : @red };
|
||||||
|
&.silverLight:hover{ background-color : @silverLight };
|
||||||
|
&.silver:hover{ background-color : @silver };
|
||||||
|
&.greyLight:hover{ background-color : @greyLight };
|
||||||
|
&.grey:hover{ background-color : @grey };
|
||||||
|
}
|
||||||
@@ -21,9 +21,7 @@ describe('Tests for static pages', ()=>{
|
|||||||
return app.get('/faq').expect(200);
|
return app.get('/faq').expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: robots.txt file can't be properly loaded under testing environment,
|
it('robots.txt works', ()=>{
|
||||||
// most likely due to __dirname being different from what is expected
|
|
||||||
it.skip('robots.txt works', ()=>{
|
|
||||||
return app.get('/robots.txt').expect(200);
|
return app.get('/robots.txt').expect(200);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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{
|
||||||
@@ -620,25 +619,28 @@ body {
|
|||||||
border-image-width : 47px;
|
border-image-width : 47px;
|
||||||
}
|
}
|
||||||
&.decoration {
|
&.decoration {
|
||||||
transform-style : preserve-3d;
|
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;
|
||||||
@@ -656,7 +658,7 @@ body {
|
|||||||
margin-bottom : 0.3cm;
|
margin-bottom : 0.3cm;
|
||||||
}
|
}
|
||||||
a{
|
a{
|
||||||
display : table;
|
display : inline;
|
||||||
color : inherit;
|
color : inherit;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
&:hover{
|
&:hover{
|
||||||
|
|||||||
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