mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-27 11:43:09 +00:00
Compare commits
374 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f9e5bcf2 | ||
|
|
0a9a5909d1 | ||
|
|
8f75ea4728 | ||
|
|
91ad46b202 | ||
|
|
752806c0ef | ||
|
|
c39bb67bf3 | ||
|
|
fe6097e0d9 | ||
|
|
6ddf0bb889 | ||
|
|
8dcbad97df | ||
|
|
64b3955bc9 | ||
|
|
0f88a13635 | ||
|
|
e8c7d38608 | ||
|
|
6020657529 | ||
|
|
a8c35f3967 | ||
|
|
62fa8f511a | ||
|
|
18cf202ac0 | ||
|
|
98d9018e13 | ||
|
|
a9e14f6165 | ||
|
|
7005e9f760 | ||
|
|
163bc4e0b3 | ||
|
|
3420886192 | ||
|
|
1704ef6557 | ||
|
|
637b3311d6 | ||
|
|
4b8c34bcbd | ||
|
|
d5a34eedb9 | ||
|
|
c997908a49 | ||
|
|
bd594fa214 | ||
|
|
6bae21a578 | ||
|
|
79db97efdf | ||
|
|
7755affa1e | ||
|
|
ea04069fe5 | ||
|
|
2c3a302c85 | ||
|
|
89d9bfe1f1 | ||
|
|
c9241e2162 | ||
|
|
99f7668901 | ||
|
|
187738ee11 | ||
|
|
68d3724e50 | ||
|
|
8526faa041 | ||
|
|
ac9fbb1f08 | ||
|
|
e82da3a6c1 | ||
|
|
40e36fd875 | ||
|
|
3c86984cf1 | ||
|
|
50d172bbd5 | ||
|
|
da676c6ec1 | ||
|
|
901615d99e | ||
|
|
6ed52f37cc | ||
|
|
15e5a31767 | ||
|
|
8cbab4d4ce | ||
|
|
a9c28e84d0 | ||
|
|
bafabb84b4 | ||
|
|
f5d592a291 | ||
|
|
385bee964d | ||
|
|
18f277182a | ||
|
|
344a9d8334 | ||
|
|
67a76f9d86 | ||
|
|
93d6d1ac6a | ||
|
|
4b3edf053f | ||
|
|
0720ac6a15 | ||
|
|
c0b9cd951e | ||
|
|
a54ebabf53 | ||
|
|
676acb9e5a | ||
|
|
387c468d84 | ||
|
|
64e7fe3422 | ||
|
|
2b1466c51b | ||
|
|
332bcde4b2 | ||
|
|
db783179ce | ||
|
|
c78d687387 | ||
|
|
b717059a39 | ||
|
|
e02bde5eed | ||
|
|
e1765cad41 | ||
|
|
6dab5f836e | ||
|
|
e47f81698c | ||
|
|
2b824839c3 | ||
|
|
26f009a295 | ||
|
|
9b4c997b57 | ||
|
|
39143295b1 | ||
|
|
6eaf2feb40 | ||
|
|
4fe0f1a7af | ||
|
|
7aac377ce8 | ||
|
|
77542c5f06 | ||
|
|
3d365339e4 | ||
|
|
66be400f5f | ||
|
|
9880792c4d | ||
|
|
ca03a473ec | ||
|
|
c364856942 | ||
|
|
4e7a96b67c | ||
|
|
39bfc818a6 | ||
|
|
6d9982f735 | ||
|
|
4f6ba7a388 | ||
|
|
0274fb214c | ||
|
|
15519f142d | ||
|
|
6b258886a4 | ||
|
|
d23a88c997 | ||
|
|
096e17ab5a | ||
|
|
da9e20e96f | ||
|
|
53d5f9f6e0 | ||
|
|
43b4fe75e2 | ||
|
|
925db96b08 | ||
|
|
efdb8e07b0 | ||
|
|
816860dc4f | ||
|
|
314f758d62 | ||
|
|
c799aaa7cb | ||
|
|
2f5bc8db54 | ||
|
|
7c61a27084 | ||
|
|
ad3e83da22 | ||
|
|
8888704b58 | ||
|
|
a3dc5e78fd | ||
|
|
32506860dd | ||
|
|
a89b20584f | ||
|
|
8e42c09721 | ||
|
|
bb5978dfea | ||
|
|
0dc491adfc | ||
|
|
f470a6185a | ||
|
|
e5febc1fef | ||
|
|
354d01e980 | ||
|
|
63e043593a | ||
|
|
770d0c141d | ||
|
|
0a885c8581 | ||
|
|
ec9c704e71 | ||
|
|
02c0176070 | ||
|
|
f847de852b | ||
|
|
86413b5767 | ||
|
|
747c976a14 | ||
|
|
326c28a11d | ||
|
|
263471bcbb | ||
|
|
9478454063 | ||
|
|
a9a9804517 | ||
|
|
0bde44ec2f | ||
|
|
13ad179a1b | ||
|
|
b72acd9e59 | ||
|
|
d0a1ef9571 | ||
|
|
d1f049871f | ||
|
|
070184b309 | ||
|
|
cbb41676e0 | ||
|
|
81130dd514 | ||
|
|
61d3edca17 | ||
|
|
248b56a706 | ||
|
|
90f8d1d6da | ||
|
|
8f08b71475 | ||
|
|
cc6527029c | ||
|
|
8a110567fc | ||
|
|
a451e562fb | ||
|
|
4e2f6b1d26 | ||
|
|
6b8db74a2b | ||
|
|
4c629772cc | ||
|
|
208593d203 | ||
|
|
99019be152 | ||
|
|
fa73e1707d | ||
|
|
d7de2e3d21 | ||
|
|
903ff4fd09 | ||
|
|
feaabacc94 | ||
|
|
a5827f66c9 | ||
|
|
bc0846c190 | ||
|
|
ecdcaadfa9 | ||
|
|
db2478f73d | ||
|
|
5d6a7e692f | ||
|
|
0fbeca1536 | ||
|
|
f5e1869a9b | ||
|
|
10c8414ab0 | ||
|
|
6832e68ada | ||
|
|
24477327aa | ||
|
|
52b11d9b38 | ||
|
|
c7b239f362 | ||
|
|
f1b67ad9d6 | ||
|
|
6279a85d2e | ||
|
|
7a4b5cca04 | ||
|
|
c4eff2478a | ||
|
|
15c918b5d4 | ||
|
|
0a5bfe2939 | ||
|
|
05b9bbdf59 | ||
|
|
b88c89d61b | ||
|
|
31d58f9075 | ||
|
|
743d0fa689 | ||
|
|
3b6d3963f1 | ||
|
|
3a4c2d8f13 | ||
|
|
7d86a40261 | ||
|
|
5527aa7990 | ||
|
|
f48484520a | ||
|
|
5957ddd01c | ||
|
|
994b1584b7 | ||
|
|
9647fbcc74 | ||
|
|
ca6f8d085a | ||
|
|
c2637a7576 | ||
|
|
4fd1cdd7e8 | ||
|
|
ad20ff514a | ||
|
|
5cd50f7138 | ||
|
|
89e6bada56 | ||
|
|
420d703f9d | ||
|
|
090da33f33 | ||
|
|
134e260d7b | ||
|
|
82c8ca55fc | ||
|
|
e92cd3be18 | ||
|
|
93c2d2d860 | ||
|
|
cd79a38755 | ||
|
|
84286b7942 | ||
|
|
a16ca8c897 | ||
|
|
fec1766e26 | ||
|
|
fea07429fe | ||
|
|
fa29f2281d | ||
|
|
002b88ab83 | ||
|
|
54833f9fc6 | ||
|
|
cba9286217 | ||
|
|
9ebbff49fb | ||
|
|
f26e3d6cd1 | ||
|
|
9ee39a83c1 | ||
|
|
c09a4c4374 | ||
|
|
3e44d0ad13 | ||
|
|
b23a09b1b8 | ||
|
|
421ed8f51d | ||
|
|
14a057cf55 | ||
|
|
2e6fcafc68 | ||
|
|
13b43e8902 | ||
|
|
837708fc0c | ||
|
|
2e305d5636 | ||
|
|
f9711de634 | ||
|
|
2c6779bb1c | ||
|
|
ee05392d27 | ||
|
|
1cc84da976 | ||
|
|
5cfdd504c1 | ||
|
|
24debfc75c | ||
|
|
ffe6272299 | ||
|
|
64c3d69641 | ||
|
|
67c19b79e3 | ||
|
|
ea29106101 | ||
|
|
8016f82040 | ||
|
|
7c93e5879c | ||
|
|
dc86f89c4f | ||
|
|
d03be052aa | ||
|
|
fef79f4fc3 | ||
|
|
cc58721ccd | ||
|
|
5f2115da0e | ||
|
|
1dd1e677e4 | ||
|
|
29b89bdc00 | ||
|
|
4493d86fd5 | ||
|
|
6a600df19a | ||
|
|
e8c6e36521 | ||
|
|
3e3610a204 | ||
|
|
3e626d91f0 | ||
|
|
6a0d8d13b0 | ||
|
|
4f762b376f | ||
|
|
bda80c9984 | ||
|
|
7de60e2345 | ||
|
|
77ad2c8958 | ||
|
|
6cca821ba6 | ||
|
|
96da053717 | ||
|
|
3723006f39 | ||
|
|
6cfdc47760 | ||
|
|
00a5600768 | ||
|
|
d8674d09a2 | ||
|
|
a46630f774 | ||
|
|
38fb9d467c | ||
|
|
fee56ca8e0 | ||
|
|
de60419926 | ||
|
|
42a1410cc2 | ||
|
|
ddd4f93f01 | ||
|
|
21de3b31b6 | ||
|
|
aea25119c0 | ||
|
|
aa1100642d | ||
|
|
4121cc0e14 | ||
|
|
3b7b56c789 | ||
|
|
ebc03aee33 | ||
|
|
03c6edf31a | ||
|
|
970d03a5e4 | ||
|
|
ed4c090f21 | ||
|
|
672d582caf | ||
|
|
b6c09683be | ||
|
|
a13fd48cda | ||
|
|
aec1147aa5 | ||
|
|
78da48b08d | ||
|
|
edce191c29 | ||
|
|
8c52a253dc | ||
|
|
589ec0251a | ||
|
|
ffddc275c1 | ||
|
|
4e26bb309c | ||
|
|
7696be5d95 | ||
|
|
ef80c23034 | ||
|
|
1f7be69624 | ||
|
|
6e069dc29d | ||
|
|
e40bbf56c7 | ||
|
|
65beb8d65e | ||
|
|
ebf4f614c4 | ||
|
|
26942e276a | ||
|
|
0bcce67e39 | ||
|
|
3318ba6277 | ||
|
|
c5eb7db432 | ||
|
|
f35345f385 | ||
|
|
a13ac2e0c5 | ||
|
|
27eb95ece8 | ||
|
|
7958bb4cda | ||
|
|
8fc01ebb12 | ||
|
|
d2c10bb9ac | ||
|
|
13eedc9f82 | ||
|
|
6f919bc214 | ||
|
|
496328d511 | ||
|
|
d6af56c51c | ||
|
|
079d59695f | ||
|
|
0867b142da | ||
|
|
ac7b2bce9f | ||
|
|
4664d88aad | ||
|
|
7d6a3da2e2 | ||
|
|
4180a7cfb4 | ||
|
|
a2e11d12c9 | ||
|
|
301c3fbdd2 | ||
|
|
35088188b3 | ||
|
|
95cda71eb5 | ||
|
|
a2e9fc67e4 | ||
|
|
50c541f3fa | ||
|
|
f98a3fe6c5 | ||
|
|
d2c86bf619 | ||
|
|
aeb4f85a48 | ||
|
|
b8f32cc2cb | ||
|
|
d562308b63 | ||
|
|
2d26e7cd07 | ||
|
|
8b011804ca | ||
|
|
93a90dbf13 | ||
|
|
2b6585545c | ||
|
|
047160e4ec | ||
|
|
6a6ee9fc12 | ||
|
|
96577794e9 | ||
|
|
5c3f7b1b82 | ||
|
|
bdf1bd1e8b | ||
|
|
df6d372243 | ||
|
|
b237456420 | ||
|
|
52a79b4f75 | ||
|
|
99382adbf6 | ||
|
|
3f0c950e72 | ||
|
|
2b71ff2dec | ||
|
|
9ccf9d0a83 | ||
|
|
1801691f49 | ||
|
|
2bad0bfcb0 | ||
|
|
ac30b3df9d | ||
|
|
f67ba7ecda | ||
|
|
04eebae3ec | ||
|
|
23a0a89ead | ||
|
|
a7a67621a1 | ||
|
|
bebb06a36d | ||
|
|
9ad915c14a | ||
|
|
9fd5fea50c | ||
|
|
41fa0f2c77 | ||
|
|
1d8781da90 | ||
|
|
93918bc26c | ||
|
|
7ae939623c | ||
|
|
bc0ee8138e | ||
|
|
91b2911bb0 | ||
|
|
9848dc54ba | ||
|
|
9a3bd4db4b | ||
|
|
891bde6990 | ||
|
|
d9228b8c4b | ||
|
|
5a3daf8ffd | ||
|
|
5469ec6683 | ||
|
|
764621f762 | ||
|
|
248687684a | ||
|
|
d7b1f89152 | ||
|
|
9a844dae39 | ||
|
|
5193271796 | ||
|
|
ed5bef27e0 | ||
|
|
be25e90009 | ||
|
|
ab98bf5d6c | ||
|
|
ffb1c77697 | ||
|
|
6bddba6762 | ||
|
|
57b0af54df | ||
|
|
0ac50017c4 | ||
|
|
9135ca1e43 | ||
|
|
45aa8bdfae | ||
|
|
4f154922c0 | ||
|
|
2b1fe5d3fe | ||
|
|
03402e4342 | ||
|
|
1d85eede43 | ||
|
|
61f4456842 | ||
|
|
c925e04f3c | ||
|
|
00412a70e9 | ||
|
|
0923c50218 | ||
|
|
fe536bc9df | ||
|
|
e8937a285c |
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- node/install-npm
|
- run: sudo npm install -g npm@8.10.0
|
||||||
- node/install-packages:
|
- node/install-packages:
|
||||||
app-dir: ~/homebrewery
|
app-dir: ~/homebrewery
|
||||||
cache-path: node_modules
|
cache-path: node_modules
|
||||||
@@ -55,9 +55,15 @@ jobs:
|
|||||||
at: .
|
at: .
|
||||||
|
|
||||||
# run tests!
|
# run tests!
|
||||||
|
- run:
|
||||||
|
name: Test - API Unit Tests
|
||||||
|
command: npm run test:api-unit
|
||||||
- run:
|
- run:
|
||||||
name: Test - Basic
|
name: Test - Basic
|
||||||
command: npm run test:basic
|
command: npm run test:basic
|
||||||
|
- run:
|
||||||
|
name: Test - Coverage
|
||||||
|
command: npm run test:coverage
|
||||||
- run:
|
- run:
|
||||||
name: Test - Mustache Spans
|
name: Test - Mustache Spans
|
||||||
command: npm run test:mustache-span
|
command: npm run test:mustache-span
|
||||||
|
|||||||
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -12,10 +12,6 @@ body:
|
|||||||
description: The best feature requests provide an explanation of the current issue and then an explanation of how it could be improved. Screenshots/images can be pasted right in as well!
|
description: The best feature requests provide an explanation of the current issue and then an explanation of how it could be improved. Screenshots/images can be pasted right in as well!
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
- type: markdown
|
||||||
id: terms
|
|
||||||
attributes:
|
attributes:
|
||||||
label: "Please confirm:"
|
value: "Please be sure to search for any close matches to your request in the GitHub Issues tracker before opening a new request, thanks!"
|
||||||
options:
|
|
||||||
- label: I have searched the Issues tracker for any duplicate requests and found none.
|
|
||||||
required: true
|
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
7
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
@@ -4,14 +4,15 @@ body:
|
|||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: Please include as much information as possible.
|
value: Please include as much information as possible.
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: renderer
|
id: renderer
|
||||||
attributes:
|
attributes:
|
||||||
label: Renderer
|
label: Renderer
|
||||||
description: Which renderer does this issue occur on? If you are unsure, you can check the renderer in the Properties Editor (click the "i" in the Snippet Menu bar above the editor).
|
description: Which renderer does this issue occur on? If you are unsure, you can check the renderer in the Properties Editor (click the "i" in the Snippet Menu bar above the editor).
|
||||||
options:
|
options:
|
||||||
- label: Legacy
|
- v3
|
||||||
- label: v3
|
- Legacy
|
||||||
|
- Both
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
155
changelog.md
155
changelog.md
@@ -1,13 +1,17 @@
|
|||||||
```css
|
```css
|
||||||
h5 {
|
h5 {
|
||||||
font-size: .35cm !important;
|
font-size: .35cm !important;
|
||||||
margin-top: 0.3cm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page ul ul {
|
.page ul ul {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page .taskList {
|
||||||
|
display:block;
|
||||||
|
break-inside:auto;
|
||||||
|
}
|
||||||
|
|
||||||
.taskList li input {
|
.taskList li input {
|
||||||
list-style-type : none;
|
list-style-type : none;
|
||||||
margin-left : -0.52cm;
|
margin-left : -0.52cm;
|
||||||
@@ -36,16 +40,154 @@ pre {
|
|||||||
margin-top : 0.1cm;
|
margin-top : 0.1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page ul + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
.page .openSans {
|
.page .openSans {
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding-bottom: 1.5cm;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
### Friday 19/10/2022 - v3.2.2
|
### Friday 23/01/2023 - v3.6.0
|
||||||
|
{{taskList
|
||||||
|
##### calculuschild
|
||||||
|
|
||||||
|
* [x] Fix Google Drive brews sometimes duplicating
|
||||||
|
|
||||||
|
Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Add unit tests with full coverage for the Homebrewery API
|
||||||
|
|
||||||
|
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
|
||||||
|
|
||||||
|
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Auto-compile Themes CSS on development server
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix cloned brews inheriting the parent view count
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Friday 23/12/2022 - v3.5.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Only brew owners or invited authors can edit a brew
|
||||||
|
|
||||||
|
- Visiting an `/edit` page of a brew that does not list you as an author will result in an error page. Authors can be added to any brew by opening its {{fa,fa-info-circle}} **Properties** menu and typing the author's username (case-sensitive) into the **Invited Authors** bubble.
|
||||||
|
- Warn user if a newer brew version has been saved on another device
|
||||||
|
|
||||||
|
Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### Saturday 10/12/2022 - v3.4.2
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Fix broken tags editor
|
||||||
|
|
||||||
|
* [x] Reduce server load to fix some saving issues
|
||||||
|
|
||||||
|
Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Account page help link for Google Drive errors
|
||||||
|
|
||||||
|
Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 05/12/2022 - v3.4.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix Account page incorrect last login time
|
||||||
|
|
||||||
|
Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Fix crashing on iOS and Safari browsers
|
||||||
|
|
||||||
|
Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Monday 28/11/2022 - v3.4.0
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Fix for Chrome v108 handling of page size
|
||||||
|
|
||||||
|
Fixes issues [#2445](https://github.com/naturalcrit/homebrewery/issues/2445), [#2516](https://github.com/naturalcrit/homebrewery/issues/2516)
|
||||||
|
|
||||||
|
* [x] New account page with some user info, at {{openSans **USERNAME {{fa,fa-user}} → ACCOUNT {{fa,fa-user}}**}}
|
||||||
|
|
||||||
|
Fixes issues [#2049](https://github.com/naturalcrit/homebrewery/issues/2049), [#2043](https://github.com/naturalcrit/homebrewery/issues/2043)
|
||||||
|
|
||||||
|
* [x] Fix "Published/Private Brews" buttons on userpage
|
||||||
|
|
||||||
|
Fixes issues [#2449](https://github.com/naturalcrit/homebrewery/issues/2449)
|
||||||
|
|
||||||
|
##### Gazook
|
||||||
|
|
||||||
|
* [x] Make autosave default on for new users
|
||||||
|
|
||||||
|
* [x] Added link to our FAQ at {{openSans **NEED HELP? {{fa,fa-question-circle}} → FAQ {{fa,fa-question-circle}}**}}
|
||||||
|
|
||||||
|
* [x] Fix curly blocks freezing with long property lists
|
||||||
|
|
||||||
|
Fixes issues [#2393](https://github.com/naturalcrit/homebrewery/issues/2393)
|
||||||
|
|
||||||
|
* [x] Items can now be removed from {{openSans **RECENT BREWS** {{fas,fa-history}} }}
|
||||||
|
|
||||||
|
Fixes issues [#1918](https://github.com/naturalcrit/homebrewery/issues/1918)
|
||||||
|
|
||||||
|
* [x] Curly injector syntax `{blue}` highlighting in editor
|
||||||
|
|
||||||
|
Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Thursday 28/10/2022 - v3.3.1
|
||||||
|
{{taskList
|
||||||
|
|
||||||
|
##### Calculuschild
|
||||||
|
|
||||||
|
* [x] Fixes to several broken CSS styles from v3.3.0
|
||||||
|
|
||||||
|
Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
||||||
|
|
||||||
|
##### Jeddai
|
||||||
|
|
||||||
|
* [x] Reduce size of thumbnails on social media links
|
||||||
|
|
||||||
|
}}
|
||||||
|
|
||||||
|
### Friday 19/10/2022 - v3.3.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### Calculuschild
|
##### Calculuschild
|
||||||
@@ -75,9 +217,6 @@ Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
|
|||||||
|
|
||||||
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
##### Gazook:
|
##### Gazook:
|
||||||
|
|
||||||
* [x] Several updates to bug reporting and error popups
|
* [x] Several updates to bug reporting and error popups
|
||||||
@@ -127,6 +266,10 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
### Wednesday 31/08/2022 - v3.2.1
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -153,8 +296,6 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Saturday 27/08/2022 - v3.2.0
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const Editor = createClass({
|
|||||||
onTextChange : ()=>{},
|
onTextChange : ()=>{},
|
||||||
onStyleChange : ()=>{},
|
onStyleChange : ()=>{},
|
||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
|
reportError : ()=>{},
|
||||||
|
|
||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
@@ -137,9 +138,17 @@ const Editor = createClass({
|
|||||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highlight injectors {style}
|
||||||
|
if(line.includes('{') && line.includes('}')){
|
||||||
|
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(line)) != null) {
|
||||||
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||||
|
}
|
||||||
|
}
|
||||||
// Highlight inline spans {{content}}
|
// Highlight inline spans {{content}}
|
||||||
if(line.includes('{{') && line.includes('}}')){
|
if(line.includes('{{') && line.includes('}}')){
|
||||||
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
||||||
let match;
|
let match;
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
@@ -158,7 +167,7 @@ const Editor = createClass({
|
|||||||
// Highlight block divs {{\n Content \n}}
|
// Highlight block divs {{\n Content \n}}
|
||||||
let endCh = line.length+1;
|
let endCh = line.length+1;
|
||||||
|
|
||||||
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
|
const match = line.match(/^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/);
|
||||||
if(match)
|
if(match)
|
||||||
endCh = match.index+match[0].length;
|
endCh = match.index+match[0].length;
|
||||||
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
|
||||||
@@ -283,7 +292,8 @@ const Editor = createClass({
|
|||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
onChange={this.props.onMetaChange} />
|
onChange={this.props.onMetaChange}
|
||||||
|
reportError={this.props.reportError}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
//font-style: italic;
|
//font-style: italic;
|
||||||
}
|
}
|
||||||
|
.injection{
|
||||||
|
color : green;
|
||||||
|
font-weight : bold;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.brewJump{
|
.brewJump{
|
||||||
|
|||||||
@@ -4,16 +4,23 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
|
const validations = require('./validations.js');
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
const homebreweryThumbnail = require('../../thumbnail.png');
|
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||||
|
|
||||||
|
const callIfExists = (val, fn, ...args)=>{
|
||||||
|
if(val[fn]) {
|
||||||
|
val[fn](...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MetadataEditor = createClass({
|
const MetadataEditor = createClass({
|
||||||
displayName : 'MetadataEditor',
|
displayName : 'MetadataEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -22,6 +29,7 @@ const MetadataEditor = createClass({
|
|||||||
editId : null,
|
editId : null,
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
|
thumbnail : '',
|
||||||
tags : [],
|
tags : [],
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
@@ -29,7 +37,8 @@ const MetadataEditor = createClass({
|
|||||||
renderer : 'legacy',
|
renderer : 'legacy',
|
||||||
theme : '5ePHB'
|
theme : '5ePHB'
|
||||||
},
|
},
|
||||||
onChange : ()=>{}
|
onChange : ()=>{},
|
||||||
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -51,11 +60,27 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleFieldChange : function(name, e){
|
handleFieldChange : function(name, e){
|
||||||
|
// load validation rules, and check input value against them
|
||||||
|
const inputRules = validations[name] ?? [];
|
||||||
|
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||||
|
|
||||||
|
// if no validation rules, save to props
|
||||||
|
if(validationErr.length === 0){
|
||||||
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.metadata,
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// if validation issues, display built-in browser error popup with each error.
|
||||||
|
const errMessage = validationErr.map((err)=>{
|
||||||
|
return `- ${err}`;
|
||||||
|
}).join('\n');
|
||||||
|
callIfExists(e.target, 'setCustomValidity', errMessage);
|
||||||
|
callIfExists(e.target, 'reportValidity');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSystem : function(system, e){
|
handleSystem : function(system, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.systems.push(system);
|
this.props.metadata.systems.push(system);
|
||||||
@@ -64,6 +89,7 @@ const MetadataEditor = createClass({
|
|||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleRenderer : function(renderer, e){
|
handleRenderer : function(renderer, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
this.props.metadata.renderer = renderer;
|
this.props.metadata.renderer = renderer;
|
||||||
@@ -96,8 +122,12 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end((err, res)=>{
|
||||||
|
if(err) {
|
||||||
|
this.props.reportError(err);
|
||||||
|
} else {
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -225,24 +255,26 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='metadataEditor'>
|
return <div className='metadataEditor'>
|
||||||
|
<h1 className='sectionHead'>Brew</h1>
|
||||||
|
|
||||||
<div className='field title'>
|
<div className='field title'>
|
||||||
<label>title</label>
|
<label>title</label>
|
||||||
<input type='text' className='value'
|
<input type='text' className='value'
|
||||||
value={this.props.metadata.title}
|
defaultValue={this.props.metadata.title}
|
||||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||||
</div>
|
</div>
|
||||||
<div className='field-group'>
|
<div className='field-group'>
|
||||||
<div className='field-column'>
|
<div className='field-column'>
|
||||||
<div className='field description'>
|
<div className='field description'>
|
||||||
<label>description</label>
|
<label>description</label>
|
||||||
<textarea value={this.props.metadata.description} className='value'
|
<textarea defaultValue={this.props.metadata.description} className='value'
|
||||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||||
</div>
|
</div>
|
||||||
<div className='field thumbnail'>
|
<div className='field thumbnail'>
|
||||||
<label>thumbnail</label>
|
<label>thumbnail</label>
|
||||||
<input type='text'
|
<input type='text'
|
||||||
value={this.props.metadata.thumbnail}
|
defaultValue={this.props.metadata.thumbnail}
|
||||||
placeholder='my.thumbnail.url'
|
placeholder='https://my.thumbnail.url'
|
||||||
className='value'
|
className='value'
|
||||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||||
@@ -258,8 +290,6 @@ const MetadataEditor = createClass({
|
|||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -271,6 +301,23 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
{this.renderRenderOptions()}
|
{this.renderRenderOptions()}
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Authors</h1>
|
||||||
|
|
||||||
|
{this.renderAuthors()}
|
||||||
|
|
||||||
|
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
|
||||||
|
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||||
|
placeholder='invite author' unique={true}
|
||||||
|
values={this.props.metadata.invitedAuthors}
|
||||||
|
notes={['Invited authors are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||||
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Privacy</h1>
|
||||||
|
|
||||||
<div className='field publish'>
|
<div className='field publish'>
|
||||||
<label>publish</label>
|
<label>publish</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
|
|||||||
@@ -10,6 +10,15 @@
|
|||||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
|
|
||||||
|
.sectionHead {
|
||||||
|
font-weight: 1000;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -30,6 +39,7 @@
|
|||||||
}
|
}
|
||||||
.field{
|
.field{
|
||||||
display : flex;
|
display : flex;
|
||||||
|
flex-wrap : wrap;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
&>label{
|
&>label{
|
||||||
@@ -42,6 +52,12 @@
|
|||||||
&>.value{
|
&>.value{
|
||||||
flex : 1 1 auto;
|
flex : 1 1 auto;
|
||||||
width : 50px;
|
width : 50px;
|
||||||
|
&:invalid {
|
||||||
|
background : #ffb9b9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input[type='text'], textarea {
|
||||||
|
border : 1px solid gray;
|
||||||
}
|
}
|
||||||
&.thumbnail{
|
&.thumbnail{
|
||||||
height : 1.4em;
|
height : 1.4em;
|
||||||
@@ -72,6 +88,11 @@
|
|||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size : 0.6em;
|
||||||
|
font-style : italic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -122,10 +143,6 @@
|
|||||||
button.unpublish{
|
button.unpublish{
|
||||||
.button(@silver);
|
.button(@silver);
|
||||||
}
|
}
|
||||||
small{
|
|
||||||
font-size : 0.6em;
|
|
||||||
font-style : italic;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete.field .value{
|
.delete.field .value{
|
||||||
@@ -190,6 +207,7 @@
|
|||||||
}
|
}
|
||||||
.field .list {
|
.field .list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
|||||||
34
client/homebrew/editor/metadataEditor/validations.js
Normal file
34
client/homebrew/editor/metadataEditor/validations.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = {
|
||||||
|
title : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 100 ? 'Max title length of 100 characters' : null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
description : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 500 ? 'Max description length of 500 characters.' : null;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
thumbnail : [
|
||||||
|
(value)=>{
|
||||||
|
return value?.length > 256 ? 'Max URL length of 256 characters.' : null;
|
||||||
|
},
|
||||||
|
(value)=>{
|
||||||
|
if(value?.length == 0){return null;}
|
||||||
|
try {
|
||||||
|
Boolean(new URL(value));
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return 'Must be a valid URL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
language : [
|
||||||
|
(value)=>{
|
||||||
|
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null;
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +9,9 @@ const StringArrayEditor = createClass({
|
|||||||
label : '',
|
label : '',
|
||||||
values : [],
|
values : [],
|
||||||
valuePatterns : null,
|
valuePatterns : null,
|
||||||
|
validators : [],
|
||||||
placeholder : '',
|
placeholder : '',
|
||||||
|
notes : [],
|
||||||
unique : false,
|
unique : false,
|
||||||
cannotEdit : [],
|
cannotEdit : [],
|
||||||
onChange : ()=>{}
|
onChange : ()=>{}
|
||||||
@@ -83,7 +85,8 @@ const StringArrayEditor = createClass({
|
|||||||
}
|
}
|
||||||
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||||
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||||
return matchesPatterns && uniqueIfSet;
|
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
|
||||||
|
return matchesPatterns && uniqueIfSet && passesValidators;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleValueInputKeyDown : function(event, index) {
|
handleValueInputKeyDown : function(event, index) {
|
||||||
@@ -123,8 +126,9 @@ const StringArrayEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <div className='field values'>
|
return <div className='field'>
|
||||||
<label>{this.props.label}</label>
|
<label>{this.props.label}</label>
|
||||||
|
<div style={{ flex: '1 0' }}>
|
||||||
<div className='list'>
|
<div className='list'>
|
||||||
{valueElements}
|
{valueElements}
|
||||||
<div className='input-group'>
|
<div className='input-group'>
|
||||||
@@ -135,6 +139,9 @@ const StringArrayEditor = createClass({
|
|||||||
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this.props.notes ? this.props.notes.map((n)=><p><small>{n}</small></p>) : null}
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const SharePage = require('./pages/sharePage/sharePage.jsx');
|
|||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||||
|
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = (props)=>{
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -61,7 +62,8 @@ const Homebrew = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function (){
|
render : function (){
|
||||||
return <Router location={this.props.url}>
|
return (
|
||||||
|
<Router location={this.props.url}>
|
||||||
<div className='homebrew'>
|
<div className='homebrew'>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||||
@@ -73,12 +75,14 @@ const Homebrew = createClass({
|
|||||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
|
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
||||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>;
|
</Router>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,14 @@ const Account = createClass({
|
|||||||
>
|
>
|
||||||
brews
|
brews
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
|
<Nav.item
|
||||||
|
className='account'
|
||||||
|
color='orange'
|
||||||
|
icon='fas fa-user'
|
||||||
|
href='/account'
|
||||||
|
>
|
||||||
|
account
|
||||||
|
</Nav.item>
|
||||||
<Nav.item
|
<Nav.item
|
||||||
className='logout'
|
className='logout'
|
||||||
color='red'
|
color='red'
|
||||||
|
|||||||
85
client/homebrew/navbar/error-navitem.jsx
Normal file
85
client/homebrew/navbar/error-navitem.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
require('./error-navitem.less');
|
||||||
|
const React = require('react');
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
|
const ErrorNavItem = createClass({
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
error : '',
|
||||||
|
parent : null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
render : function() {
|
||||||
|
const clearError = ()=>{
|
||||||
|
const state = {
|
||||||
|
error : null
|
||||||
|
};
|
||||||
|
if(this.props.parent.state.isSaving) {
|
||||||
|
state.isSaving = false;
|
||||||
|
}
|
||||||
|
this.props.parent.setState(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = this.props.error;
|
||||||
|
const response = error.response;
|
||||||
|
const status = response.status;
|
||||||
|
const message = response.body?.message;
|
||||||
|
let errMsg = '';
|
||||||
|
try {
|
||||||
|
errMsg += `${error.toString()}\n\n`;
|
||||||
|
errMsg += `\`\`\`\n${error.stack}\n`;
|
||||||
|
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
|
||||||
|
console.log(errMsg);
|
||||||
|
} catch (e){}
|
||||||
|
|
||||||
|
if(status === 409) {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
{message ?? 'Conflict: please refresh to get latest changes'}
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
} else if(status === 412) {
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(response.req.url.match(/^\/api.*Google.*$/m)){
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer' onClick={clearError}>
|
||||||
|
Looks like your Google credentials have
|
||||||
|
expired! Visit our log in page to sign out
|
||||||
|
and sign back in with Google,
|
||||||
|
then try saving again!
|
||||||
|
<a target='_blank' rel='noopener noreferrer'
|
||||||
|
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
|
||||||
|
<div className='confirm'>
|
||||||
|
Sign In
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div className='deny'>
|
||||||
|
Not Now
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer'>
|
||||||
|
Looks like there was a problem saving. <br />
|
||||||
|
Report the issue <a target='_blank' rel='noopener noreferrer' href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||||
|
here
|
||||||
|
</a>.
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = ErrorNavItem;
|
||||||
77
client/homebrew/navbar/error-navitem.less
Normal file
77
client/homebrew/navbar/error-navitem.less
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
.navItem {
|
||||||
|
&.error {
|
||||||
|
position : relative;
|
||||||
|
background-color : @red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.errorContainer{
|
||||||
|
animation-name: glideDown;
|
||||||
|
animation-duration: 0.4s;
|
||||||
|
position : absolute;
|
||||||
|
top : 100%;
|
||||||
|
left : 50%;
|
||||||
|
z-index : 1000;
|
||||||
|
width : 140px;
|
||||||
|
padding : 3px;
|
||||||
|
color : white;
|
||||||
|
background-color : #333;
|
||||||
|
border : 3px solid #444;
|
||||||
|
border-radius : 5px;
|
||||||
|
transform : translate(-50% + 3px, 10px);
|
||||||
|
text-align : center;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
text-transform : uppercase;
|
||||||
|
a{
|
||||||
|
color : @teal;
|
||||||
|
}
|
||||||
|
&:before {
|
||||||
|
content: "";
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-top: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid #444;
|
||||||
|
left: 53px;
|
||||||
|
top: -23px;
|
||||||
|
}
|
||||||
|
&:after {
|
||||||
|
content: "";
|
||||||
|
width: 0px;
|
||||||
|
height: 0px;
|
||||||
|
position: absolute;
|
||||||
|
border-left: 10px solid transparent;
|
||||||
|
border-right: 10px solid transparent;
|
||||||
|
border-top: 10px solid transparent;
|
||||||
|
border-bottom: 10px solid #333;
|
||||||
|
left: 53px;
|
||||||
|
top: -19px;
|
||||||
|
}
|
||||||
|
.deny {
|
||||||
|
width : 48%;
|
||||||
|
margin : 1px;
|
||||||
|
padding : 5px;
|
||||||
|
background-color : #333;
|
||||||
|
display : inline-block;
|
||||||
|
border-left : 1px solid #666;
|
||||||
|
.animate(background-color);
|
||||||
|
&:hover{
|
||||||
|
background-color : red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.confirm {
|
||||||
|
width : 48%;
|
||||||
|
margin : 1px;
|
||||||
|
padding : 5px;
|
||||||
|
background-color : #333;
|
||||||
|
display : inline-block;
|
||||||
|
color : white;
|
||||||
|
.animate(background-color);
|
||||||
|
&:hover{
|
||||||
|
background-color : teal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,12 @@ module.exports = function(props){
|
|||||||
rel='noopener noreferrer'>
|
rel='noopener noreferrer'>
|
||||||
report issue
|
report issue
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
|
<Nav.item color='green' icon='fas fa-question-circle'
|
||||||
|
href='/faq'
|
||||||
|
newTab={true}
|
||||||
|
rel='noopener noreferrer'>
|
||||||
|
FAQ
|
||||||
|
</Nav.item>
|
||||||
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
||||||
href='/migrate'
|
href='/migrate'
|
||||||
newTab={true}
|
newTab={true}
|
||||||
|
|||||||
@@ -115,8 +115,36 @@
|
|||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
border-top : 1px solid #888;
|
border-top : 1px solid #888;
|
||||||
|
overflow : clip;
|
||||||
|
.clear{
|
||||||
|
display : none;
|
||||||
|
position : absolute;
|
||||||
|
top : 50%;
|
||||||
|
transform : translateY(-50%);
|
||||||
|
right : 0px;
|
||||||
|
width : 20px;
|
||||||
|
height : 100%;
|
||||||
|
background-color : #333;
|
||||||
|
opacity : 70%;
|
||||||
|
border-radius : 3px;
|
||||||
|
&:hover {
|
||||||
|
opacity : 100%;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
text-align : center;
|
||||||
|
font-size : 10px;
|
||||||
|
margin : 0;
|
||||||
|
height :100%;
|
||||||
|
width :100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color : @blue;
|
background-color : @blue;
|
||||||
|
|
||||||
|
.clear{
|
||||||
|
display : grid;
|
||||||
|
place-content : center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.title{
|
.title{
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
|
|||||||
@@ -119,6 +119,25 @@ const RecentItems = createClass({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
removeItem : function(url, evt){
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||||
|
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||||
|
|
||||||
|
edited = edited.filter((item)=>{ return (item.url !== url);});
|
||||||
|
viewed = viewed.filter((item)=>{ return (item.url !== url);});
|
||||||
|
|
||||||
|
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||||
|
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
edit : edited,
|
||||||
|
view : viewed
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
renderDropdown : function(){
|
renderDropdown : function(){
|
||||||
if(!this.state.showDropdown) return null;
|
if(!this.state.showDropdown) return null;
|
||||||
|
|
||||||
@@ -127,6 +146,7 @@ const RecentItems = createClass({
|
|||||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} 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>
|
||||||
|
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||||
</a>;
|
</a>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
74
client/homebrew/pages/accountPage/accountPage.jsx
Normal file
74
client/homebrew/pages/accountPage/accountPage.jsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const cx = require('classnames');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
|
||||||
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
|
const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||||
|
|
||||||
|
const AccountPage = createClass({
|
||||||
|
displayName : 'AccountPage',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
brew : {},
|
||||||
|
uiItems : {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
uiItems : this.props.uiItems
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
renderNavItems : function() {
|
||||||
|
return <Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrew />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUiItems : function() {
|
||||||
|
return <>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||||
|
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
|
||||||
|
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
||||||
|
</div>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||||
|
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
||||||
|
</div>
|
||||||
|
<div className='dataGroup'>
|
||||||
|
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||||
|
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
||||||
|
{this.props.uiItems.googleId &&
|
||||||
|
<p>
|
||||||
|
<strong>Brews on Google Drive: </strong> {this.props.uiItems.googleCount ?? <>Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a></>}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</>;
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <UIPage brew={this.props.brew}>
|
||||||
|
{this.renderUiItems()}
|
||||||
|
</UIPage>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = AccountPage;
|
||||||
@@ -4,7 +4,7 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const request = require('superagent');
|
const request = require('../../../../utils/request-middleware.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.png');
|
const googleDriveIcon = require('../../../../googleDrive.png');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
@@ -18,7 +18,8 @@ const BrewItem = createClass({
|
|||||||
description : '',
|
description : '',
|
||||||
authors : [],
|
authors : [],
|
||||||
stubbed : true
|
stubbed : true
|
||||||
}
|
},
|
||||||
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -33,8 +34,12 @@ const BrewItem = createClass({
|
|||||||
|
|
||||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end((err, res)=>{
|
||||||
|
if(err) {
|
||||||
|
this.props.reportError(err);
|
||||||
|
} else {
|
||||||
location.reload();
|
location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -117,7 +122,7 @@ const BrewItem = createClass({
|
|||||||
<i className='fas fa-tags'/>
|
<i className='fas fa-tags'/>
|
||||||
{brew.tags.map((tag, idx)=>{
|
{brew.tags.map((tag, idx)=>{
|
||||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||||
return <span className={matches[1]}>{matches[2]}</span>;
|
return <span key={idx} className={matches[1]}>{matches[2]}</span>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</> : <></>
|
</> : <></>
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const ListPage = createClass({
|
|||||||
brews : []
|
brews : []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
navItems : <></>
|
navItems : <></>,
|
||||||
|
reportError : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -81,7 +82,7 @@ const ListPage = createClass({
|
|||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
return _.map(brews, (brew, idx)=>{
|
return _.map(brews, (brew, idx)=>{
|
||||||
return <BrewItem brew={brew} key={idx}/>;
|
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,29 @@
|
|||||||
font-size : 1.3em;
|
font-size : 1.3em;
|
||||||
font-style : italic;
|
font-style : italic;
|
||||||
}
|
}
|
||||||
|
.brewCollection {
|
||||||
|
h1:hover{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.active::before, .inactive::before {
|
||||||
|
font-family: 'Font Awesome 5 Free';
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 0.6cm;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
color: var(--HB_Color_HeaderText);
|
||||||
|
}
|
||||||
|
.active::before {
|
||||||
|
content: '\f107';
|
||||||
|
}
|
||||||
|
.inactive {
|
||||||
|
color: #707070;
|
||||||
|
}
|
||||||
|
.inactive::before {
|
||||||
|
content: '\f105';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sort-container{
|
.sort-container{
|
||||||
|
|||||||
38
client/homebrew/pages/basePages/uiPage/uiPage.jsx
Normal file
38
client/homebrew/pages/basePages/uiPage/uiPage.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
require('./uiPage.less');
|
||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Navbar = require('../../../navbar/navbar.jsx');
|
||||||
|
const NewBrewItem = require('../../../navbar/newbrew.navitem.jsx');
|
||||||
|
const HelpNavItem = require('../../../navbar/help.navitem.jsx');
|
||||||
|
const RecentNavItem = require('../../../navbar/recent.navitem.jsx').both;
|
||||||
|
const Account = require('../../../navbar/account.navitem.jsx');
|
||||||
|
|
||||||
|
|
||||||
|
const UIPage = createClass({
|
||||||
|
displayName : 'UIPage',
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <div className='uiPage sitePage'>
|
||||||
|
<Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||||
|
</Nav.section>
|
||||||
|
|
||||||
|
<Nav.section>
|
||||||
|
<NewBrewItem />
|
||||||
|
<HelpNavItem />
|
||||||
|
<RecentNavItem />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>
|
||||||
|
|
||||||
|
<div className='content'>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = UIPage;
|
||||||
47
client/homebrew/pages/basePages/uiPage/uiPage.less
Normal file
47
client/homebrew/pages/basePages/uiPage/uiPage.less
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
.uiPage{
|
||||||
|
.content{
|
||||||
|
overflow-y : hidden;
|
||||||
|
width : 90vw;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
margin-top: 25px;
|
||||||
|
padding: 2% 4%;
|
||||||
|
font-size: 0.8em;
|
||||||
|
line-height: 1.8em;
|
||||||
|
.dataGroup{
|
||||||
|
padding: 6px 20px 15px;
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 5px 0px;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4{
|
||||||
|
font-weight: 900;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0.5em 30% 0.25em 0;
|
||||||
|
border-bottom: 2px solid slategrey;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
border-bottom: 2px solid darkslategrey;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.75em;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
svg {
|
||||||
|
width: 19px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ require('./editPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -12,6 +12,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const googleDriveActive = require('../../googleDrive.png');
|
const googleDriveActive = require('../../googleDrive.png');
|
||||||
const googleDriveInactive = require('../../googleDriveMono.png');
|
const googleDriveInactive = require('../../googleDriveMono.png');
|
||||||
|
|
||||||
@@ -30,24 +33,7 @@ const EditPage = createClass({
|
|||||||
displayName : 'EditPage',
|
displayName : 'EditPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW_LOAD
|
||||||
text : '',
|
|
||||||
style : '',
|
|
||||||
shareId : null,
|
|
||||||
editId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
gDrive : false,
|
|
||||||
trashed : false,
|
|
||||||
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
tags : '',
|
|
||||||
published : false,
|
|
||||||
authors : [],
|
|
||||||
systems : [],
|
|
||||||
renderer : 'legacy'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -60,7 +46,7 @@ const EditPage = createClass({
|
|||||||
alertLoginToTransfer : false,
|
alertLoginToTransfer : false,
|
||||||
saveGoogle : this.props.brew.googleId ? true : false,
|
saveGoogle : this.props.brew.googleId ? true : false,
|
||||||
confirmGoogleTransfer : false,
|
confirmGoogleTransfer : false,
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||||
url : '',
|
url : '',
|
||||||
autoSave : true,
|
autoSave : true,
|
||||||
@@ -75,10 +61,9 @@ const EditPage = createClass({
|
|||||||
url : window.location.href
|
url : window.location.href
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||||
|
|
||||||
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) }, ()=>{
|
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
||||||
if(this.state.autoSave){
|
if(this.state.autoSave){
|
||||||
this.trySave();
|
this.trySave();
|
||||||
} else {
|
} else {
|
||||||
@@ -172,7 +157,10 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||||
}));
|
}));
|
||||||
this.clearErrors();
|
this.setState({
|
||||||
|
error : null,
|
||||||
|
isSaving : false
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
closeAlerts : function(event){
|
closeAlerts : function(event){
|
||||||
@@ -188,24 +176,16 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
saveGoogle : !prevState.saveGoogle,
|
saveGoogle : !prevState.saveGoogle,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
errors : null
|
error : null
|
||||||
}), ()=>this.save());
|
}), ()=>this.save());
|
||||||
},
|
},
|
||||||
|
|
||||||
clearErrors : function(){
|
|
||||||
this.setState({
|
|
||||||
errors : null,
|
|
||||||
isSaving : false
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
isSaving : true,
|
isSaving : true,
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -220,8 +200,9 @@ const EditPage = createClass({
|
|||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error Updating Local Brew');
|
console.log('Error Updating Local Brew');
|
||||||
this.setState({ errors: err });
|
this.setState({ error: err });
|
||||||
});
|
});
|
||||||
|
if(!res) return;
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
this.savedBrew = res.body;
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
@@ -230,7 +211,8 @@ const EditPage = createClass({
|
|||||||
brew : { ...prevState.brew,
|
brew : { ...prevState.brew,
|
||||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||||
editId : this.savedBrew.editId,
|
editId : this.savedBrew.editId,
|
||||||
shareId : this.savedBrew.shareId
|
shareId : this.savedBrew.shareId,
|
||||||
|
version : this.savedBrew.version
|
||||||
},
|
},
|
||||||
isPending : false,
|
isPending : false,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
@@ -280,67 +262,6 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
if(this.state.errors){
|
|
||||||
let errMsg = '';
|
|
||||||
try {
|
|
||||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
|
||||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
|
||||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
|
||||||
console.log(errMsg);
|
|
||||||
} catch (e){}
|
|
||||||
|
|
||||||
// if(this.state.errors.status == '401'){
|
|
||||||
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
// Oops!
|
|
||||||
// <div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
// You must be signed in to a Google account
|
|
||||||
// to save this to<br />Google Drive!<br />
|
|
||||||
// <a target='_blank' rel='noopener noreferrer'
|
|
||||||
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
// <div className='confirm'>
|
|
||||||
// Sign In
|
|
||||||
// </div>
|
|
||||||
// </a>
|
|
||||||
// <div className='deny'>
|
|
||||||
// Not Now
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </Nav.item>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
Looks like your Google credentials have
|
|
||||||
expired! Visit our log in page to sign out
|
|
||||||
and sign back in with Google,
|
|
||||||
then try saving again!
|
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
<div className='confirm'>
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div className='deny'>
|
|
||||||
Not Now
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer'>
|
|
||||||
Looks like there was a problem saving. <br />
|
|
||||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
|
||||||
here
|
|
||||||
</a>.
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.autoSaveWarning && this.hasChanges()){
|
if(this.state.autoSaveWarning && this.hasChanges()){
|
||||||
this.setAutosaveWarning();
|
this.setAutosaveWarning();
|
||||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||||
@@ -384,6 +305,12 @@ const EditPage = createClass({
|
|||||||
this.warningTimer;
|
this.warningTimer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errorReported : function(error) {
|
||||||
|
this.setState({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
renderAutoSaveButton : function(){
|
renderAutoSaveButton : function(){
|
||||||
return <Nav.item onClick={this.handleAutoSave}>
|
return <Nav.item onClick={this.handleAutoSave}>
|
||||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||||
@@ -428,10 +355,13 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
<Nav.dropdown className='save-menu'>
|
<Nav.dropdown className='save-menu'>
|
||||||
{this.renderSaveButton()}
|
{this.renderSaveButton()}
|
||||||
{this.renderAutoSaveButton()}
|
{this.renderAutoSaveButton()}
|
||||||
</Nav.dropdown>
|
</Nav.dropdown>
|
||||||
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem/>
|
<HelpNavItem/>
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
@@ -469,6 +399,7 @@ const EditPage = createClass({
|
|||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors} />
|
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors} />
|
||||||
|
|||||||
@@ -13,10 +13,6 @@
|
|||||||
cursor : initial;
|
cursor : initial;
|
||||||
color : #666;
|
color : #666;
|
||||||
}
|
}
|
||||||
&.error{
|
|
||||||
position : relative;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.googleDriveStorage {
|
.googleDriveStorage {
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -26,74 +22,4 @@
|
|||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
}
|
||||||
.errorContainer{
|
|
||||||
animation-name: glideDown;
|
|
||||||
animation-duration: 0.4s;
|
|
||||||
position : absolute;
|
|
||||||
top : 100%;
|
|
||||||
left : 50%;
|
|
||||||
z-index : 500;
|
|
||||||
width : 140px;
|
|
||||||
padding : 3px;
|
|
||||||
color : white;
|
|
||||||
background-color : #333;
|
|
||||||
border : 3px solid #444;
|
|
||||||
border-radius : 5px;
|
|
||||||
transform : translate(-50% + 3px, 10px);
|
|
||||||
text-align : center;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
text-transform : uppercase;
|
|
||||||
a{
|
|
||||||
color : @teal;
|
|
||||||
}
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #444;
|
|
||||||
left: 53px;
|
|
||||||
top: -23px;
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #333;
|
|
||||||
left: 53px;
|
|
||||||
top: -19px;
|
|
||||||
}
|
|
||||||
.deny {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
border-left : 1px solid #666;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.confirm {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
color : white;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : teal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -12,35 +12,38 @@ const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
|||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const HomePage = createClass({
|
const HomePage = createClass({
|
||||||
displayName : 'HomePage',
|
displayName : 'HomePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW,
|
||||||
text : '',
|
|
||||||
},
|
|
||||||
ver : '0.0.0'
|
ver : '0.0.0'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text
|
welcomeText : this.props.brew.text,
|
||||||
|
error : undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(this.state.brew)
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err) return;
|
if(err) {
|
||||||
|
this.setState({ error: err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const brew = res.body;
|
const brew = res.body;
|
||||||
window.location = `/edit/${brew.editId}`;
|
window.location = `/edit/${brew.editId}`;
|
||||||
});
|
});
|
||||||
@@ -56,6 +59,10 @@ const HomePage = createClass({
|
|||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar ver={this.props.ver}>
|
return <Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
|
|||||||
@@ -40,4 +40,11 @@
|
|||||||
right : 350px;
|
right : 350px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navItem.save{
|
||||||
|
background-color: @orange;
|
||||||
|
&:hover{
|
||||||
|
background-color: @green;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ require('./newPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
@@ -26,36 +29,18 @@ const NewPage = createClass({
|
|||||||
displayName : 'NewPage',
|
displayName : 'NewPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW
|
||||||
text : '',
|
|
||||||
style : undefined,
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
renderer : 'V3',
|
|
||||||
theme : '5ePHB'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
let brew = this.props.brew;
|
const brew = this.props.brew;
|
||||||
|
|
||||||
if(this.props.brew.shareId) {
|
|
||||||
brew = {
|
|
||||||
text : brew.text ?? '',
|
|
||||||
style : brew.style ?? undefined,
|
|
||||||
title : brew.title ?? '',
|
|
||||||
description : brew.description ?? '',
|
|
||||||
renderer : brew.renderer ?? 'legacy',
|
|
||||||
theme : brew.theme ?? '5ePHB'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brew : brew,
|
brew : brew,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text)
|
htmlErrors : Markdown.validate(brew.text)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -83,6 +68,7 @@ const NewPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
|
if(brew.style)
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
|
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
|
||||||
},
|
},
|
||||||
@@ -137,14 +123,6 @@ const NewPage = createClass({
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
clearErrors : function(){
|
|
||||||
this.setState({
|
|
||||||
errors : null,
|
|
||||||
isSaving : false
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
this.setState({
|
this.setState({
|
||||||
isSaving : true
|
isSaving : true
|
||||||
@@ -167,7 +145,7 @@ const NewPage = createClass({
|
|||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
this.setState({ isSaving: false, errors: err });
|
this.setState({ isSaving: false, error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
@@ -179,67 +157,6 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
if(this.state.errors){
|
|
||||||
let errMsg = '';
|
|
||||||
try {
|
|
||||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
|
||||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
|
||||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
|
||||||
console.log(errMsg);
|
|
||||||
} catch (e){}
|
|
||||||
|
|
||||||
// if(this.state.errors.status == '401'){
|
|
||||||
// return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
// Oops!
|
|
||||||
// <div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
// You must be signed in to a Google account
|
|
||||||
// to save this to<br />Google Drive!<br />
|
|
||||||
// <a target='_blank' rel='noopener noreferrer'
|
|
||||||
// href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
// <div className='confirm'>
|
|
||||||
// Sign In
|
|
||||||
// </div>
|
|
||||||
// </a>
|
|
||||||
// <div className='deny'>
|
|
||||||
// Not Now
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
// </Nav.item>;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={this.clearErrors}>
|
|
||||||
Looks like your Google credentials have
|
|
||||||
expired! Visit our log in page to sign out
|
|
||||||
and sign back in with Google,
|
|
||||||
then try saving again!
|
|
||||||
<a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
|
|
||||||
<div className='confirm'>
|
|
||||||
Sign In
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div className='deny'>
|
|
||||||
Not Now
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer'>
|
|
||||||
Looks like there was a problem saving. <br />
|
|
||||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
|
||||||
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
|
||||||
here
|
|
||||||
</a>.
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.state.isSaving){
|
if(this.state.isSaving){
|
||||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||||
save...
|
save...
|
||||||
@@ -269,7 +186,10 @@ const NewPage = createClass({
|
|||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderSaveButton()}
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
this.renderSaveButton()
|
||||||
|
}
|
||||||
{this.renderLocalPrintButton()}
|
{this.renderLocalPrintButton()}
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
|
|||||||
@@ -4,79 +4,5 @@
|
|||||||
&:hover{
|
&:hover{
|
||||||
background-color: @green;
|
background-color: @green;
|
||||||
}
|
}
|
||||||
&.error{
|
|
||||||
position : relative;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.errorContainer{
|
|
||||||
animation-name: glideDown;
|
|
||||||
animation-duration: 0.4s;
|
|
||||||
position : absolute;
|
|
||||||
top : 100%;
|
|
||||||
left : 50%;
|
|
||||||
z-index : 100000;
|
|
||||||
width : 140px;
|
|
||||||
padding : 3px;
|
|
||||||
color : white;
|
|
||||||
background-color : #333;
|
|
||||||
border : 3px solid #444;
|
|
||||||
border-radius : 5px;
|
|
||||||
transform : translate(-50% + 3px, 10px);
|
|
||||||
text-align : center;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
text-transform : uppercase;
|
|
||||||
a{
|
|
||||||
color : @teal;
|
|
||||||
}
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #444;
|
|
||||||
left: 53px;
|
|
||||||
top: -23px;
|
|
||||||
}
|
|
||||||
&:after {
|
|
||||||
content: "";
|
|
||||||
width: 0px;
|
|
||||||
height: 0px;
|
|
||||||
position: absolute;
|
|
||||||
border-left: 10px solid transparent;
|
|
||||||
border-right: 10px solid transparent;
|
|
||||||
border-top: 10px solid transparent;
|
|
||||||
border-bottom: 10px solid #333;
|
|
||||||
left: 53px;
|
|
||||||
top: -19px;
|
|
||||||
}
|
|
||||||
.deny {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
border-left : 1px solid #666;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.confirm {
|
|
||||||
width : 48%;
|
|
||||||
margin : 1px;
|
|
||||||
padding : 5px;
|
|
||||||
background-color : #333;
|
|
||||||
display : inline-block;
|
|
||||||
color : white;
|
|
||||||
.animate(background-color);
|
|
||||||
&:hover{
|
|
||||||
background-color : teal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ const PrintPage = createClass({
|
|||||||
brew : {
|
brew : {
|
||||||
text : this.props.brew.text || '',
|
text : this.props.brew.text || '',
|
||||||
style : this.props.brew.style || undefined,
|
style : this.props.brew.style || undefined,
|
||||||
renderer : this.props.brew.renderer || 'legacy'
|
renderer : this.props.brew.renderer || 'legacy',
|
||||||
|
theme : this.props.brew.theme || '5ePHB'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -91,6 +92,7 @@ const PrintPage = createClass({
|
|||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
|
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
||||||
{baseThemePath &&
|
{baseThemePath &&
|
||||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,21 +12,13 @@ const Account = require('../../navbar/account.navitem.jsx');
|
|||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = createClass({
|
||||||
displayName : 'SharePage',
|
displayName : 'SharePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW_LOAD
|
||||||
title : '',
|
|
||||||
text : '',
|
|
||||||
style : '',
|
|
||||||
shareId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
views : 0,
|
|
||||||
renderer : ''
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
|
||||||
const UserPage = createClass({
|
const UserPage = createClass({
|
||||||
displayName : 'UserPage',
|
displayName : 'UserPage',
|
||||||
@@ -19,7 +20,8 @@ const UserPage = createClass({
|
|||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : [],
|
brews : [],
|
||||||
query : ''
|
query : '',
|
||||||
|
error : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -50,10 +52,19 @@ const UserPage = createClass({
|
|||||||
brewCollection : brewCollection
|
brewCollection : brewCollection
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
errorReported : function(error) {
|
||||||
|
this.setState({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
navItems : function() {
|
navItems : function() {
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
@@ -63,7 +74,7 @@ const UserPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
|
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
12
client/homebrew/utils/request-middleware.js
Normal file
12
client/homebrew/utils/request-middleware.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const request = require('superagent');
|
||||||
|
|
||||||
|
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
|
||||||
|
|
||||||
|
const requestMiddleware = {
|
||||||
|
get : (path)=>addHeader(request.get(path)),
|
||||||
|
put : (path)=>addHeader(request.put(path)),
|
||||||
|
post : (path)=>addHeader(request.post(path)),
|
||||||
|
delete : (path)=>addHeader(request.delete(path)),
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = requestMiddleware;
|
||||||
@@ -1,21 +1,22 @@
|
|||||||
module.exports = async(name, title = '', props = {})=>{
|
const template = async function(name, title='', props = {}){
|
||||||
const HOMEBREWERY_PUBLIC_URL=props.config.publicUrl;
|
const ogTags = [];
|
||||||
|
const ogMeta = props.ogMeta ?? {};
|
||||||
|
Object.entries(ogMeta).forEach(([key, value])=>{
|
||||||
|
if(!value) return;
|
||||||
|
const tag = `<meta property="og:${key}" content="${value}">`;
|
||||||
|
ogTags.push(tag);
|
||||||
|
});
|
||||||
|
const ogMetaTags = ogTags.join('\n');
|
||||||
|
|
||||||
return `
|
return `<!DOCTYPE html>
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||||
<meta property="og:title" content="${props.brew?.title || 'Homebrewery - Untitled Brew'}">
|
${ogMetaTags}
|
||||||
<meta property="og:url" content="${HOMEBREWERY_PUBLIC_URL}/${props.brew?.shareId ? `share/${props.brew.shareId}` : ''}">
|
<meta name="twitter:card" content="summary">
|
||||||
<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>
|
||||||
@@ -26,3 +27,5 @@ module.exports = async(name, title = '', props = {})=>{
|
|||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = template;
|
||||||
18705
package-lock.json
generated
18705
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
50
package.json
50
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.3.0",
|
"version": "3.6.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.11.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
@@ -14,11 +14,14 @@
|
|||||||
"quick": "node scripts/quick.js",
|
"quick": "node scripts/quick.js",
|
||||||
"build": "node scripts/buildHomebrew.js",
|
"build": "node scripts/buildHomebrew.js",
|
||||||
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||||
|
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||||
"lint": "eslint --fix **/*.{js,jsx}",
|
"lint": "eslint --fix **/*.{js,jsx}",
|
||||||
"lint:dry": "eslint **/*.{js,jsx}",
|
"lint:dry": "eslint **/*.{js,jsx}",
|
||||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||||
|
"test:coverage": "jest --coverage --silent",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
||||||
@@ -39,7 +42,21 @@
|
|||||||
"mode_modules",
|
"mode_modules",
|
||||||
"shared",
|
"shared",
|
||||||
"server"
|
"server"
|
||||||
]
|
],
|
||||||
|
"coverageThreshold" : {
|
||||||
|
"global" : {
|
||||||
|
"statements" : 25,
|
||||||
|
"branches" : 10,
|
||||||
|
"functions" : 22,
|
||||||
|
"lines" : 25
|
||||||
|
},
|
||||||
|
"server/homebrew.api.js" : {
|
||||||
|
"statements" : 65,
|
||||||
|
"branches" : 50,
|
||||||
|
"functions" : 60,
|
||||||
|
"lines" : 70
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
@@ -51,8 +68,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.19.3",
|
"@babel/core": "^7.20.12",
|
||||||
"@babel/plugin-transform-runtime": "^7.19.1",
|
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||||
"@babel/preset-env": "^7.19.4",
|
"@babel/preset-env": "^7.19.4",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"body-parser": "^1.20.1",
|
"body-parser": "^1.20.1",
|
||||||
@@ -60,35 +77,36 @@
|
|||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.1",
|
"dedent-tabs": "^0.10.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "10.1.0",
|
"fs-extra": "11.1.0",
|
||||||
"googleapis": "108.0.0",
|
"googleapis": "110.0.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "4.1.1",
|
"marked": "4.2.12",
|
||||||
"marked-extended-tables": "^1.0.5",
|
"marked-extended-tables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongoose": "^6.6.5",
|
"mongoose": "^6.8.4",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.0",
|
||||||
"react": "^16.14.0",
|
"npm": "^8.10.0",
|
||||||
"react-dom": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
"react-frame-component": "4.1.3",
|
"react-frame-component": "4.1.3",
|
||||||
"react-router-dom": "6.4.2",
|
"react-router-dom": "6.7.0",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.25.0",
|
"eslint": "^8.32.0",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.32.1",
|
||||||
"jest": "^29.2.1",
|
"jest": "^29.2.2",
|
||||||
"supertest": "^6.3.0"
|
"supertest": "^6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,6 @@ fs.emptyDirSync('./build');
|
|||||||
if(isDev){
|
if(isDev){
|
||||||
livereload('./build');
|
livereload('./build');
|
||||||
watchFile('./server.js', {
|
watchFile('./server.js', {
|
||||||
watch : ['./client', './server'] // Watch additional folders if you want
|
watch : ['./client', './server', './themes'] // Watch additional folders if you want
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
143
server/app.js
143
server/app.js
@@ -1,4 +1,4 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
|
|
||||||
@@ -15,6 +15,8 @@ const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
|||||||
const sanitizeFilename = require('sanitize-filename');
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
const splitTextStyleAndMetadata = (brew)=>{
|
||||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||||
if(brew.text.startsWith('```metadata')) {
|
if(brew.text.startsWith('```metadata')) {
|
||||||
@@ -29,7 +31,6 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
brew.style = brew.text.slice(7, index - 1);
|
brew.style = brew.text.slice(7, index - 1);
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
}
|
}
|
||||||
_.defaults(brew, { 'renderer': 'legacy', 'theme': '5ePHB' });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
@@ -77,6 +78,14 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
|||||||
|
|
||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
|
const defaultMetaTags = {
|
||||||
|
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
|
||||||
|
title : 'The Homebrewery',
|
||||||
|
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
|
||||||
|
image : `${config.get('publicUrl')}/thumbnail.png`,
|
||||||
|
type : 'website'
|
||||||
|
};
|
||||||
|
|
||||||
//Robots.txt
|
//Robots.txt
|
||||||
app.get('/robots.txt', (req, res)=>{
|
app.get('/robots.txt', (req, res)=>{
|
||||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||||
@@ -87,17 +96,29 @@ app.get('/', (req, res, next)=>{
|
|||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeText,
|
text : welcomeText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Homepage',
|
||||||
|
description : 'Homepage'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
//Home page v3
|
//Home page Legacy
|
||||||
app.get('/legacy', (req, res, next)=>{
|
app.get('/legacy', (req, res, next)=>{
|
||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeTextLegacy,
|
text : welcomeTextLegacy,
|
||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Homepage (Legacy)',
|
||||||
|
description : 'Homepage'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -107,7 +128,13 @@ app.get('/migrate', (req, res, next)=>{
|
|||||||
req.brew = {
|
req.brew = {
|
||||||
text : migrateText,
|
text : migrateText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'v3 Migration Guide',
|
||||||
|
description : 'A brief guide to converting Legacy documents to the v3 renderer.'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -118,7 +145,13 @@ app.get('/changelog', async (req, res, next)=>{
|
|||||||
title : 'Changelog',
|
title : 'Changelog',
|
||||||
text : changelogText,
|
text : changelogText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'Changelog',
|
||||||
|
description : 'Development changelog.'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -129,7 +162,13 @@ app.get('/faq', async (req, res, next)=>{
|
|||||||
title : 'FAQ',
|
title : 'FAQ',
|
||||||
text : faqText,
|
text : faqText,
|
||||||
renderer : 'V3'
|
renderer : 'V3'
|
||||||
|
},
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'FAQ',
|
||||||
|
description : 'Frequently Asked Questions'
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
@@ -153,12 +192,19 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
sanitizeBrew(brew, 'share');
|
sanitizeBrew(brew, 'share');
|
||||||
const prefix = 'HB - ';
|
const prefix = 'HB - ';
|
||||||
|
|
||||||
|
const encodeRFC3986ValueChars = (str)=>{
|
||||||
|
return (
|
||||||
|
encodeURIComponent(str)
|
||||||
|
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||||
res.set({
|
res.set({
|
||||||
'Cache-Control' : 'no-cache',
|
'Cache-Control' : 'no-cache',
|
||||||
'Content-Type' : 'text/plain',
|
'Content-Type' : 'text/plain',
|
||||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
|
||||||
});
|
});
|
||||||
res.status(200).send(brew.text);
|
res.status(200).send(brew.text);
|
||||||
});
|
});
|
||||||
@@ -167,6 +213,12 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
app.get('/user/:username', async (req, res, next)=>{
|
app.get('/user/:username', async (req, res, next)=>{
|
||||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : `${req.params.username}'s Collection`,
|
||||||
|
description : 'View my collection of homebrew on the Homebrewery.'
|
||||||
|
// type : could be 'profile'?
|
||||||
|
};
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
'googleId',
|
'googleId',
|
||||||
'title',
|
'title',
|
||||||
@@ -224,6 +276,14 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : req.brew.title || 'Untitled Brew',
|
||||||
|
description : req.brew.description || 'No description.',
|
||||||
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
|
type : 'article'
|
||||||
|
};
|
||||||
|
|
||||||
sanitizeBrew(req.brew, 'edit');
|
sanitizeBrew(req.brew, 'edit');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||||
@@ -234,7 +294,14 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
|||||||
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||||
sanitizeBrew(req.brew, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
|
req.brew.views = 0;
|
||||||
req.brew.title = `CLONE - ${req.brew.title}`;
|
req.brew.title = `CLONE - ${req.brew.title}`;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : 'New',
|
||||||
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||||
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -242,6 +309,13 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
const { brew } = req;
|
const { brew } = req;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : req.brew.title || 'Untitled Brew',
|
||||||
|
description : req.brew.description || 'No description.',
|
||||||
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
|
type : 'article'
|
||||||
|
};
|
||||||
|
|
||||||
if(req.params.id.length > 12 && !brew._id) {
|
if(req.params.id.length > 12 && !brew._id) {
|
||||||
const googleId = req.params.id.slice(0, -12);
|
const googleId = req.params.id.slice(0, -12);
|
||||||
const shareId = req.params.id.slice(-12);
|
const shareId = req.params.id.slice(-12);
|
||||||
@@ -262,6 +336,61 @@ app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Account Page
|
||||||
|
app.get('/account', asyncHandler(async (req, res, next)=>{
|
||||||
|
const data = {};
|
||||||
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
|
let auth;
|
||||||
|
let googleCount = [];
|
||||||
|
if(req.account) {
|
||||||
|
if(req.account.googleId) {
|
||||||
|
try {
|
||||||
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
|
} catch (e) {
|
||||||
|
auth = undefined;
|
||||||
|
console.log('Google auth check failed!');
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
if(auth.credentials.access_token) {
|
||||||
|
try {
|
||||||
|
googleCount = await GoogleActions.listGoogleBrews(auth);
|
||||||
|
} catch (e) {
|
||||||
|
googleCount = undefined;
|
||||||
|
console.log('List Google files failed!');
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
|
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||||
|
.catch((err)=>{
|
||||||
|
mongoCount = 0;
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.uiItems = {
|
||||||
|
username : req.account.username,
|
||||||
|
issued : req.account.issued,
|
||||||
|
googleId : Boolean(req.account.googleId),
|
||||||
|
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||||
|
mongoCount : mongoCount,
|
||||||
|
googleCount : googleCount?.length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
req.brew = data;
|
||||||
|
|
||||||
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
|
title : `Account Page`,
|
||||||
|
description : null
|
||||||
|
};
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
const nodeEnv = config.get('node_env');
|
const nodeEnv = config.get('node_env');
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
||||||
// Local only
|
// Local only
|
||||||
@@ -276,11 +405,10 @@ if(isLocalEnvironment){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Render the page
|
//Render the page
|
||||||
const templateFn = require('./../client/template.js');
|
const templateFn = require('./../client/template.js');
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
|
|
||||||
// Create configuration object
|
// Create configuration object
|
||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
@@ -296,7 +424,8 @@ app.use(asyncHandler(async (req, res, next)=>{
|
|||||||
account : req.account,
|
account : req.account,
|
||||||
enable_v3 : config.get('enable_v3'),
|
enable_v3 : config.get('enable_v3'),
|
||||||
enable_themes : config.get('enable_themes'),
|
enable_themes : config.get('enable_themes'),
|
||||||
config : configuration
|
config : configuration,
|
||||||
|
ogMeta : req.ogMeta
|
||||||
};
|
};
|
||||||
const title = req.brew ? req.brew.title : '';
|
const title = req.brew ? req.brew.title : '';
|
||||||
const page = await templateFn('homebrew', title, props)
|
const page = await templateFn('homebrew', title, props)
|
||||||
|
|||||||
36
server/brewDefaults.js
Normal file
36
server/brewDefaults.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
// Default properties for newly-created brews
|
||||||
|
const DEFAULT_BREW = {
|
||||||
|
title : '',
|
||||||
|
text : '',
|
||||||
|
style : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : undefined,
|
||||||
|
shareId : undefined,
|
||||||
|
createdAt : undefined,
|
||||||
|
updatedAt : undefined,
|
||||||
|
renderer : 'V3',
|
||||||
|
theme : '5ePHB',
|
||||||
|
authors : [],
|
||||||
|
tags : [],
|
||||||
|
systems : [],
|
||||||
|
thumbnail : '',
|
||||||
|
published : false,
|
||||||
|
pageCount : 1,
|
||||||
|
gDrive : false,
|
||||||
|
trashed : false
|
||||||
|
|
||||||
|
};
|
||||||
|
// Default values for older brews with missing properties
|
||||||
|
// e.g., missing "renderer" is assumed to be "legacy"
|
||||||
|
const DEFAULT_BREW_LOAD = _.defaults(
|
||||||
|
{
|
||||||
|
renderer : 'legacy',
|
||||||
|
},
|
||||||
|
DEFAULT_BREW);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
DEFAULT_BREW,
|
||||||
|
DEFAULT_BREW_LOAD
|
||||||
|
};
|
||||||
@@ -5,24 +5,28 @@ const { nanoid } = require('nanoid');
|
|||||||
const token = require('./token.js');
|
const token = require('./token.js');
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
|
|
||||||
|
let serviceAuth;
|
||||||
|
if(!config.get('service_account')){
|
||||||
|
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
|
||||||
|
} else {
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||||
JSON.parse(config.get('service_account')) :
|
JSON.parse(config.get('service_account')) :
|
||||||
config.get('service_account');
|
config.get('service_account');
|
||||||
let serviceAuth;
|
|
||||||
try {
|
try {
|
||||||
serviceAuth = google.auth.fromJSON(keys);
|
serviceAuth = google.auth.fromJSON(keys);
|
||||||
serviceAuth.scopes = [
|
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
'https://www.googleapis.com/auth/drive'
|
|
||||||
];
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
|
console.log('Please make sure the Google Service Account is set up properly in your config files.');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
||||||
|
|
||||||
const GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
authCheck : (account, res)=>{
|
authCheck : (account, res, updateTokens=true)=>{
|
||||||
if(!account || !account.googleId){ // If not signed into Google
|
if(!account || !account.googleId){ // If not signed into Google
|
||||||
const err = new Error('Not Signed In');
|
const err = new Error('Not Signed In');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
@@ -40,7 +44,7 @@ const GoogleActions = {
|
|||||||
refresh_token : account.googleRefreshToken
|
refresh_token : account.googleRefreshToken
|
||||||
});
|
});
|
||||||
|
|
||||||
oAuth2Client.on('tokens', (tokens)=>{
|
updateTokens && oAuth2Client.on('tokens', (tokens)=>{
|
||||||
if(tokens.refresh_token) {
|
if(tokens.refresh_token) {
|
||||||
account.googleRefreshToken = tokens.refresh_token;
|
account.googleRefreshToken = tokens.refresh_token;
|
||||||
}
|
}
|
||||||
@@ -249,7 +253,6 @@ const GoogleActions = {
|
|||||||
text : file.data,
|
text : file.data,
|
||||||
|
|
||||||
description : obj.data.description,
|
description : obj.data.description,
|
||||||
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
|
||||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||||
authors : [],
|
authors : [],
|
||||||
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
published : obj.data.properties.published ? obj.data.properties.published == 'true' : false,
|
||||||
|
|||||||
@@ -9,13 +9,19 @@ const yaml = require('js-yaml');
|
|||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||||
|
|
||||||
// const getTopBrews = (cb) => {
|
// const getTopBrews = (cb) => {
|
||||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||||
// cb(brews);
|
// cb(brews);
|
||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const getId = (req)=>{
|
const MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
homebrewApi : router,
|
||||||
|
getId : (req)=>{
|
||||||
// Set the id and initial potential google id, where the google id is present on the existing brew.
|
// Set the id and initial potential google id, where the google id is present on the existing brew.
|
||||||
let id = req.params.id, googleId = req.body?.googleId;
|
let id = req.params.id, googleId = req.body?.googleId;
|
||||||
|
|
||||||
@@ -25,13 +31,12 @@ const getId = (req)=>{
|
|||||||
id = id.slice(-12);
|
id = id.slice(-12);
|
||||||
}
|
}
|
||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
};
|
},
|
||||||
|
getBrew : (accessType, stubOnly = false)=>{
|
||||||
const getBrew = (accessType)=>{
|
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
return async (req, res, next)=>{
|
return async (req, res, next)=>{
|
||||||
// Get relevant IDs for the brew
|
// Get relevant IDs for the brew
|
||||||
const { id, googleId } = getId(req);
|
const { id, googleId } = api.getId(req);
|
||||||
|
|
||||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||||
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
||||||
@@ -45,34 +50,43 @@ const getBrew = (accessType)=>{
|
|||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there is a google id, try to find the google brew
|
||||||
if(googleId || stub?.googleId) {
|
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||||
let googleError;
|
let googleError;
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.warn(err);
|
|
||||||
googleError = err;
|
googleError = err;
|
||||||
});
|
});
|
||||||
// If we can't find the google brew and there is a google id for the brew, throw an error.
|
// Throw any error caught while attempting to retrieve Google brew.
|
||||||
if(!googleBrew) throw googleError;
|
if(googleError) throw googleError;
|
||||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||||
stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew;
|
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||||
|
}
|
||||||
|
const authorsExist = stub?.authors?.length > 0;
|
||||||
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||||
|
throw `The current logged in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
if(!stub) {
|
if(!stub && !stubOnly) {
|
||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||||
}
|
}
|
||||||
|
|
||||||
if(typeof stub.tags === 'string') {
|
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||||
stub.tags = [];
|
if(stub) {
|
||||||
|
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||||
|
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||||
|
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||||
}
|
}
|
||||||
req.brew = stub;
|
|
||||||
|
|
||||||
|
req.brew = stub ?? {};
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
};
|
},
|
||||||
|
mergeBrewText : (brew)=>{
|
||||||
const mergeBrewText = (brew)=>{
|
|
||||||
let text = brew.text;
|
let text = brew.text;
|
||||||
if(brew.style !== undefined) {
|
if(brew.style !== undefined) {
|
||||||
text = `\`\`\`css\n` +
|
text = `\`\`\`css\n` +
|
||||||
@@ -86,61 +100,54 @@ const mergeBrewText = (brew)=>{
|
|||||||
`\`\`\`\n\n` +
|
`\`\`\`\n\n` +
|
||||||
`${text}`;
|
`${text}`;
|
||||||
return text;
|
return text;
|
||||||
};
|
},
|
||||||
|
getGoodBrewTitle : (text)=>{
|
||||||
const MAX_TITLE_LENGTH = 100;
|
|
||||||
|
|
||||||
const getGoodBrewTitle = (text)=>{
|
|
||||||
const tokens = Markdown.marked.lexer(text);
|
const tokens = Markdown.marked.lexer(text);
|
||||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||||
.slice(0, MAX_TITLE_LENGTH);
|
.slice(0, MAX_TITLE_LENGTH);
|
||||||
};
|
},
|
||||||
|
excludePropsFromUpdate : (brew)=>{
|
||||||
const excludePropsFromUpdate = (brew)=>{
|
|
||||||
// Remove undesired properties
|
// Remove undesired properties
|
||||||
const modified = _.clone(brew);
|
const modified = _.clone(brew);
|
||||||
const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId'];
|
const propsToExclude = ['_id', 'views', 'lastViewed'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
delete modified[prop];
|
delete modified[prop];
|
||||||
}
|
}
|
||||||
return modified;
|
return modified;
|
||||||
};
|
},
|
||||||
|
excludeGoogleProps : (brew)=>{
|
||||||
const excludeGoogleProps = (brew)=>{
|
|
||||||
const modified = _.clone(brew);
|
const modified = _.clone(brew);
|
||||||
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
delete modified[prop];
|
delete modified[prop];
|
||||||
}
|
}
|
||||||
return modified;
|
return modified;
|
||||||
};
|
},
|
||||||
|
excludeStubProps : (brew)=>{
|
||||||
const excludeStubProps = (brew)=>{
|
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
||||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version'];
|
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
brew[prop] = undefined;
|
brew[prop] = undefined;
|
||||||
}
|
}
|
||||||
return brew;
|
return brew;
|
||||||
};
|
},
|
||||||
|
beforeNewSave : (account, brew)=>{
|
||||||
const beforeNewSave = (account, brew)=>{
|
|
||||||
if(!brew.title) {
|
if(!brew.title) {
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
brew.title = api.getGoodBrewTitle(brew.text);
|
||||||
}
|
}
|
||||||
|
|
||||||
brew.authors = (account) ? [account.username] : [];
|
brew.authors = (account) ? [account.username] : [];
|
||||||
brew.text = mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
};
|
|
||||||
|
|
||||||
const newGoogleBrew = async (account, brew, res)=>{
|
_.defaults(brew, DEFAULT_BREW);
|
||||||
|
},
|
||||||
|
newGoogleBrew : async (account, brew, res)=>{
|
||||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
|
|
||||||
const newBrew = excludeGoogleProps(brew);
|
const newBrew = api.excludeGoogleProps(brew);
|
||||||
|
|
||||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||||
};
|
},
|
||||||
|
newBrew : async (req, res)=>{
|
||||||
const newBrew = async (req, res)=>{
|
|
||||||
const brew = req.body;
|
const brew = req.body;
|
||||||
const { saveToGoogle } = req.query;
|
const { saveToGoogle } = req.query;
|
||||||
|
|
||||||
@@ -148,7 +155,7 @@ const newBrew = async (req, res)=>{
|
|||||||
delete brew.shareId;
|
delete brew.shareId;
|
||||||
delete brew.googleId;
|
delete brew.googleId;
|
||||||
|
|
||||||
beforeNewSave(req.account, brew);
|
api.beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
const newHomebrew = new HomebrewModel(brew);
|
const newHomebrew = new HomebrewModel(brew);
|
||||||
newHomebrew.editId = nanoid(12);
|
newHomebrew.editId = nanoid(12);
|
||||||
@@ -156,13 +163,13 @@ const newBrew = async (req, res)=>{
|
|||||||
|
|
||||||
let googleId, saved;
|
let googleId, saved;
|
||||||
if(saveToGoogle) {
|
if(saveToGoogle) {
|
||||||
googleId = await newGoogleBrew(req.account, newHomebrew, res)
|
googleId = await api.newGoogleBrew(req.account, newHomebrew, res)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
||||||
});
|
});
|
||||||
if(!googleId) return;
|
if(!googleId) return;
|
||||||
excludeStubProps(newHomebrew);
|
api.excludeStubProps(newHomebrew);
|
||||||
newHomebrew.googleId = googleId;
|
newHomebrew.googleId = googleId;
|
||||||
} else {
|
} else {
|
||||||
// Compress brew text to binary before saving
|
// Compress brew text to binary before saving
|
||||||
@@ -180,21 +187,28 @@ const newBrew = async (req, res)=>{
|
|||||||
saved = saved.toObject();
|
saved = saved.toObject();
|
||||||
|
|
||||||
res.status(200).send(saved);
|
res.status(200).send(saved);
|
||||||
};
|
},
|
||||||
|
updateBrew : async (req, res)=>{
|
||||||
|
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||||
|
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
|
const brewFromServer = req.brew;
|
||||||
|
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
||||||
|
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
|
}
|
||||||
|
|
||||||
const updateBrew = async (req, res)=>{
|
let brew = _.assign(brewFromServer, brewFromClient);
|
||||||
// Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
|
|
||||||
let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
|
|
||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
|
||||||
const googleId = brew.googleId;
|
const googleId = brew.googleId;
|
||||||
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
let afterSave = async ()=>true;
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
brew.text = mergeBrewText(brew);
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
if(brew.googleId && removeFromGoogle) {
|
||||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||||
afterSave = async ()=>{
|
afterSave = async ()=>{
|
||||||
return await deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||||
@@ -204,7 +218,7 @@ const updateBrew = async (req, res)=>{
|
|||||||
brew.googleId = undefined;
|
brew.googleId = undefined;
|
||||||
} else if(!brew.googleId && saveToGoogle) {
|
} else if(!brew.googleId && saveToGoogle) {
|
||||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||||
brew.googleId = await newGoogleBrew(req.account, excludeGoogleProps(brew), res)
|
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || err.response.status).send(err.message || err);
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
@@ -212,7 +226,7 @@ const updateBrew = async (req, res)=>{
|
|||||||
if(!brew.googleId) return;
|
if(!brew.googleId) return;
|
||||||
} else if(brew.googleId) {
|
} else if(brew.googleId) {
|
||||||
// If the google id exists and no other actions are being performed, update the google brew
|
// If the google id exists and no other actions are being performed, update the google brew
|
||||||
const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew))
|
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew))
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err?.response?.status || 500).send(err);
|
res.status(err?.response?.status || 500).send(err);
|
||||||
@@ -222,7 +236,7 @@ const updateBrew = async (req, res)=>{
|
|||||||
|
|
||||||
if(brew.googleId) {
|
if(brew.googleId) {
|
||||||
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
||||||
excludeStubProps(brew);
|
api.excludeStubProps(brew);
|
||||||
} else {
|
} else {
|
||||||
// Compress brew text to binary before saving
|
// Compress brew text to binary before saving
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
@@ -230,48 +244,46 @@ const updateBrew = async (req, res)=>{
|
|||||||
brew.text = undefined;
|
brew.text = undefined;
|
||||||
}
|
}
|
||||||
brew.updatedAt = new Date();
|
brew.updatedAt = new Date();
|
||||||
|
brew.version = (brew.version || 1) + 1;
|
||||||
|
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||||
|
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it
|
// define a function to catch our save errors
|
||||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
const saveError = (err)=>{
|
||||||
|
|
||||||
if(!brew.markModified) {
|
|
||||||
// If it wasn't in the database, create a new db brew
|
|
||||||
brew = new HomebrewModel(brew);
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.markModified('authors');
|
|
||||||
brew.markModified('systems');
|
|
||||||
|
|
||||||
// Save the database brew
|
|
||||||
const saved = await brew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||||
});
|
};
|
||||||
|
let saved;
|
||||||
|
if(!brew._id) {
|
||||||
|
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
|
||||||
|
saved = await new HomebrewModel(brew).save().catch(saveError);
|
||||||
|
} else {
|
||||||
|
// if the brew does have a stub id, update it using the stub id as the key.
|
||||||
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
saved = await brew.save()
|
||||||
|
.catch(saveError);
|
||||||
|
}
|
||||||
if(!saved) return;
|
if(!saved) return;
|
||||||
// Call and wait for afterSave to complete
|
// Call and wait for afterSave to complete
|
||||||
const after = await afterSave();
|
const after = await afterSave();
|
||||||
if(!after) return;
|
if(!after) return;
|
||||||
|
|
||||||
res.status(200).send(saved);
|
res.status(200).send(saved);
|
||||||
};
|
},
|
||||||
|
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||||
const deleteGoogleBrew = async (account, id, editId, res)=>{
|
|
||||||
const auth = await GoogleActions.authCheck(account, res);
|
const auth = await GoogleActions.authCheck(account, res);
|
||||||
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
||||||
return true;
|
return true;
|
||||||
};
|
},
|
||||||
|
deleteBrew : async (req, res, next)=>{
|
||||||
const deleteBrew = async (req, res, next)=>{
|
|
||||||
// Delete an orphaned stub if its Google brew doesn't exist
|
// Delete an orphaned stub if its Google brew doesn't exist
|
||||||
try {
|
try {
|
||||||
await getBrew('edit')(req, res, ()=>{});
|
await api.getBrew('edit')(req, res, ()=>{});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const { id, googleId } = getId(req);
|
const { id, googleId } = api.getId(req);
|
||||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||||
await HomebrewModel.deleteOne({ editId: id });
|
await HomebrewModel.deleteOne({ editId: id });
|
||||||
return next();
|
return next();
|
||||||
@@ -289,7 +301,6 @@ const deleteBrew = async (req, res, next)=>{
|
|||||||
if(account) {
|
if(account) {
|
||||||
// Remove current user as author
|
// Remove current user as author
|
||||||
brew.authors = _.pull(brew.authors, account.username);
|
brew.authors = _.pull(brew.authors, account.username);
|
||||||
brew.markModified('authors');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(brew.authors.length === 0) {
|
if(brew.authors.length === 0) {
|
||||||
@@ -315,7 +326,7 @@ const deleteBrew = async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(shouldDeleteGoogleBrew) {
|
if(shouldDeleteGoogleBrew) {
|
||||||
const deleted = await deleteGoogleBrew(account, googleId, editId, res)
|
const deleted = await api.deleteGoogleBrew(account, googleId, editId, res)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).send(err);
|
res.status(500).send(err);
|
||||||
@@ -324,15 +335,14 @@ const deleteBrew = async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', asyncHandler(newBrew));
|
router.use('/api', require('./middleware/check-client-version.js'));
|
||||||
router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.post('/api', asyncHandler(api.newBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(deleteBrew));
|
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||||
|
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||||
|
|
||||||
module.exports = {
|
module.exports = api;
|
||||||
homebrewApi : router,
|
|
||||||
getBrew
|
|
||||||
};
|
|
||||||
|
|||||||
748
server/homebrew.api.spec.js
Normal file
748
server/homebrew.api.spec.js
Normal file
@@ -0,0 +1,748 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
describe('Tests for api', ()=>{
|
||||||
|
let api;
|
||||||
|
let google;
|
||||||
|
let model;
|
||||||
|
let hbBrew;
|
||||||
|
let googleBrew;
|
||||||
|
let res;
|
||||||
|
|
||||||
|
let modelBrew;
|
||||||
|
let saveFunc;
|
||||||
|
let removeFunc;
|
||||||
|
let saved;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
saved = undefined;
|
||||||
|
saveFunc = jest.fn(async function() {
|
||||||
|
saved = { ...this, _id: '1' };
|
||||||
|
return saved;
|
||||||
|
});
|
||||||
|
removeFunc = jest.fn(async function() {});
|
||||||
|
|
||||||
|
modelBrew = (brew)=>({
|
||||||
|
...brew,
|
||||||
|
save : saveFunc,
|
||||||
|
remove : removeFunc,
|
||||||
|
toObject : function() {
|
||||||
|
delete this.save;
|
||||||
|
delete this.toObject;
|
||||||
|
delete this.remove;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
google = require('./googleActions.js');
|
||||||
|
model = require('./homebrew.model.js').model;
|
||||||
|
|
||||||
|
jest.mock('./googleActions.js');
|
||||||
|
google.authCheck = jest.fn(()=>'client');
|
||||||
|
google.newGoogleBrew = jest.fn(()=>'id');
|
||||||
|
google.deleteGoogleBrew = jest.fn(()=>true);
|
||||||
|
|
||||||
|
jest.mock('./homebrew.model.js');
|
||||||
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
|
res = {
|
||||||
|
status : jest.fn(()=>res),
|
||||||
|
send : jest.fn(()=>{})
|
||||||
|
};
|
||||||
|
|
||||||
|
api = require('./homebrew.api');
|
||||||
|
|
||||||
|
hbBrew = {
|
||||||
|
text : `brew text`,
|
||||||
|
style : 'hello yes i am css',
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
published : true,
|
||||||
|
authors : ['1', '2'],
|
||||||
|
owner : '1',
|
||||||
|
thumbnail : '',
|
||||||
|
_id : 'mongoid',
|
||||||
|
editId : 'abcdefg',
|
||||||
|
shareId : 'hijklmnop',
|
||||||
|
views : 1,
|
||||||
|
lastViewed : new Date(),
|
||||||
|
version : 1,
|
||||||
|
pageCount : 1,
|
||||||
|
textBin : ''
|
||||||
|
};
|
||||||
|
googleBrew = {
|
||||||
|
...hbBrew,
|
||||||
|
googleId : '12345'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getId', ()=>{
|
||||||
|
it('should return only id if google id is not present', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefgh'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefgh');
|
||||||
|
expect(googleId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return id and google id from request body', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefgh'
|
||||||
|
},
|
||||||
|
body : {
|
||||||
|
googleId : '12345'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefgh');
|
||||||
|
expect(googleId).toEqual('12345');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return id and google id from params', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : '123456789012abcdefghijkl'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefghijkl');
|
||||||
|
expect(googleId).toEqual('123456789012');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBrew', ()=>{
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
|
||||||
|
|
||||||
|
it('returns middleware', ()=>{
|
||||||
|
const getFn = api.getBrew('share');
|
||||||
|
expect(getFn).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch from mongoose', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', authors: [] };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual(testBrew);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mongoose error', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>new Promise((_, rej)=>rej('Unable to find brew')));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, next);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual(notFoundError);
|
||||||
|
expect(req.brew).toEqual({});
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes tags from string to array', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', authors: [], tags: '' };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew.tags).toEqual([]);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if invalid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, null);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw if no authors', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: [] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(req.brew.title).toEqual('test brew');
|
||||||
|
expect(req.brew.authors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw if valid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {}, account: { username: 'a' } };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(req.brew.title).toEqual('test brew');
|
||||||
|
expect(req.brew.authors).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches google brew if needed', async()=>{
|
||||||
|
const stubBrew = { title: 'test brew', authors: ['a'] };
|
||||||
|
const googleBrew = { title: 'test google brew', text: 'brew text' };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(stubBrew));
|
||||||
|
google.getGoogleBrew = jest.fn(()=>new Promise((res)=>res(googleBrew)));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual({
|
||||||
|
title : 'test google brew',
|
||||||
|
authors : ['a'],
|
||||||
|
text : 'brew text',
|
||||||
|
stubbed : true,
|
||||||
|
description : '',
|
||||||
|
editId : undefined,
|
||||||
|
pageCount : 1,
|
||||||
|
published : false,
|
||||||
|
renderer : 'legacy',
|
||||||
|
shareId : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
textBin : undefined,
|
||||||
|
version : undefined,
|
||||||
|
createdAt : undefined,
|
||||||
|
gDrive : false,
|
||||||
|
style : undefined,
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined
|
||||||
|
});
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mergeBrewText', ()=>{
|
||||||
|
it('should set metadata and no style if it is not present', ()=>{
|
||||||
|
const result = api.mergeBrewText({
|
||||||
|
text : `brew`,
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
googleId : '12345'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(`\`\`\`metadata
|
||||||
|
title: some title
|
||||||
|
description: this is a description
|
||||||
|
tags:
|
||||||
|
- something
|
||||||
|
- fun
|
||||||
|
systems:
|
||||||
|
- D&D 5e
|
||||||
|
renderer: v3
|
||||||
|
theme: phb
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
brew`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set metadata and style', ()=>{
|
||||||
|
const result = api.mergeBrewText({
|
||||||
|
text : `brew`,
|
||||||
|
style : 'hello yes i am css',
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
googleId : '12345'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(`\`\`\`metadata
|
||||||
|
title: some title
|
||||||
|
description: this is a description
|
||||||
|
tags:
|
||||||
|
- something
|
||||||
|
- fun
|
||||||
|
systems:
|
||||||
|
- D&D 5e
|
||||||
|
renderer: v3
|
||||||
|
theme: phb
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
\`\`\`css
|
||||||
|
hello yes i am css
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
brew`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exclusion methods', ()=>{
|
||||||
|
it('excludePropsFromUpdate removes the correct keys', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludePropsFromUpdate(sent);
|
||||||
|
|
||||||
|
expect(sent).toEqual(googleBrew);
|
||||||
|
expect(result._id).toBeUndefined();
|
||||||
|
expect(result.views).toBeUndefined();
|
||||||
|
expect(result.lastViewed).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludeGoogleProps removes the correct keys', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludeGoogleProps(sent);
|
||||||
|
|
||||||
|
expect(sent).toEqual(googleBrew);
|
||||||
|
expect(result.tags).toBeUndefined();
|
||||||
|
expect(result.systems).toBeUndefined();
|
||||||
|
expect(result.published).toBeUndefined();
|
||||||
|
expect(result.authors).toBeUndefined();
|
||||||
|
expect(result.owner).toBeUndefined();
|
||||||
|
expect(result.views).toBeUndefined();
|
||||||
|
expect(result.thumbnail).toBeUndefined();
|
||||||
|
expect(result.version).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludeStubProps removes the correct keys from the original object', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludeStubProps(sent);
|
||||||
|
|
||||||
|
expect(sent).not.toEqual(googleBrew);
|
||||||
|
expect(result.text).toBeUndefined();
|
||||||
|
expect(result.textBin).toBeUndefined();
|
||||||
|
expect(result.renderer).toBeUndefined();
|
||||||
|
expect(result.pageCount).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('beforeNewSave', ()=>{
|
||||||
|
it('sets the title if none', ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
title : undefined
|
||||||
|
};
|
||||||
|
api.beforeNewSave({}, brew);
|
||||||
|
|
||||||
|
expect(brew.title).toEqual('brew text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not override the title if present', ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
title : 'test'
|
||||||
|
};
|
||||||
|
api.beforeNewSave({}, brew);
|
||||||
|
|
||||||
|
expect(brew.title).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set authors if account missing username', ()=>{
|
||||||
|
api.beforeNewSave({}, hbBrew);
|
||||||
|
|
||||||
|
expect(hbBrew.authors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets authors if account has username', ()=>{
|
||||||
|
api.beforeNewSave({ username: 'hi' }, hbBrew);
|
||||||
|
|
||||||
|
expect(hbBrew.authors).toEqual(['hi']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges brew text', ()=>{
|
||||||
|
api.mergeBrewText = jest.fn(()=>'merged');
|
||||||
|
api.beforeNewSave({}, hbBrew);
|
||||||
|
|
||||||
|
expect(api.mergeBrewText).toHaveBeenCalled();
|
||||||
|
expect(hbBrew.text).toEqual('merged');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newGoogleBrew', ()=>{
|
||||||
|
it('should call the correct methods', ()=>{
|
||||||
|
api.excludeGoogleProps = jest.fn(()=>'newBrew');
|
||||||
|
|
||||||
|
const acct = { username: 'test' };
|
||||||
|
const brew = { title: 'test title' };
|
||||||
|
api.newGoogleBrew(acct, brew, res);
|
||||||
|
|
||||||
|
expect(google.authCheck).toHaveBeenCalledWith(acct, res);
|
||||||
|
expect(api.excludeGoogleProps).toHaveBeenCalledWith(brew);
|
||||||
|
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newBrew', ()=>{
|
||||||
|
it('should set up a default brew via Homebrew model', async ()=>{
|
||||||
|
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
_id : '1',
|
||||||
|
authors : ['test user'],
|
||||||
|
createdAt : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : expect.any(String),
|
||||||
|
gDrive : false,
|
||||||
|
pageCount : 1,
|
||||||
|
published : false,
|
||||||
|
renderer : 'V3',
|
||||||
|
shareId : expect.any(String),
|
||||||
|
style : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
text : undefined,
|
||||||
|
textBin : expect.objectContaining({}),
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
title : 'asdf',
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove edit/share/google ids', async ()=>{
|
||||||
|
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalled();
|
||||||
|
const sent = res.send.mock.calls[0][0];
|
||||||
|
expect(sent.editId).not.toEqual('1234');
|
||||||
|
expect(sent.shareId).not.toEqual('1234');
|
||||||
|
expect(sent.googleId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mongo error', async ()=>{
|
||||||
|
saveFunc = jest.fn(async function() {
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(res.send).not.toHaveBeenCalled();
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save to google if requested', async()=>{
|
||||||
|
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(google.newGoogleBrew).toHaveBeenCalled();
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
_id : '1',
|
||||||
|
authors : ['test user'],
|
||||||
|
createdAt : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : expect.any(String),
|
||||||
|
gDrive : false,
|
||||||
|
pageCount : undefined,
|
||||||
|
published : false,
|
||||||
|
renderer : undefined,
|
||||||
|
shareId : expect.any(String),
|
||||||
|
googleId : expect.any(String),
|
||||||
|
style : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
text : undefined,
|
||||||
|
textBin : undefined,
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
title : 'asdf',
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle google error', async()=>{
|
||||||
|
google.newGoogleBrew = jest.fn(()=>{
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('err');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteGoogleBrew', ()=>{
|
||||||
|
it('should check auth and delete brew', async ()=>{
|
||||||
|
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
|
||||||
|
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteBrew', ()=>{
|
||||||
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
|
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
const next = jest.fn(()=>{});
|
||||||
|
|
||||||
|
await api.deleteBrew(null, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalledWith({ editId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete if no authors', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : []
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
const req = {};
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on delete error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : []
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
removeFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||||
|
const req = {};
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete when one author', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove one author when multiple present', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle save error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
saveFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete google brew', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle google brew delete error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>{
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete google brew and retain stub when multiple authors and owner request deletion', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test2']);
|
||||||
|
expect(saved.googleId).toEqual(undefined);
|
||||||
|
expect(saved.text).toEqual(undefined);
|
||||||
|
expect(saved.textBin).not.toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test2' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test']);
|
||||||
|
expect(saved.googleId).toEqual(brew.googleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -17,6 +17,7 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
systems : [String],
|
systems : [String],
|
||||||
renderer : { type: String, default: '' },
|
renderer : { type: String, default: '' },
|
||||||
authors : [String],
|
authors : [String],
|
||||||
|
invitedAuthors : [String],
|
||||||
published : { type: Boolean, default: false },
|
published : { type: Boolean, default: false },
|
||||||
thumbnail : { type: String, default: '' },
|
thumbnail : { type: String, default: '' },
|
||||||
|
|
||||||
@@ -46,8 +47,6 @@ HomebrewSchema.statics.get = function(query, fields=null){
|
|||||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||||
brews[0].text = unzipped.toString();
|
brews[0].text = unzipped.toString();
|
||||||
}
|
}
|
||||||
if(!brews[0].renderer)
|
|
||||||
brews[0].renderer = 'legacy';
|
|
||||||
return resolve(brews[0]);
|
return resolve(brews[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
12
server/middleware/check-client-version.js
Normal file
12
server/middleware/check-client-version.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = (req, res, next)=>{
|
||||||
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = require('../../package.json').version;
|
||||||
|
|
||||||
|
if(userVersion != version) {
|
||||||
|
return res.status(412).send({
|
||||||
|
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -32,7 +32,7 @@ const mustacheSpans = {
|
|||||||
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/{{[^{]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
const completeSpan = /^{{[^\n]*}}/; // Regex for the complete token
|
||||||
const inlineRegex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
|
const inlineRegex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
|
||||||
const match = completeSpan.exec(src);
|
const match = completeSpan.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -82,7 +82,7 @@ const mustacheDivs = {
|
|||||||
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{{[^{]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
const completeBlock = /^ *{{[^\n}]* *\n.*\n *}}/s; // Regex for the complete token
|
||||||
const blockRegex = /^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/gm;
|
const blockRegex = /^ *{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *$|^ *}}$/gm;
|
||||||
const match = completeBlock.exec(src);
|
const match = completeBlock.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
//Find closing delimiter
|
//Find closing delimiter
|
||||||
@@ -130,7 +130,7 @@ const mustacheInjectInline = {
|
|||||||
level : 'inline',
|
level : 'inline',
|
||||||
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/ *{[^{\n]/)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/g;
|
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
@@ -165,7 +165,7 @@ const mustacheInjectBlock = {
|
|||||||
level : 'block',
|
level : 'block',
|
||||||
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
start(src) { return src.match(/\n *{[^{\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match
|
||||||
tokenizer(src, tokens) {
|
tokenizer(src, tokens) {
|
||||||
const inlineRegex = /^ *{((?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*)}/ym;
|
const inlineRegex = /^ *{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/ym;
|
||||||
const match = inlineRegex.exec(src);
|
const match = inlineRegex.exec(src);
|
||||||
if(match) {
|
if(match) {
|
||||||
const lastToken = tokens[tokens.length - 1];
|
const lastToken = tokens[tokens.length - 1];
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ describe('Tests for static pages', ()=>{
|
|||||||
return app.get('/').expect(200);
|
return app.get('/').expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Home page v3 works', ()=>{
|
it('Home page legacy works', ()=>{
|
||||||
return app.get('/v3_preview').expect(200);
|
return app.get('/legacy').expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Changelog page works', ()=>{
|
it('Changelog page works', ()=>{
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
|
contain : size;
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
@@ -77,9 +77,12 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|
||||||
|
.page{
|
||||||
p{
|
p{
|
||||||
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
|
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
|
||||||
display : block;
|
display : block;
|
||||||
@@ -175,12 +178,24 @@ body {
|
|||||||
font-size : 0.575cm;
|
font-size : 0.575cm;
|
||||||
border-bottom : 2px solid var(--HB_Color_HeaderUnderline);;
|
border-bottom : 2px solid var(--HB_Color_HeaderUnderline);;
|
||||||
line-height : 0.995em; //Font is misaligned. Shift up slightly
|
line-height : 0.995em; //Font is misaligned. Shift up slightly
|
||||||
|
& + * {
|
||||||
|
margin-top: 0.17cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
* + h3 {
|
||||||
|
margin-top : 0.155cm; //(0.325 - 0.17)
|
||||||
}
|
}
|
||||||
h4{
|
h4{
|
||||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||||
//margin-bottom : 0.02cm;
|
//margin-bottom : 0.02cm;
|
||||||
font-size : 0.458cm;
|
font-size : 0.458cm;
|
||||||
line-height : 0.971em; //Font is misaligned. Shift up slightly
|
line-height : 0.971em; //Font is misaligned. Shift up slightly
|
||||||
|
& + * {
|
||||||
|
margin-top: 0.09cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
* + h4 {
|
||||||
|
margin-top : 0.235cm; //(0.325 - 0.09)
|
||||||
}
|
}
|
||||||
h5{
|
h5{
|
||||||
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
//margin-top : -0.02cm; //Font is misaligned. Shift up slightly
|
||||||
@@ -199,6 +214,7 @@ body {
|
|||||||
table{
|
table{
|
||||||
.useSansSerif();
|
.useSansSerif();
|
||||||
width : 100%;
|
width : 100%;
|
||||||
|
line-height : 16px;
|
||||||
& + * {
|
& + * {
|
||||||
margin-top : 0.325cm;
|
margin-top : 0.325cm;
|
||||||
}
|
}
|
||||||
@@ -207,15 +223,17 @@ body {
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
th{
|
th{
|
||||||
vertical-align : bottom;
|
vertical-align : bottom;
|
||||||
padding : 0.14em 0.4em;
|
//padding : 0.14em 0.4em;
|
||||||
|
padding : 0px 1.5px; // Both of these are temporary, just to force
|
||||||
|
//line-height : 16px; // PDF to render at same height until Chrome 108
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tbody{
|
tbody{
|
||||||
tr{
|
tr{
|
||||||
td{
|
td{
|
||||||
//padding : 0.14em 0.4em;
|
//padding : 0.14em 0.4em;
|
||||||
padding : 1.7px 5px; // Both of these are temporary, just to force
|
padding : 0px 1.5px; // Both of these are temporary, just to force
|
||||||
height : 18px; // PDF to render at same height until Chrome 108
|
//line-height : 16px; // PDF to render at same height until Chrome 108
|
||||||
}
|
}
|
||||||
&:nth-child(odd){
|
&:nth-child(odd){
|
||||||
background-color : var(--HB_Color_Accent);
|
background-color : var(--HB_Color_Accent);
|
||||||
@@ -627,6 +645,9 @@ body {
|
|||||||
&.wide:first-child {
|
&.wide:first-child {
|
||||||
margin-top: 0.12cm;
|
margin-top: 0.12cm;
|
||||||
}
|
}
|
||||||
|
& + * {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.decoration {
|
&.decoration {
|
||||||
position:relative;
|
position:relative;
|
||||||
@@ -733,24 +754,6 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//*****************************
|
|
||||||
// * MUSTACHE DIVS/SPANS
|
|
||||||
// *****************************/
|
|
||||||
.page {
|
|
||||||
.block {
|
|
||||||
break-inside : avoid;
|
|
||||||
display : inline-block;
|
|
||||||
.page :where(&) {
|
|
||||||
width : 100%;
|
|
||||||
}
|
|
||||||
//-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns
|
|
||||||
}
|
|
||||||
.inline-block {
|
|
||||||
display : inline-block;
|
|
||||||
text-indent : initial;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * DEFINITION LISTS
|
// * DEFINITION LISTS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
@@ -781,26 +784,13 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//*****************************
|
|
||||||
// * BLANK LINE
|
|
||||||
// *****************************/
|
|
||||||
.page {
|
|
||||||
.blank {
|
|
||||||
height : 1em;
|
|
||||||
margin-top : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * WIDE
|
// * WIDE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page .wide{
|
.page .wide{
|
||||||
column-span : all;
|
margin-bottom : 0.325cm;
|
||||||
-webkit-column-span : all;
|
}
|
||||||
-moz-column-span : all;
|
|
||||||
display : block;
|
.page h1 + *{
|
||||||
margin-bottom : 0.34cm;
|
|
||||||
&+* {
|
|
||||||
margin-top : 0;
|
margin-top : 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,11 +39,12 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
|
contain : size;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
:where(.page){
|
.page{
|
||||||
p{
|
p{
|
||||||
overflow-wrap : break-word;
|
overflow-wrap : break-word;
|
||||||
display : block;
|
display : block;
|
||||||
@@ -77,13 +78,7 @@ body {
|
|||||||
img{
|
img{
|
||||||
z-index : -1;
|
z-index : -1;
|
||||||
}
|
}
|
||||||
:not(:where(.wide,.columnSplit,.blank,hr)) + :where(h1,h2,h3,h4,h5,h6,table,dl,.block) {
|
|
||||||
margin-top : 1em; //NOTE: MAKE ALL MARGINS TOP-ONLY FOR BEST RESULTS WITH COLUMN BREAKS. USE * + * STYLE SELECTORS
|
|
||||||
}
|
|
||||||
|
|
||||||
:where(h1,h3,h3,h4,h5,h6) + * {
|
|
||||||
margin-top : 0;
|
|
||||||
}
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * HEADERS
|
// * HEADERS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
@@ -116,6 +111,9 @@ body {
|
|||||||
font-weight : bold;
|
font-weight : bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div:not(.columnWrapper) > table + table { // Side-by-side tables should not
|
||||||
|
margin-top : 0; // have vertical spacing.
|
||||||
|
}
|
||||||
|
|
||||||
/* Watermark */
|
/* Watermark */
|
||||||
.watermark {
|
.watermark {
|
||||||
@@ -191,6 +189,10 @@ 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;
|
||||||
|
margin-top : 0;
|
||||||
|
& + * {
|
||||||
|
margin-top : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//Avoid breaking up
|
//Avoid breaking up
|
||||||
blockquote,table{
|
blockquote,table{
|
||||||
@@ -214,14 +216,12 @@ body {
|
|||||||
//*****************************
|
//*****************************
|
||||||
// * MUSTACHE DIVS/SPANS
|
// * MUSTACHE DIVS/SPANS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
:where(.page) {
|
.page {
|
||||||
.block {
|
.block {
|
||||||
break-inside : avoid;
|
break-inside : avoid;
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
.page :where(&) {
|
|
||||||
width : 100%;
|
width : 100%;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.inline-block {
|
.inline-block {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
text-indent : initial;
|
text-indent : initial;
|
||||||
@@ -231,7 +231,7 @@ body {
|
|||||||
//*****************************
|
//*****************************
|
||||||
// * DEFINITION LISTS
|
// * DEFINITION LISTS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
:where(.page) {
|
.page {
|
||||||
dl {
|
dl {
|
||||||
padding-left : 1em;
|
padding-left : 1em;
|
||||||
white-space : pre-line;
|
white-space : pre-line;
|
||||||
@@ -251,17 +251,20 @@ body {
|
|||||||
//*****************************
|
//*****************************
|
||||||
// * BLANK LINE
|
// * BLANK LINE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
:where(.page) {
|
.page {
|
||||||
.blank {
|
.blank {
|
||||||
height : 1em;
|
height : 1em;
|
||||||
margin-top : 0;
|
margin-top : 0;
|
||||||
|
& + * {
|
||||||
|
margin-top : 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * WIDE
|
// * WIDE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
:where(.page) {
|
.page {
|
||||||
.wide{
|
.wide{
|
||||||
column-span : all;
|
column-span : all;
|
||||||
display : block;
|
display : block;
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
:where(.page){
|
.page{
|
||||||
color : var(--HB_Color_Text);
|
color : var(--HB_Color_Text);
|
||||||
font-family : ReenieBeanie;
|
font-family : ReenieBeanie;
|
||||||
font-size : 0.53cm;
|
font-size : 0.53cm;
|
||||||
@@ -554,6 +554,6 @@
|
|||||||
//*****************************
|
//*****************************
|
||||||
// * WIDE
|
// * WIDE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
:where(.page) .wide {
|
.page .wide {
|
||||||
margin-bottom : 0.45cm;
|
margin-bottom : 0.45cm;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user