mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 18:33:01 +00:00
Compare commits
321 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c85d0f35a | ||
|
|
331fcf0714 | ||
|
|
0ec9e8932e | ||
|
|
67eb7fdbd4 | ||
|
|
a1e7da8d84 | ||
|
|
f53b0ec9af | ||
|
|
57d0e8eea3 | ||
|
|
7403ef60c1 | ||
|
|
42ee461f56 | ||
|
|
77e8952e8a | ||
|
|
2be365c839 | ||
|
|
c0c08b3354 | ||
|
|
b0d8462a60 | ||
|
|
f246c96b28 | ||
|
|
aa7b3d985f | ||
|
|
5ed6e9842c | ||
|
|
eaf8b02aa9 | ||
|
|
7aa0aed7c9 | ||
|
|
562ba42b1b | ||
|
|
c081234021 | ||
|
|
bba0208361 | ||
|
|
b19efcebb9 | ||
|
|
4d6ce6b917 | ||
|
|
92d8027640 | ||
|
|
c9935fa45c | ||
|
|
a140deae54 | ||
|
|
9e5cc57441 | ||
|
|
8bab346cbb | ||
|
|
0a52cafefe | ||
|
|
83b3fdff21 | ||
|
|
97dfbe9e35 | ||
|
|
e4e6b5426e | ||
|
|
58815a3910 | ||
|
|
b17f173e03 | ||
|
|
9535fea964 | ||
|
|
4134e43f6e | ||
|
|
274b3bcb7e | ||
|
|
8800397ba4 | ||
|
|
0a90218d2a | ||
|
|
06598e0665 | ||
|
|
03e5d86b73 | ||
|
|
8533240407 | ||
|
|
2753005386 | ||
|
|
9178d061ff | ||
|
|
934c77cee9 | ||
|
|
2ec2239124 | ||
|
|
257262e3cc | ||
|
|
e85975308f | ||
|
|
323ccf3b25 | ||
|
|
be892516d5 | ||
|
|
a6e956472f | ||
|
|
392ce35efa | ||
|
|
ef35991a8c | ||
|
|
1c2992c887 | ||
|
|
1eaeebf2fe | ||
|
|
ffc027a309 | ||
|
|
6821d84f9b | ||
|
|
dcb25d8a40 | ||
|
|
2f20eeb016 | ||
|
|
98e40e2b49 | ||
|
|
2d10394690 | ||
|
|
ebe76aacf3 | ||
|
|
b144f0c1d7 | ||
|
|
8ec8b2c66d | ||
|
|
5bb5af2b5e | ||
|
|
2229686057 | ||
|
|
1a419f7e28 | ||
|
|
b8973d63c0 | ||
|
|
b1932dc8e4 | ||
|
|
de54bd4817 | ||
|
|
424bc9fa6e | ||
|
|
9282bdc09d | ||
|
|
2cb34c6535 | ||
|
|
5329f21896 | ||
|
|
6d73f2eb9f | ||
|
|
52a777aae6 | ||
|
|
e44bbae07a | ||
|
|
f8abca6053 | ||
|
|
156e697042 | ||
|
|
39d338e5bf | ||
|
|
8fb25646bd | ||
|
|
04cd53397a | ||
|
|
c16588578b | ||
|
|
fc000af68c | ||
|
|
b3414b23ce | ||
|
|
3143c4e51c | ||
|
|
bf7d43768b | ||
|
|
ea5a96f87f | ||
|
|
50f7dec026 | ||
|
|
98de9f1d7f | ||
|
|
4dc3d5dcf7 | ||
|
|
49566756cd | ||
|
|
5de89949b3 | ||
|
|
7b49f66ab7 | ||
|
|
412193f1d7 | ||
|
|
4c52c1b188 | ||
|
|
66152c52ca | ||
|
|
ca7b758dd4 | ||
|
|
8ea2780a44 | ||
|
|
a679c615ed | ||
|
|
7cc7bd4786 | ||
|
|
99761f0a93 | ||
|
|
b87f57cd25 | ||
|
|
14ff9aeae5 | ||
|
|
6ce37db3dc | ||
|
|
90dcbdfd02 | ||
|
|
ba7976c5c6 | ||
|
|
6520d3fd76 | ||
|
|
aafe9724d4 | ||
|
|
72207f9222 | ||
|
|
f1be8c88f2 | ||
|
|
927345b131 | ||
|
|
bb68421474 | ||
|
|
7699e1e79a | ||
|
|
59d08a7414 | ||
|
|
41c2d2a3d7 | ||
|
|
0c0be58e65 | ||
|
|
42afbd3e70 | ||
|
|
da9c0712a8 | ||
|
|
143d0f294a | ||
|
|
e197ab7bc3 | ||
|
|
d3bb075c47 | ||
|
|
afeb797c78 | ||
|
|
9ab14a9fd8 | ||
|
|
55a5546f25 | ||
|
|
938f0a028b | ||
|
|
c2b9a19c12 | ||
|
|
29c32f03ae | ||
|
|
950e03e321 | ||
|
|
a1876f16da | ||
|
|
c4b0dd5aa6 | ||
|
|
fa60258edc | ||
|
|
6846d5c6f0 | ||
|
|
30867960ce | ||
|
|
ee201ae6d8 | ||
|
|
6e5b4ca6e0 | ||
|
|
cbc3c36dc3 | ||
|
|
9675b1cf0b | ||
|
|
31967428ca | ||
|
|
9f60fe49ab | ||
|
|
d51340649b | ||
|
|
6907ec3a2e | ||
|
|
30b1aef8ba | ||
|
|
e1bbd76208 | ||
|
|
d38bf3b450 | ||
|
|
63e1849854 | ||
|
|
0b7fee0cc5 | ||
|
|
402301f201 | ||
|
|
c41141fe10 | ||
|
|
eccf5e15b1 | ||
|
|
6299e87569 | ||
|
|
0611db1bdf | ||
|
|
660004e348 | ||
|
|
ac5ce90eba | ||
|
|
0a41e7a4af | ||
|
|
5170b991b4 | ||
|
|
cd27933f98 | ||
|
|
6985f69caa | ||
|
|
aaf36a29a7 | ||
|
|
790420b320 | ||
|
|
2f011ebb24 | ||
|
|
191adf0a7c | ||
|
|
2ab95d908b | ||
|
|
62f505f982 | ||
|
|
eaafce0517 | ||
|
|
10f529c6b6 | ||
|
|
8424e51592 | ||
|
|
45e391b273 | ||
|
|
78e042cb9a | ||
|
|
0c41fdee6f | ||
|
|
11bbf1b8fa | ||
|
|
f89f686097 | ||
|
|
d93e4c7458 | ||
|
|
6bf4fc6cf8 | ||
|
|
35a8f7dd98 | ||
|
|
d0ec8ba22f | ||
|
|
57d3db5322 | ||
|
|
e43ee7ddba | ||
|
|
7a7bffab24 | ||
|
|
1001e57249 | ||
|
|
ccbeca2cad | ||
|
|
d7aa4afa60 | ||
|
|
e4c2ce6a8c | ||
|
|
de115c5113 | ||
|
|
4e0ab4b393 | ||
|
|
050bc472d0 | ||
|
|
48da1da5ee | ||
|
|
1b5f408bef | ||
|
|
b1869a33f9 | ||
|
|
bb07cdaa9f | ||
|
|
db0e4fcc0c | ||
|
|
ba8a2af87d | ||
|
|
85e7071d6c | ||
|
|
ece6df023a | ||
|
|
4c08f4a6e1 | ||
|
|
039e4dd4e5 | ||
|
|
041abf1220 | ||
|
|
56fc23f23a | ||
|
|
87c28c76f3 | ||
|
|
235f878dba | ||
|
|
b2ec0d4a0c | ||
|
|
02560d82ab | ||
|
|
22b80ffbb2 | ||
|
|
2db127d805 | ||
|
|
c2ca9f8f10 | ||
|
|
e614fbc5a1 | ||
|
|
7f001ee391 | ||
|
|
9432304be5 | ||
|
|
38c0527d35 | ||
|
|
6fc176e616 | ||
|
|
588bcebc87 | ||
|
|
4fd085b684 | ||
|
|
d2250cdabb | ||
|
|
179d5e6312 | ||
|
|
12d0f69e9c | ||
|
|
788ff65283 | ||
|
|
d7d93c8975 | ||
|
|
eb07fd7c38 | ||
|
|
c09b87482a | ||
|
|
19562a2445 | ||
|
|
543d65f43f | ||
|
|
fc1af353f3 | ||
|
|
9c57450330 | ||
|
|
0573084ffd | ||
|
|
6cb39709c4 | ||
|
|
4ea2fc34f0 | ||
|
|
a0e2bcb8e4 | ||
|
|
015644453b | ||
|
|
199c7d4e02 | ||
|
|
1d71e96421 | ||
|
|
8d0dbac882 | ||
|
|
6c1b4b1839 | ||
|
|
d4a4e7d139 | ||
|
|
0dfe18cd18 | ||
|
|
8895b44be9 | ||
|
|
279352377b | ||
|
|
51cf363c84 | ||
|
|
bdd554851d | ||
|
|
f611a36089 | ||
|
|
0861e1ed29 | ||
|
|
4070c53112 | ||
|
|
ac8ad98939 | ||
|
|
e315c29620 | ||
|
|
e84cd4fe8b | ||
|
|
2d85638d7d | ||
|
|
1daa700a1a | ||
|
|
c06176b3bf | ||
|
|
85f93c7861 | ||
|
|
e1457b5308 | ||
|
|
fc6fd00fe9 | ||
|
|
3ccc36f87a | ||
|
|
ceae540aa0 | ||
|
|
a5cab7005e | ||
|
|
e48e8cd05b | ||
|
|
e74800916e | ||
|
|
34f154d09d | ||
|
|
1bcdd6bc38 | ||
|
|
dd82ee68f0 | ||
|
|
564486f6d0 | ||
|
|
bf632a8584 | ||
|
|
506cf78dac | ||
|
|
0dbb5f18ba | ||
|
|
85e9c57ee2 | ||
|
|
257c266a2e | ||
|
|
605ea2aa62 | ||
|
|
896d9ae2c7 | ||
|
|
0beabc6c0c | ||
|
|
834a4c13a7 | ||
|
|
eca12aae82 | ||
|
|
00158c1894 | ||
|
|
77f5e3e835 | ||
|
|
48a5c12ab7 | ||
|
|
28793e06fc | ||
|
|
ba74b5aa13 | ||
|
|
01bceca7df | ||
|
|
ccca313a15 | ||
|
|
c463eedc50 | ||
|
|
7f49d6f08b | ||
|
|
78d4487c58 | ||
|
|
8a3f52b704 | ||
|
|
6e04535eff | ||
|
|
5bb580147a | ||
|
|
1adaa9f5c4 | ||
|
|
7b19bbb1a7 | ||
|
|
e3d4165fa4 | ||
|
|
ec54434427 | ||
|
|
67bf69fc21 | ||
|
|
c60e287cbe | ||
|
|
de4b2861b6 | ||
|
|
eb4234d814 | ||
|
|
4da7b8bd17 | ||
|
|
c0f5f224bf | ||
|
|
9b89814056 | ||
|
|
12d0baf5d3 | ||
|
|
8a695c14d7 | ||
|
|
f253bdf954 | ||
|
|
38d8764f15 | ||
|
|
01f6d106a2 | ||
|
|
9119860012 | ||
|
|
eeaaa0e6c9 | ||
|
|
db5987a466 | ||
|
|
b5d5cb085b | ||
|
|
dcf17e3b72 | ||
|
|
5b746c0d9c | ||
|
|
41bc6ca444 | ||
|
|
aba2f58fc4 | ||
|
|
aa4de67e90 | ||
|
|
b1a9fbe3ca | ||
|
|
603cf2c0ab | ||
|
|
0bc27e83ed | ||
|
|
25c1d03cca | ||
|
|
e2b4151ab4 | ||
|
|
889d307372 | ||
|
|
fd23396b95 | ||
|
|
f1c4910993 | ||
|
|
b817148d1c | ||
|
|
f422b22af1 | ||
|
|
6cd56dfd62 | ||
|
|
fe708e0a0b | ||
|
|
4fc0bbc9d7 | ||
|
|
fd0eb4ca7d |
@@ -2,17 +2,23 @@
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
|
||||
#
|
||||
version: 2
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
node: circleci/node@3.0.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:16.10.0
|
||||
- image: circleci/mongo:4.4
|
||||
- image: cimg/node:16.11.0
|
||||
- image: mongo:4.4
|
||||
|
||||
working_directory: ~/repo
|
||||
working_directory: ~/homebrewery
|
||||
executor: node/default
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
- checkout:
|
||||
path: ~/homebrewery
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
@@ -21,12 +27,48 @@ jobs:
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- run: npm install
|
||||
- node/install-npm
|
||||
- node/install-packages:
|
||||
app-dir: ~/homebrewery
|
||||
cache-path: node_modules
|
||||
override-ci-command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
test:
|
||||
docker:
|
||||
- image: cimg/node:16.11.0
|
||||
|
||||
working_directory: ~/homebrewery
|
||||
parallelism: 4
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
# run tests!
|
||||
- run: npm run circleci
|
||||
- run:
|
||||
name: Test - Basic
|
||||
command: npm run test:basic
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-span
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
|
||||
workflows:
|
||||
build_and_test:
|
||||
jobs:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
57
README.md
57
README.md
@@ -9,37 +9,37 @@ using [Markdown][markdown-url]. It is distributed under the terms of the [MIT Li
|
||||
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
|
||||
|
||||
## Quick Start
|
||||
The easiest way to get started using the Homebrewery is to use it
|
||||
The easiest way to get started using The Homebrewery is to use it
|
||||
[on our website][homebrewery-url]. The code is open source, so feel free to
|
||||
clone it, tinker with it. If you want to make changes to the code, you can run
|
||||
clone it and tinker with it. If you want to make changes to the code, you can run
|
||||
your own local version for testing by following the installation instructions
|
||||
below.
|
||||
|
||||
[homebrewery-url]: https://homebrewery.naturalcrit.com
|
||||
|
||||
### Installation
|
||||
First, install three programs that the Homebrewery requires to run and retrieve
|
||||
First, install three programs that The Homebrewery requires to run and retrieve
|
||||
updates:
|
||||
|
||||
1. install [node](https://nodejs.org/en/)
|
||||
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
||||
|
||||
For easiest installation, follow these steps:
|
||||
1. In the installer, uncheck the option to run as a service
|
||||
1. You can install MongoDB Compass if you want a GUI to view your database documents
|
||||
1. Go to the C drive and create a folder called "data"
|
||||
1. Inside the "data" folder, create a new folder called "db"
|
||||
1. Open a command prompt or other terminal and navigate to your mongodb install folder (c:program files\mongo\server\4.4\bin)
|
||||
1. In the command prompt, run "mongod", which will start up your local database server
|
||||
1. While MongoD is running, open a second command prompt and navigate to the mongodb install folder
|
||||
1. In the second command prompt, run "mongo", which allows you to edit the database
|
||||
1. Type `use homebrewery` to create the homebrewery database. You should see `switched to db homebrewery`
|
||||
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`
|
||||
1. Search in Windows for "Advanced system settings" and open it
|
||||
1. Click "Environment variables", find the "path" variable, and double-click to open it
|
||||
1. Click "New" and paste in the path to the mongodb "bin" folder
|
||||
1. Click "OK", "OK", "OK" to close all the windows
|
||||
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt)
|
||||
For the easiest installation, follow these steps:
|
||||
1. In the installer, uncheck the option to run as a service.
|
||||
1. You can install MongoDB Compass if you want a GUI to view your database documents.
|
||||
1. Go to the C:\ drive and create a folder called "data".
|
||||
1. Inside the "data" folder, create a new folder called "db".
|
||||
1. Open a command prompt or other terminal and navigate to your MongoDB install folder (C:\Program Files\Mongo\Server\4.4\bin).
|
||||
1. In the command prompt, run "mongod", which will start up your local database server.
|
||||
1. While MongoD is running, open a second command prompt and navigate to the MongoDB install folder.
|
||||
1. In the second command prompt, run "mongo", which allows you to edit the database.
|
||||
1. Type `use homebrewery` to create The Homebrewery database. You should see `switched to db homebrewery`.
|
||||
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`.
|
||||
1. Search in Windows for "Advanced system settings" and open it.
|
||||
1. Click "Environment variables", find the "path" variable, and double-click to open it.
|
||||
1. Click "New" and paste in the path to the MongoDB "bin" folder.
|
||||
1. Click "OK" three times to close all the windows.
|
||||
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt).
|
||||
|
||||
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
||||
```
|
||||
@@ -54,7 +54,7 @@ the project to run locally.
|
||||
You can set this temporarily in your shell of choice:
|
||||
* Windows Powershell: `$env:NODE_ENV="local"`
|
||||
* Windows CMD: `set NODE_ENV=local`
|
||||
* Linux / OSX: `export NODE_ENV=local`
|
||||
* Linux / macOS: `export NODE_ENV=local`
|
||||
|
||||
Third, you will need to install the Node dependencies, compile the app, and run
|
||||
it using the two commands:
|
||||
@@ -63,7 +63,7 @@ it using the two commands:
|
||||
1. `npm start`
|
||||
|
||||
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
||||
in your browser and use the Homebrewery offline.
|
||||
in your browser and use The Homebrewery offline.
|
||||
|
||||
### Running the application via Docker
|
||||
|
||||
@@ -95,11 +95,11 @@ You can check out the [changelog](./changelog.md).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](./license). Which means you
|
||||
This project is licensed under the [MIT license](./license), which means you
|
||||
are free to use The Homebrewery in any way that you want, except for claiming
|
||||
that you made it yourself.
|
||||
|
||||
If you wish to sell or in some way gain profit for what's created on this site,
|
||||
If you wish to sell, or in some way gain profit for, what's created on this site,
|
||||
it's your responsibility to ensure you have the proper licenses/rights for any
|
||||
images or resources used.
|
||||
|
||||
@@ -108,13 +108,12 @@ images or resources used.
|
||||
You are welcome to contribute to the development and maintenance of the
|
||||
project! There are several ways of doing that:
|
||||
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some
|
||||
of them are outdated, duplicates or doesn't contain any useful info. In order
|
||||
to help you can [mark duplicates][github-mark-duplicate-url], try to
|
||||
reproduce some complex or weird issues, try with finding a workaround for a
|
||||
reported bug or just mention issue managers team to let them know about
|
||||
outdated issue via `@naturalcrit/issue-managers`.
|
||||
of them are outdated, duplicates, or don't contain any useful info. To help, you can [mark duplicates][github-mark-duplicate-url], try to
|
||||
reproduce some complex or weird issues, try finding a workaround for a
|
||||
reported bug, or just mention our issue managers team to let them know about
|
||||
outdated issues via `@naturalcrit/issue-managers`.
|
||||
- Our [subreddit][subreddit-url] is constantly growing and there are number of
|
||||
bug reports: any help with sorting them out is very welcome.
|
||||
bug reports. Any help with sorting them out is very welcome.
|
||||
- And of course you can contribute by fixing a bug or implementing a new
|
||||
feature by yourself, we are waiting for your
|
||||
[pull requests][github-pr-docs-url]!
|
||||
|
||||
61
changelog.md
61
changelog.md
@@ -29,21 +29,74 @@ pre {
|
||||
.page p + pre {
|
||||
margin-top : 0.1cm;
|
||||
}
|
||||
|
||||
.page .openSans {
|
||||
font-family: 'Open Sans';
|
||||
font-size: 0.9em;
|
||||
}
|
||||
```
|
||||
|
||||
## changelog
|
||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||
|
||||
### Wednesday 27/03/2022 - v3.0.8
|
||||
{{taskList
|
||||
* [x] Style updates to user page.
|
||||
|
||||
* [x] Added a logout button (finally)! You can find it under {{openSans **USERNAME {{fa,fa-user}} → LOGOUT {{fas,fa-power-off}}**}}
|
||||
|
||||
Fixes issues: [#303](https://github.com/naturalcrit/homebrewery/issues/303)
|
||||
|
||||
* [x] Clarified the default text when submitting an issue via Reddit post.
|
||||
|
||||
* [x] Fixed broken Table of Contents links in PDFs. (Thanks lucastucious!)
|
||||
|
||||
Fixes issues: [#1749](https://github.com/naturalcrit/homebrewery/issues/1749)
|
||||
|
||||
* [x] Fixed window resizing causing the edit page divider to get lost off of the edge of the page.
|
||||
|
||||
Fixes issues: [#2053](https://github.com/naturalcrit/homebrewery/issues/2053)
|
||||
|
||||
* [x] Fixed Class Table decorations overlapping main text.
|
||||
|
||||
Fixes issues: [#1985](https://github.com/naturalcrit/homebrewery/issues/1985)
|
||||
|
||||
* [x] Updated {{openSans **STYLE EDITOR {{fa,fa-pencil-alt}} → REMOVE DROP CAP {{fas,fa-remove-format}}**}} snippet to also remove small-caps first line font.
|
||||
|
||||
* [x] Background work in preparation for brew themes.
|
||||
}}
|
||||
|
||||
### Wednesday 02/02/2022 - v3.0.7
|
||||
{{taskList
|
||||
* [x] Revert active line highlighting.
|
||||
|
||||
Fixes issues: [#1913](https://github.com/naturalcrit/homebrewery/issues/1913)
|
||||
|
||||
* [x] Added install steps for Ubuntu. [HERE](https://github.com/naturalcrit/homebrewery/blob/master/install/README.UBUNTU.md)
|
||||
|
||||
Fixes issues: [#1900](https://github.com/naturalcrit/homebrewery/issues/1900)
|
||||
|
||||
* [x] Added social media links to home page.
|
||||
|
||||
* [x] Increase brews visible on the user page to 1,000.
|
||||
|
||||
Fixes issues: [#1943](https://github.com/naturalcrit/homebrewery/issues/1943)
|
||||
|
||||
* [x] Added a Legacy to V3 migration guide under {{openSans **NEED HELP? {{fa,fa-question-circle}} → MIGRATE {{fas,fa-file-import}}**}}
|
||||
|
||||
* [x] Background refactoring and unit tests.
|
||||
}}
|
||||
|
||||
### Saturday 18/12/2021 - v3.0.6
|
||||
{{taskList
|
||||
* [x] Fixed text wrapping for long strings in code blocks.
|
||||
|
||||
Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736)
|
||||
|
||||
* [x] Code search/replace `CTRL F / CTRL SHIFT F`
|
||||
|
||||
* [x] Code search/replace PC: `CTRL F / CTRL SHIFT F` / Mac: `CMD F / OPTION CMD F`
|
||||
|
||||
Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201)
|
||||
|
||||
|
||||
* [x] Auto-closing HTML tags and curly braces `{{ }}`
|
||||
* [x] Highlight current active line
|
||||
|
||||
@@ -56,7 +109,7 @@ For a full record of development, visit our [Github Page](https://github.com/nat
|
||||
* [x] Make columns even in V3 Table of Contents.
|
||||
|
||||
Fixes issues: [#1671](https://github.com/naturalcrit/homebrewery/issues/1671)
|
||||
|
||||
|
||||
* [x] Fix `CTRL P` failing to print from `/new` pages.
|
||||
|
||||
Fixes issues: [#1815](https://github.com/naturalcrit/homebrewery/issues/1815)
|
||||
|
||||
@@ -17,6 +17,7 @@ const PAGE_HEIGHT = 1056;
|
||||
const PPR_THRESHOLD = 50;
|
||||
|
||||
const BrewRenderer = createClass({
|
||||
displayName : 'BrewRenderer',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
text : '',
|
||||
|
||||
@@ -5,6 +5,7 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const ErrorBar = createClass({
|
||||
displayName : 'ErrorBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
errors : []
|
||||
|
||||
@@ -7,6 +7,7 @@ const cx = require('classnames'); //Unused variable
|
||||
const DISMISS_KEY = 'dismiss_notification09-9-21';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
displayName : 'NotificationPopup',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
notifications : {}
|
||||
|
||||
@@ -26,6 +26,7 @@ const splice = function(str, index, inject){
|
||||
|
||||
|
||||
const Editor = createClass({
|
||||
displayName : 'Editor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
@@ -208,6 +209,7 @@ const Editor = createClass({
|
||||
view={this.state.view}
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={false}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const request = require('superagent');
|
||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||
|
||||
const MetadataEditor = createClass({
|
||||
displayName : 'MetadataEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
metadata : {
|
||||
@@ -58,7 +59,7 @@ const MetadataEditor = createClass({
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
request.delete(`/api/${this.props.metadata.editId}`)
|
||||
request.delete(`/api/${this.props.metadata.googleId}${this.props.metadata.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
window.location.href = '/';
|
||||
|
||||
@@ -14,6 +14,7 @@ const execute = function(val, brew){
|
||||
};
|
||||
|
||||
const Snippetbar = createClass({
|
||||
displayName : 'SnippetBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
@@ -103,6 +104,7 @@ module.exports = Snippetbar;
|
||||
|
||||
|
||||
const SnippetGroup = createClass({
|
||||
displayName : 'SnippetGroup',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
|
||||
@@ -97,7 +97,11 @@ module.exports = [
|
||||
gen : dedent`/* Removes Drop Caps */
|
||||
.page h1+p:first-letter {
|
||||
all: unset;
|
||||
}\n\n`
|
||||
}\n\n
|
||||
/* Removes Small-Caps in first line */
|
||||
.page h1+p:first-line {
|
||||
all: unset;
|
||||
}`
|
||||
},
|
||||
{
|
||||
name : 'Tweak Drop Cap',
|
||||
|
||||
@@ -13,6 +13,7 @@ const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
|
||||
const Homebrew = createClass({
|
||||
displayName : 'Homebrewery',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
url : '',
|
||||
@@ -31,11 +32,14 @@ const Homebrew = createClass({
|
||||
}
|
||||
};
|
||||
},
|
||||
componentWillMount : function() {
|
||||
global.account = this.props.account;
|
||||
|
||||
getInitialState : function(){
|
||||
global.version = this.props.version;
|
||||
global.account = this.props.account;
|
||||
global.enable_v3 = this.props.enable_v3;
|
||||
return {};
|
||||
},
|
||||
|
||||
render : function (){
|
||||
return (
|
||||
<Router location={this.props.url}>
|
||||
|
||||
@@ -3,7 +3,7 @@ const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const Account = createClass({
|
||||
|
||||
displayName : 'AccountNavItem',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
url : ''
|
||||
@@ -18,11 +18,42 @@ const Account = createClass({
|
||||
}
|
||||
},
|
||||
|
||||
handleLogout : function(){
|
||||
if(confirm('Are you sure you want to log out?')) {
|
||||
// Reset divider position
|
||||
window.localStorage.removeItem('naturalcrit-pane-split');
|
||||
// Clear login cookie
|
||||
document.cookie = `nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;samesite=lax;${window.domain ? `domain=${window.domain}` : ''}`;
|
||||
window.location = '/';
|
||||
};
|
||||
},
|
||||
|
||||
render : function(){
|
||||
if(global.account){
|
||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
|
||||
{global.account.username}
|
||||
</Nav.item>;
|
||||
return <Nav.dropdown>
|
||||
<Nav.item
|
||||
className='account'
|
||||
color='orange'
|
||||
icon='fas fa-user'
|
||||
>
|
||||
{global.account.username}
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
href={`/user/${global.account.username}`}
|
||||
color='yellow'
|
||||
icon='fas fa-beer'
|
||||
>
|
||||
brews
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='logout'
|
||||
color='red'
|
||||
icon='fas fa-power-off'
|
||||
onClick={this.handleLogout}
|
||||
>
|
||||
logout
|
||||
</Nav.item>
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
|
||||
|
||||
@@ -7,6 +7,7 @@ const MAX_TITLE_LENGTH = 50;
|
||||
|
||||
|
||||
const EditTitle = createClass({
|
||||
displayName : 'EditTitleNavItem',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
title : '',
|
||||
|
||||
30
client/homebrew/navbar/help.navitem.jsx
Normal file
30
client/homebrew/navbar/help.navitem.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.dropdown>
|
||||
<Nav.item color='grey' icon='fas fa-question-circle'>
|
||||
need help?
|
||||
</Nav.item>
|
||||
<Nav.item color='red' icon='fas fa-fw fa-bug'
|
||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent`
|
||||
**Browser(s)** :
|
||||
**Operating System** :
|
||||
**Legacy or v3 Renderer** :
|
||||
**Issue** : `)}`}
|
||||
newTab={true}
|
||||
rel='noopener noreferrer'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
||||
href='/migrate'
|
||||
newTab={true}
|
||||
rel='noopener noreferrer'>
|
||||
migrate
|
||||
</Nav.item>
|
||||
</Nav.dropdown>;
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item
|
||||
newTab={true}
|
||||
color='red'
|
||||
icon='fas fa-bug'
|
||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
||||
report issue
|
||||
</Nav.item>;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const PatreonNavItem = require('./patreon.navitem.jsx');
|
||||
|
||||
const Navbar = createClass({
|
||||
displayName : 'Navbar',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
//showNonChromeWarning : false,
|
||||
@@ -13,12 +14,10 @@ const Navbar = createClass({
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
this.setState({
|
||||
//showNonChromeWarning : !isChrome,
|
||||
ver : window.version
|
||||
});
|
||||
getInitialState : function() {
|
||||
return {
|
||||
ver : global.version
|
||||
};
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@navbarHeight : 28px;
|
||||
@keyframes coloring {
|
||||
@keyframes pinkColoring {
|
||||
//from {color: white;}
|
||||
//to {color: red;}
|
||||
0% {color: pink;}
|
||||
@@ -62,19 +63,21 @@
|
||||
}
|
||||
i{
|
||||
.animate(color);
|
||||
animation-name: coloring;
|
||||
animation-name: pinkColoring;
|
||||
animation-duration: 2s;
|
||||
color: pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem{
|
||||
.recent.navItem {
|
||||
position : relative;
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
h4{
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
@@ -88,11 +91,12 @@
|
||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
||||
}
|
||||
.item{
|
||||
#backgroundColors;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 13px 5px;
|
||||
padding : 8px 5px 13px;
|
||||
background-color : #333;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
@@ -138,4 +142,7 @@
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
.account.navItem{
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const VIEW_KEY = 'homebrewery-recently-viewed';
|
||||
|
||||
|
||||
const RecentItems = createClass({
|
||||
|
||||
DisplayName : 'RecentItems',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
storageKey : '',
|
||||
@@ -123,8 +123,8 @@ const RecentItems = createClass({
|
||||
if(!this.state.showDropdown) return null;
|
||||
|
||||
const makeItems = (brews)=>{
|
||||
return _.map(brews, (brew)=>{
|
||||
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
return _.map(brews, (brew, i)=>{
|
||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
</a>;
|
||||
|
||||
@@ -8,6 +8,7 @@ const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true'
|
||||
|
||||
|
||||
const RedditShare = createClass({
|
||||
displayName : 'RedditShareNavItem',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -6,10 +6,11 @@ const cx = require('classnames');
|
||||
const moment = require('moment');
|
||||
const request = require('superagent');
|
||||
|
||||
const googleDriveIcon = require('../../../googleDrive.png');
|
||||
const googleDriveIcon = require('../../../../googleDrive.png');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = createClass({
|
||||
displayName : 'BrewItem',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
@@ -30,19 +31,11 @@ const BrewItem = createClass({
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
if(this.props.brew.googleId) {
|
||||
request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
request.delete(`/api/${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
request.delete(`/api/${this.props.brew.googleId}${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
renderDeleteBrewLink : function(){
|
||||
@@ -17,6 +17,8 @@
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
box-shadow : 0px 4px 5px 0px #333;
|
||||
background-color : #cab2802e;
|
||||
.text {
|
||||
min-height : 54px;
|
||||
h4{
|
||||
160
client/homebrew/pages/basePages/listPage/listPage.jsx
Normal file
160
client/homebrew/pages/basePages/listPage/listPage.jsx
Normal file
@@ -0,0 +1,160 @@
|
||||
require('./listPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||
|
||||
const ListPage = createClass({
|
||||
displayName : 'ListPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brewCollection : [
|
||||
{
|
||||
title : '',
|
||||
class : '',
|
||||
brews : []
|
||||
}
|
||||
],
|
||||
navItems : <></>
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
sortType : 'alpha',
|
||||
sortDir : 'asc',
|
||||
filterString : ''
|
||||
};
|
||||
},
|
||||
|
||||
renderBrews : function(brews){
|
||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||
|
||||
return _.map(brews, (brew, idx)=>{
|
||||
return <BrewItem brew={brew} key={idx}/>;
|
||||
});
|
||||
},
|
||||
|
||||
sortBrewOrder : function(brew){
|
||||
if(!brew.title){brew.title = 'No Title';}
|
||||
const mapping = {
|
||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
||||
'created' : moment(brew.createdAt).format(),
|
||||
'updated' : moment(brew.updatedAt).format(),
|
||||
'views' : brew.views,
|
||||
'latest' : moment(brew.lastViewed).format()
|
||||
};
|
||||
return mapping[this.state.sortType];
|
||||
},
|
||||
|
||||
handleSortOptionChange : function(event){
|
||||
this.setState({
|
||||
sortType : event.target.value
|
||||
});
|
||||
},
|
||||
|
||||
handleSortDirChange : function(event){
|
||||
this.setState({
|
||||
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
||||
});
|
||||
},
|
||||
|
||||
renderSortOption : function(sortTitle, sortValue){
|
||||
return <td>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.handleSortOptionChange}
|
||||
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
</td>;
|
||||
},
|
||||
|
||||
handleFilterTextChange : function(e){
|
||||
this.setState({
|
||||
filterString : e.target.value
|
||||
});
|
||||
return;
|
||||
},
|
||||
|
||||
renderFilterOption : function(){
|
||||
return <td>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
type='search'
|
||||
placeholder='search title/description'
|
||||
onChange={this.handleFilterTextChange}
|
||||
/>
|
||||
</label>
|
||||
</td>;
|
||||
},
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>Sort by :</h6>
|
||||
</td>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
<td>
|
||||
<h6>Direction :</h6>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={this.handleSortDirChange}
|
||||
className='sortDir'
|
||||
>
|
||||
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
|
||||
</button>
|
||||
</td>
|
||||
{this.renderFilterOption()}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
getSortedBrews : function(brews){
|
||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||
brews = _.filter(brews, (brew)=>{
|
||||
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
||||
(_.deburr(brew.description).toLowerCase().includes(testString));
|
||||
});
|
||||
|
||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||
},
|
||||
|
||||
renderBrewCollection : function(brewCollection){
|
||||
return _.map(brewCollection, (brewGroup, idx)=>{
|
||||
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
||||
<h1>{brewGroup.title || 'No Title'}</h1>
|
||||
{this.renderBrews(this.getSortedBrews(brewGroup.brews))}
|
||||
</div>;
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='listPage sitePage'>
|
||||
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='phb'>
|
||||
{this.renderSortOptions()}
|
||||
{this.renderBrewCollection(this.props.brewCollection)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ListPage;
|
||||
@@ -11,7 +11,7 @@
|
||||
-webkit-column-gap : auto;
|
||||
-moz-column-gap : auto;
|
||||
}
|
||||
.userPage{
|
||||
.listPage{
|
||||
.content{
|
||||
overflow-y : scroll;
|
||||
.phb{
|
||||
@@ -10,7 +10,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
@@ -27,6 +27,7 @@ const googleDriveInactive = require('../../googleDriveMono.png');
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
|
||||
const EditPage = createClass({
|
||||
displayName : 'EditPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
@@ -199,73 +200,18 @@ const EditPage = createClass({
|
||||
const brew = this.state.brew;
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
if(this.state.saveGoogle) {
|
||||
if(transfer) {
|
||||
const res = await request
|
||||
.post('/api/newGoogle/')
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Transferring to Google!');
|
||||
this.setState({ errors: err, saveGoogle: false });
|
||||
});
|
||||
const params = `${transfer ? `?transfer${this.state.saveGoogle ? 'To' : 'From'}Google=true` : ''}`;
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}${params}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ errors: err });
|
||||
});
|
||||
|
||||
if(!res) { return; }
|
||||
|
||||
console.log('Deleting Local Copy');
|
||||
await request.delete(`/api/${brew.editId}`)
|
||||
.send()
|
||||
.catch((err)=>{
|
||||
console.log('Error deleting Local Copy');
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
|
||||
} else {
|
||||
const res = await request
|
||||
.put(`/api/updateGoogle/${brew.editId}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Saving to Google!');
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
}
|
||||
} else {
|
||||
if(transfer) {
|
||||
const res = await request.post('/api')
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error creating Local Copy');
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
await request.get(`/api/removeGoogle/${brew.googleId}${brew.editId}`)
|
||||
.send()
|
||||
.catch((err)=>{
|
||||
console.log('Error Deleting Google Brew');
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
|
||||
} else {
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ errors: err });
|
||||
return;
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
}
|
||||
this.savedBrew = res.body;
|
||||
if(transfer) {
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.googleId ?? ''}${this.savedBrew.editId}`);
|
||||
}
|
||||
|
||||
this.setState((prevState)=>({
|
||||
@@ -433,7 +379,7 @@ const EditPage = createClass({
|
||||
{this.renderGoogleDriveIcon()}
|
||||
{this.renderSaveButton()}
|
||||
<NewBrew />
|
||||
<ReportIssue />
|
||||
<HelpNavItem/>
|
||||
<Nav.dropdown>
|
||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||
share
|
||||
|
||||
@@ -7,8 +7,8 @@ const cx = require('classnames');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
@@ -33,7 +33,7 @@ const ErrorPage = createClass({
|
||||
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<IssueNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
@@ -9,7 +9,7 @@ const { Meta } = require('vitreum/headtags');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
|
||||
@@ -21,6 +21,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
const HomePage = createClass({
|
||||
displayName : 'HomePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
@@ -58,7 +59,7 @@ const HomePage = createClass({
|
||||
return <Navbar ver={this.props.ver}>
|
||||
<Nav.section>
|
||||
<NewBrewItem />
|
||||
<IssueNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
|
||||
202
client/homebrew/pages/homePage/migrate.md
Normal file
202
client/homebrew/pages/homePage/migrate.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# How to Convert a Legacy Document to v3
|
||||
Here you will find a number of steps to guide you through converting a Legacy document into a Homebrewery v3 document.
|
||||
|
||||
**The first thing you'll want to do is switch the editor's rendering engine from `Legacy` to `v3`.** This will be the renderer we design features for moving forward.
|
||||
|
||||
There are some examples of Legacy code in the code pane if you need more context behind some of the changes.
|
||||
|
||||
**This document will evolve as users like yourself inform us of issues with it, or areas of conversion that it does not cover. _Please_ reach out if you have any suggestions for this document.**
|
||||
|
||||
## Simple Replacements
|
||||
To make your life a little easier with this section, a text editor like [VSCode](https://code.visualstudio.com/) or Notepad will help a lot.
|
||||
|
||||
The following table describes Legacy and other document elements and their Homebrewery counterparts. A simple find/replace should get these in working order.
|
||||
|
||||
| Legacy / Other | Homebrewery |
|
||||
|:----------------|:-----------------------------|
|
||||
| `\pagebreak` | `\page` |
|
||||
| `======` | `\page` |
|
||||
| `\pagebreaknum` | `{{pageNumber,auto}}\n\page` |
|
||||
| `@=====` | `{{pageNumber,auto}}\n\page` |
|
||||
| `\columnbreak` | `\column` |
|
||||
| `.phb` | `.page` |
|
||||
|
||||
## Classed or Styled Divs
|
||||
Anything that relies on the following syntax can be changed to the new Homebrewery v3 curly brace syntax:
|
||||
|
||||
```
|
||||
<div class="classTable wide">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
:
|
||||
The above example is equivalent to the following in v3 syntax.
|
||||
|
||||
```
|
||||
{{classTable,wide
|
||||
...
|
||||
}}
|
||||
```
|
||||
:
|
||||
Some examples of this include class tables (as shown above), descriptive blocks, notes, and spell lists.
|
||||
|
||||
\column
|
||||
|
||||
## Margins and Padding
|
||||
Any manual margins and padding to push text down the page will likely need to be updated. Colons can be used on lines by themselves to push things down the page vertically if you'd rather not set pixel-perfect margins or padding.
|
||||
|
||||
## Notes
|
||||
|
||||
In Legacy, notes are denoted using markdown blockquote syntax. In Homebrewery v3, this is replaced by the curly brace syntax.
|
||||
|
||||
<!--
|
||||
> ##### Catchy Title
|
||||
> Useful Information
|
||||
-->
|
||||
|
||||
{{note
|
||||
##### Title
|
||||
Information
|
||||
}}
|
||||
|
||||
## Split Tables
|
||||
Split tables also use the curly brace syntax, as the new renderer can handle style values separately from class names.
|
||||
|
||||
<!--
|
||||
<div style='column-count:2'>
|
||||
|
||||
| d8 | Loot |
|
||||
|:---:|:-----------:|
|
||||
| 1 | 100gp |
|
||||
| 2 | 200gp |
|
||||
| 3 | 300gp |
|
||||
| 4 | 400gp |
|
||||
|
||||
| d8 | Loot |
|
||||
|:---:|:-----------:|
|
||||
| 5 | 500gp |
|
||||
| 6 | 600gp |
|
||||
| 7 | 700gp |
|
||||
| 8 | 1000gp |
|
||||
|
||||
</div>
|
||||
-->
|
||||
|
||||
##### Typical Difficulty Classes
|
||||
{{column-count:2
|
||||
| Task Difficulty | DC |
|
||||
|:----------------|:--:|
|
||||
| Very easy | 5 |
|
||||
| Easy | 10 |
|
||||
| Medium | 15 |
|
||||
|
||||
| Task Difficulty | DC |
|
||||
|:------------------|:--:|
|
||||
| Hard | 20 |
|
||||
| Very hard | 25 |
|
||||
| Nearly impossible | 30 |
|
||||
}}
|
||||
|
||||
## Blockquotes
|
||||
Blockquotes are denoted by the `>` character at the beginning of the line. In Homebrewery's v3 renderer, they hold virtually no meaning and have no CSS styling. You are free to use blockquotes when styling your document or creating themes without needing to worry about your CSS affecting other parts of the document.
|
||||
|
||||
{{pageNumber,auto}}
|
||||
|
||||
\page
|
||||
|
||||
## Stat Blocks
|
||||
|
||||
There are pretty significant differences between stat blocks on the Legacy renderer and Homebrewery v3. This section contains a list of changes that will need to be made to update the stat block.
|
||||
|
||||
### Initial Changes
|
||||
You will want to **remove all leading** `___` that started the stat block in Legacy, and replace that with `{{monster` before the stat block, and `}}` after it.
|
||||
|
||||
**If you want a frame** around the stat block, you can add `,frame` to the curly brace definition.
|
||||
|
||||
**If the stat block was wide**, make sure to add `,wide` to the curly brace definition.
|
||||
|
||||
### Blockquotes
|
||||
The key difference is the lack of blockquotes. Legacy documents use the `>` symbol at the start of the line for each line in the stat block, and the v3 renderer does not. **You will want to remove all `>` characters at the beginning of all lines, and delete any leading spaces.**
|
||||
|
||||
### Lists
|
||||
The basic characteristics and advanced characteristics sections are not list elements in Homebrewery. You will want to **remove all `-` or `*` characters from the beginning of lines.**
|
||||
|
||||
### Spacing
|
||||
In order to have the correct spacing after removing the list elements, you will want to **add two colons between the name of each basic/advanced characteristic and its value.** _(see example in the code pane)_
|
||||
|
||||
Additionally, in the special traits and actions sections, you will want to add a colon at the beginning of each line that separates a trait/action from another, as seen below. **Any empty lines between special traits and actions should contain only a colon.** _(see example in the code pane)_
|
||||
|
||||
\column
|
||||
|
||||
{{margin-top:102px}}
|
||||
|
||||
<!--
|
||||
### Legacy/Other Document Example:
|
||||
___
|
||||
> ## Centaur
|
||||
> *Large Monstrosity, neutral good*
|
||||
>___
|
||||
> - **Armor Class** 12
|
||||
> - **Hit Points** 45(6d10 + 12)
|
||||
> - **Speed** 50ft.
|
||||
>___
|
||||
>|STR|DEX|CON|INT|WIS|CHA|
|
||||
>|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
>|18 (+4)|14 (+2)|14 (+2)|9 (-1)|13 (+1)|11 (+0)|
|
||||
>___
|
||||
> - **Skills** Athletics +6, Perception +3, Survival +3
|
||||
> - **Senses** passive Perception 13
|
||||
> - **Languages** Elvish, Sylvan
|
||||
> - **Challenge** 2 (450 XP)
|
||||
> ___
|
||||
> ***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||
>
|
||||
> ***Second Thing*** More details.
|
||||
>
|
||||
> ### Actions
|
||||
> ***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||
>
|
||||
> ***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||
>
|
||||
> ***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||
>
|
||||
> ***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||
-->
|
||||
|
||||
### Homebrewery v3 Example:
|
||||
|
||||
{{monster
|
||||
## Centaur
|
||||
*Large monstrosity, neutral good*
|
||||
___
|
||||
**Armor Class** :: 12
|
||||
**Hit Points** :: 45(6d10 + 12)
|
||||
**Speed** :: 50ft.
|
||||
___
|
||||
| STR | DEX | CON | INT | WIS | CHA |
|
||||
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||
|18 (+4)|14 (+2)|14 (+2)|9 (-1) |13 (+1)|11 (+0)|
|
||||
___
|
||||
**Skills** :: Athletics +6, Perception +3, Survival +3
|
||||
**Senses** :: passive Perception 13
|
||||
**Languages** :: Elvish, Sylvan
|
||||
**Challenge** :: 2 (450 XP)
|
||||
___
|
||||
***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||
:
|
||||
***Second Thing*** More details.
|
||||
|
||||
### Actions
|
||||
***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||
:
|
||||
***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||
:
|
||||
***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||
:
|
||||
***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||
}}
|
||||
|
||||
{{pageNumber,auto}}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# The Homebrewery
|
||||
|
||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||
|
||||
### Homebrew D&D made easy
|
||||
@@ -57,17 +58,20 @@ The Homebrewery is licensed using the [MIT License](https://github.com/naturalcr
|
||||
If you wish to sell or in some way gain profit for what you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
### More Resources
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources).
|
||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
|
||||
|
||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
|
||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
|
||||
|
||||
<div style='position: absolute; top: 20px; right: 20px;'>
|
||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||
</div>
|
||||
|
||||
\page
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
.page #example + table td {
|
||||
border:1px dashed #00000030;
|
||||
}
|
||||
|
||||
.page {
|
||||
padding-bottom : 1.1cm;
|
||||
}
|
||||
@@ -50,9 +49,9 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
|
||||
\column
|
||||
|
||||
## New in V3.0.0
|
||||
With the latest major update to *The Homebrewery* we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
||||
|
||||
Much of the syntax and styling has changed in V3. Code in one version may be broken in the other, and updating an older brew to V3 will require more than just a copy and paste. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
||||
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
||||
|
||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
||||
|
||||
@@ -80,8 +79,15 @@ If you wish to sell or in some way gain profit for what's created on this site,
|
||||
If you'd like to credit me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
### More Homebrew Resources
|
||||
Check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources).
|
||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
{{position:absolute;top:20px;right:20px;width:auto
|
||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
@@ -123,9 +129,7 @@ A blank line can be achieved with a run of one or more `:` alone on a line. More
|
||||
Much nicer than `<br><br><br><br><br>`
|
||||
|
||||
### Definition Lists
|
||||
V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||
|
||||
**Senses** :: Here is some text that is long and overflows into a second line, creating a "hanging indent".
|
||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||
|
||||
### Column Breaks
|
||||
Column and page breaks with `\column` and `\page`.
|
||||
@@ -153,9 +157,9 @@ These can be combined to span a cell across both columns and rows. Cells must ha
|
||||
| 6A | 6B ^| 6C |
|
||||
|
||||
## Images
|
||||
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*. Images can be included using Markdown-style images.
|
||||
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*.
|
||||
|
||||
Using *Curly Injection* you can assign an id, classes, or specific inline CSS properties to the image.
|
||||
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
||||
|
||||
 {width:100px,border:"2px solid",border-radius:10px}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
@@ -23,6 +23,7 @@ const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
|
||||
const NewPage = createClass({
|
||||
displayName : 'NewPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
@@ -156,45 +157,24 @@ const NewPage = createClass({
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
};
|
||||
}
|
||||
|
||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
if(this.state.saveGoogle) {
|
||||
const res = await request
|
||||
.post('/api/newGoogle/')
|
||||
const res = await request
|
||||
.post(`/api${this.state.saveGoogle ? '?transferToGoogle=true' : ''}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err.status === 401
|
||||
? 'Not signed in!'
|
||||
: 'Error Creating New Google Brew!');
|
||||
console.log(err);
|
||||
this.setState({ isSaving: false, errors: err });
|
||||
return;
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
brew = res.body;
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.googleId}${brew.editId}`;
|
||||
} else {
|
||||
request.post('/api')
|
||||
.send(brew)
|
||||
.end((err, res)=>{
|
||||
if(err){
|
||||
this.setState({
|
||||
isSaving : false
|
||||
});
|
||||
return;
|
||||
}
|
||||
window.onbeforeunload = function(){};
|
||||
brew = res.body;
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
});
|
||||
}
|
||||
brew = res.body;
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.googleId ?? ''}${brew.editId}`;
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
@@ -290,7 +270,7 @@ const NewPage = createClass({
|
||||
<Nav.section>
|
||||
{this.renderSaveButton()}
|
||||
{this.renderLocalPrintButton()}
|
||||
<IssueNavItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
|
||||
@@ -12,6 +12,7 @@ const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const PrintPage = createClass({
|
||||
displayName : 'PrintPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
query : {},
|
||||
|
||||
@@ -14,6 +14,7 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
const SharePage = createClass({
|
||||
displayName : 'SharePage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
require('./userPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const moment = require('moment');
|
||||
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
@@ -12,18 +11,10 @@ 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 BrewItem = require('./brewItem/brewItem.jsx');
|
||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
|
||||
// const brew = {
|
||||
// title : 'SUPER Long title woah now',
|
||||
// authors : []
|
||||
// };
|
||||
|
||||
//const BREWS = _.times(25, ()=>{ return brew;});
|
||||
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const UserPage = createClass({
|
||||
displayName : 'UserPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
username : '',
|
||||
@@ -31,160 +22,47 @@ const UserPage = createClass({
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
sortType : 'alpha',
|
||||
sortDir : 'asc',
|
||||
filterString : ''
|
||||
};
|
||||
},
|
||||
getUsernameWithS : function() {
|
||||
if(this.props.username.endsWith('s'))
|
||||
return `${this.props.username}'`;
|
||||
return `${this.props.username}'s`;
|
||||
},
|
||||
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `'` : `'s`);
|
||||
|
||||
renderBrews : function(brews){
|
||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||
|
||||
const sortedBrews = this.sortBrews(brews);
|
||||
|
||||
return _.map(sortedBrews, (brew, idx)=>{
|
||||
return <BrewItem brew={brew} key={idx}/>;
|
||||
});
|
||||
},
|
||||
|
||||
sortBrewOrder : function(brew){
|
||||
if(!brew.title){brew.title = 'No Title';}
|
||||
const mapping = {
|
||||
'alpha' : _.deburr(brew.title.toLowerCase()),
|
||||
'created' : moment(brew.createdAt).format(),
|
||||
'updated' : moment(brew.updatedAt).format(),
|
||||
'views' : brew.views,
|
||||
'latest' : moment(brew.lastViewed).format()
|
||||
};
|
||||
return mapping[this.state.sortType];
|
||||
},
|
||||
|
||||
sortBrews : function(brews){
|
||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||
},
|
||||
|
||||
handleSortOptionChange : function(event){
|
||||
this.setState({
|
||||
sortType : event.target.value
|
||||
});
|
||||
},
|
||||
|
||||
handleSortDirChange : function(event){
|
||||
this.setState({
|
||||
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
|
||||
});
|
||||
},
|
||||
|
||||
renderSortOption : function(sortTitle, sortValue){
|
||||
return <td>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.handleSortOptionChange}
|
||||
className={`sortOption ${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
</td>;
|
||||
},
|
||||
|
||||
handleFilterTextChange : function(e){
|
||||
this.setState({
|
||||
filterString : e.target.value
|
||||
});
|
||||
return;
|
||||
},
|
||||
|
||||
renderFilterOption : function(){
|
||||
return <td>
|
||||
<label className='filterOption'>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
type='search'
|
||||
placeholder='search title/description'
|
||||
onChange={this.handleFilterTextChange}
|
||||
/>
|
||||
</label>
|
||||
</td>;
|
||||
},
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<h6>Sort by :</h6>
|
||||
</td>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
<td>
|
||||
<h6>Direction :</h6>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={this.handleSortDirChange}
|
||||
className='sortDir'
|
||||
>
|
||||
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
|
||||
</button>
|
||||
</td>
|
||||
{this.renderFilterOption()}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
getSortedBrews : function(){
|
||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||
const brewCollection = this.state.filterString ? _.filter(this.props.brews, (brew)=>{
|
||||
return (_.deburr(brew.title).toLowerCase().includes(testString)) ||
|
||||
(_.deburr(brew.description).toLowerCase().includes(testString));
|
||||
}) : this.props.brews;
|
||||
return _.groupBy(brewCollection, (brew)=>{
|
||||
const brews = _.groupBy(this.props.brews, (brew)=>{
|
||||
return (brew.published ? 'published' : 'private');
|
||||
});
|
||||
|
||||
const brewCollection = [
|
||||
{
|
||||
title : `${usernameWithS} published brews`,
|
||||
class : 'published',
|
||||
brews : brews.published
|
||||
}
|
||||
];
|
||||
if(this.props.username == global.account?.username){
|
||||
brewCollection.push(
|
||||
{
|
||||
title : `${usernameWithS} unpublished brews`,
|
||||
class : 'unpublished',
|
||||
brews : brews.private
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
brewCollection : brewCollection
|
||||
};
|
||||
},
|
||||
|
||||
navItems : function() {
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<NewBrew />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const brews = this.getSortedBrews();
|
||||
|
||||
return <div className='userPage sitePage'>
|
||||
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<NewBrew />
|
||||
<ReportIssue />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='phb'>
|
||||
{this.renderSortOptions()}
|
||||
<div className='published'>
|
||||
<h1>{this.getUsernameWithS()} published brews</h1>
|
||||
{this.renderBrews(brews.published)}
|
||||
</div>
|
||||
{this.props.username == global.account?.username &&
|
||||
<div className='unpublished'>
|
||||
<h1>{this.getUsernameWithS()} unpublished brews</h1>
|
||||
{this.renderBrews(brews.private)}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()}></ListPage>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ These instructions assume that you are installing to a completely new, fresh Fre
|
||||
|
||||
2. Install wget (`pkg install -y wget`). On a fresh jail, you will be prompted to press 'Y' to set up `pkg`.
|
||||
|
||||
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
|
||||
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
|
||||
|
||||
4. Make the downloaded file executable (`chmod +x install.sh`).
|
||||
|
||||
35
install/README.UBUNTU.md
Normal file
35
install/README.UBUNTU.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Ubuntu Installation Instructions
|
||||
|
||||
## Before Installing
|
||||
|
||||
These instructions assume that you are installing to a completely new, fresh Ubuntu installation. As such, some steps will not be necessary if you are installing to an existing Ubuntu instance.
|
||||
|
||||
## Installation instructions
|
||||
|
||||
1. Install Ubuntu.
|
||||
|
||||
2. Install wget (`apt install -y wget`). This may already be installed, depending on your exact Ubuntu version.
|
||||
|
||||
3. Download the installation script (`wget https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/ubuntu/install.sh`).
|
||||
|
||||
4. Make the downloaded file executable (`chmod +x install.sh`).
|
||||
|
||||
5. Run the script (`sudo ./install.sh`). This will automatically download all of the required packages, install both them and HomeBrewery, configure the system and finally start HomeBrewery.
|
||||
|
||||
**NOTE:** At this time, the script **ONLY** installs HomeBrewery. It does **NOT** install the NaturalCrit login system, as that is currently a completely separate project.
|
||||
|
||||
---
|
||||
|
||||
### Testing
|
||||
|
||||
These installation instructions have been tested on the following Ubuntu releases:
|
||||
|
||||
- *ubuntu-20.04.3-desktop-amd64*
|
||||
|
||||
## Final Notes
|
||||
|
||||
While this installation process works successfully at the time of writing (December 19, 2021), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
|
||||
|
||||
Regards,
|
||||
G
|
||||
December 19, 2021
|
||||
13
install/ubuntu/etc/systemd/system/homebrewery.service
Normal file
13
install/ubuntu/etc/systemd/system/homebrewery.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Homebrewery Web Server
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
After=mongodb
|
||||
Environment=NODE_ENV=local
|
||||
WorkingDirectory=/usr/local/homebrewery
|
||||
ExecStart=node server.js
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
34
install/ubuntu/install.sh
Normal file
34
install/ubuntu/install.sh
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Install CURL and add required NodeJS source to package repo
|
||||
echo ::Install CURL
|
||||
apt install -y curl
|
||||
echo ::Add NodeJS source to package repo
|
||||
curl -fsSL https://deb.nodesource.com/setup_16.x | sudo -E bash -
|
||||
|
||||
# Install required packages
|
||||
echo ::Install Homebrewery requirements
|
||||
apt satisfy -y git nodejs npm mongodb
|
||||
|
||||
# Clone Homebrewery repo
|
||||
echo ::Get Homebrewery files
|
||||
cd /usr/local/
|
||||
git clone https://github.com/naturalcrit/homebrewery.git
|
||||
|
||||
# Install Homebrewery
|
||||
echo ::Install Homebrewery
|
||||
cd homebrewery
|
||||
npm install
|
||||
npm audit fix
|
||||
npm run postinstall
|
||||
|
||||
# Create Homebrewery service
|
||||
echo ::Create Homebrewery service
|
||||
ln -s /usr/local/homebrewery/install/ubuntu/etc/systemd/system/homebrewery.service /etc/systemd/system/homebrewery.service
|
||||
systemctl daemon-reload
|
||||
echo ::Set Homebrewery to start automatically
|
||||
systemctl enable homebrewery
|
||||
|
||||
# Start Homebrewery
|
||||
echo ::Start Homebrewery
|
||||
systemctl start homebrewery
|
||||
9979
package-lock.json
generated
9979
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
57
package.json
57
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.8",
|
||||
"engines": {
|
||||
"node": "16.11.x"
|
||||
},
|
||||
@@ -18,8 +18,11 @@
|
||||
"lint:dry": "eslint **/*.{js,jsx}",
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
"verify": "npm run lint && npm test",
|
||||
"test": "pico-check",
|
||||
"test:dev": "pico-check -v -w",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --verbose --watch",
|
||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
"postinstall": "npm run buildall",
|
||||
@@ -30,44 +33,51 @@
|
||||
"eslintIgnore": [
|
||||
"build/*"
|
||||
],
|
||||
"pico-check": {
|
||||
"require": "./tests/test.init.js"
|
||||
"jest": {
|
||||
"testTimeout" : 15000,
|
||||
"modulePaths": [
|
||||
"mode_modules",
|
||||
"shared",
|
||||
"server"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"@babel/preset-env",
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.16.5",
|
||||
"@babel/plugin-transform-runtime": "^7.16.5",
|
||||
"@babel/preset-env": "^7.16.5",
|
||||
"@babel/preset-react": "^7.16.5",
|
||||
"body-parser": "^1.19.1",
|
||||
"@babel/core": "^7.17.8",
|
||||
"@babel/plugin-transform-runtime": "^7.17.0",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@babel/preset-react": "^7.16.7",
|
||||
"body-parser": "^1.19.2",
|
||||
"classnames": "^2.3.1",
|
||||
"codemirror": "^5.64.0",
|
||||
"codemirror": "^5.65.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"create-react-class": "^15.7.0",
|
||||
"dedent-tabs": "^0.10.1",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.17.3",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-static-gzip": "2.1.1",
|
||||
"fs-extra": "10.0.0",
|
||||
"googleapis": "92.0.0",
|
||||
"express-static-gzip": "2.1.5",
|
||||
"fs-extra": "10.0.1",
|
||||
"googleapis": "98.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.13.1",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "4.0.7",
|
||||
"marked": "4.0.12",
|
||||
"marked-extended-tables": "^1.0.3",
|
||||
"markedLegacy": "npm:marked@^0.3.19",
|
||||
"moment": "^2.29.1",
|
||||
"mongoose": "^6.1.2",
|
||||
"nanoid": "3.1.30",
|
||||
"mongoose": "^6.2.8",
|
||||
"nanoid": "3.3.1",
|
||||
"nconf": "^0.11.3",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "7.0.1",
|
||||
"query-string": "7.1.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-frame-component": "4.1.3",
|
||||
@@ -77,8 +87,9 @@
|
||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.4.1",
|
||||
"eslint-plugin-react": "^7.27.1",
|
||||
"pico-check": "^2.2.0"
|
||||
"eslint": "^8.11.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"jest": "^27.5.1",
|
||||
"supertest": "^6.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const build = async ({ bundle, render, ssr })=>{
|
||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||
await fs.copy('./themes/fonts', './build/fonts');
|
||||
await fs.copy('./themes/assets', './build/assets');
|
||||
let src = './themes/5ePhbLegacy.style.less';
|
||||
//Parse brew theme files
|
||||
less.render(fs.readFileSync(src).toString(), {
|
||||
@@ -73,6 +74,6 @@ pack('./client/homebrew/homebrew.jsx', {
|
||||
if(isDev){
|
||||
livereload('./build');
|
||||
watchFile('./server.js', {
|
||||
watch : ['./client'] // Watch additional folders if you want
|
||||
watch : ['./client', './server'] // Watch additional folders if you want
|
||||
});
|
||||
}
|
||||
|
||||
315
server.js
315
server.js
@@ -1,309 +1,12 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jwt-simple');
|
||||
const express = require('express');
|
||||
const yaml = require('js-yaml');
|
||||
const app = express();
|
||||
const DB = require('./server/db.js');
|
||||
const server = require('./server/app.js');
|
||||
const config = require('./server/config.js');
|
||||
|
||||
const homebrewApi = require('./server/homebrew.api.js');
|
||||
const GoogleActions = require('./server/googleActions.js');
|
||||
const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
|
||||
const brewAccessTypes = ['edit', 'share', 'raw'];
|
||||
|
||||
//Get the brew object from the HB database or Google Drive
|
||||
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
||||
if(!brewAccessTypes.includes(accessType))
|
||||
throw ('Invalid Access Type when getting brew');
|
||||
let brew;
|
||||
if(id.length > 12) {
|
||||
const googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
|
||||
} else {
|
||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
||||
}
|
||||
|
||||
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
||||
//Split brew.text into text and style
|
||||
//unless the Access Type is RAW, in which case return immediately
|
||||
if(accessType == 'raw') {
|
||||
return brew;
|
||||
}
|
||||
splitTextStyleAndMetadata(brew);
|
||||
return brew;
|
||||
});
|
||||
|
||||
const sanitizeBrew = (brew, full=false)=>{
|
||||
delete brew._id;
|
||||
delete brew.__v;
|
||||
if(full){
|
||||
delete brew.editId;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
|
||||
|
||||
process.chdir(__dirname);
|
||||
|
||||
//app.use(express.static(`${__dirname}/build`));
|
||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('./server/forcessl.mw.js'));
|
||||
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true })
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
|
||||
//DB
|
||||
const mongoose = require('mongoose');
|
||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
|
||||
{ retryWrites: false });
|
||||
mongoose.connection.on('error', (err)=>{
|
||||
console.log('Error : Could not connect to a Mongo Database.');
|
||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||
console.log(err);
|
||||
throw 'Can not connect to Mongo';
|
||||
});
|
||||
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try {
|
||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||
//console.log("Just loaded up JWT from cookie:");
|
||||
//console.log(req.account);
|
||||
} catch (e){}
|
||||
}
|
||||
|
||||
req.config = {
|
||||
google_client_id : config.get('google_client_id'),
|
||||
google_client_secret : config.get('google_client_secret')
|
||||
};
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use(homebrewApi);
|
||||
app.use(require('./server/admin.api.js'));
|
||||
|
||||
const HomebrewModel = require('./server/homebrew.model.js').model;
|
||||
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const welcomeTextV3 = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
|
||||
const faqText = require('fs').readFileSync('./faq.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`${__dirname}/robots.txt`);
|
||||
});
|
||||
|
||||
//Home page
|
||||
app.get('/', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeText
|
||||
};
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page v3
|
||||
app.get('/v3_preview', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeTextV3,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Changelog page
|
||||
app.get('/changelog', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//FAQ page
|
||||
app.get('/faq', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Source page
|
||||
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
|
||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||
let text = brew.text;
|
||||
for (const replaceStr in replaceStrings) {
|
||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||
}
|
||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||
res.status(200).send(text);
|
||||
}));
|
||||
|
||||
//Download brew source page
|
||||
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
||||
DB.connect(config).then(()=>{
|
||||
// Ensure that we have successfully connected to the database
|
||||
// before launching server
|
||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||
server.app.listen(PORT, ()=>{
|
||||
console.log(`server on port: ${PORT}`);
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
}));
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const googleBrews = await GoogleActions.listGoogleBrews(req, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if(googleBrews)
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
return sanitizeBrew(brew, !ownAccount);
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
brew.title = `CLONE - ${brew.title}`;
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./client/template.js');
|
||||
app.use((req, res)=>{
|
||||
const props = {
|
||||
version : require('./package.json').version,
|
||||
url : req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3')
|
||||
};
|
||||
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
|
||||
.then((page)=>{ res.send(page); })
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
});
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
error[key] = value[key];
|
||||
});
|
||||
return error;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
});
|
||||
//^=====--------------------------------------=====^//
|
||||
|
||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||
app.listen(PORT, ()=>{
|
||||
console.log(`server on port:${PORT}`);
|
||||
});
|
||||
|
||||
309
server/app.js
Normal file
309
server/app.js
Normal file
@@ -0,0 +1,309 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
// Set working directory to project root
|
||||
process.chdir(`${__dirname}/..`);
|
||||
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jwt-simple');
|
||||
const express = require('express');
|
||||
const yaml = require('js-yaml');
|
||||
const app = express();
|
||||
const config = require('./config.js');
|
||||
|
||||
const homebrewApi = require('./homebrew.api.js');
|
||||
const GoogleActions = require('./googleActions.js');
|
||||
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||
const sanitizeFilename = require('sanitize-filename');
|
||||
const asyncHandler = require('express-async-handler');
|
||||
|
||||
const brewAccessTypes = ['edit', 'share', 'raw'];
|
||||
|
||||
//Get the brew object from the HB database or Google Drive
|
||||
const getBrewFromId = asyncHandler(async (id, accessType)=>{
|
||||
if(!brewAccessTypes.includes(accessType))
|
||||
throw ('Invalid Access Type when getting brew');
|
||||
let brew;
|
||||
if(id.length > 12) {
|
||||
const googleId = id.slice(0, -12);
|
||||
id = id.slice(-12);
|
||||
brew = await GoogleActions.getGoogleBrew(googleId, id, accessType);
|
||||
} else {
|
||||
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
|
||||
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
|
||||
}
|
||||
|
||||
brew = sanitizeBrew(brew, accessType === 'edit' ? false : true);
|
||||
//Split brew.text into text and style
|
||||
//unless the Access Type is RAW, in which case return immediately
|
||||
if(accessType == 'raw') {
|
||||
return brew;
|
||||
}
|
||||
splitTextStyleAndMetadata(brew);
|
||||
return brew;
|
||||
});
|
||||
|
||||
const sanitizeBrew = (brew, full=false)=>{
|
||||
delete brew._id;
|
||||
delete brew.__v;
|
||||
if(full){
|
||||
delete brew.editId;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
const splitTextStyleAndMetadata = (brew)=>{
|
||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||
if(brew.text.startsWith('```metadata')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
const metadataSection = brew.text.slice(12, index - 1);
|
||||
const metadata = yaml.load(metadataSection);
|
||||
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer']));
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
if(brew.text.startsWith('```css')) {
|
||||
const index = brew.text.indexOf('```\n\n');
|
||||
brew.style = brew.text.slice(7, index - 1);
|
||||
brew.text = brew.text.slice(index + 5);
|
||||
}
|
||||
};
|
||||
|
||||
app.use('/', serveCompressedStaticAssets(`build`));
|
||||
|
||||
//app.use(express.static(`${__dirname}/build`));
|
||||
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||
app.use(require('cookie-parser')());
|
||||
app.use(require('./forcessl.mw.js'));
|
||||
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try {
|
||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||
//console.log("Just loaded up JWT from cookie:");
|
||||
//console.log(req.account);
|
||||
} catch (e){}
|
||||
}
|
||||
|
||||
req.config = {
|
||||
google_client_id : config.get('google_client_id'),
|
||||
google_client_secret : config.get('google_client_secret')
|
||||
};
|
||||
return next();
|
||||
});
|
||||
|
||||
app.use(homebrewApi);
|
||||
app.use(require('./admin.api.js'));
|
||||
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const welcomeTextV3 = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_v3.md', 'utf8');
|
||||
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
|
||||
const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
||||
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
|
||||
//Robots.txt
|
||||
app.get('/robots.txt', (req, res)=>{
|
||||
return res.sendFile(`robots.txt`, { root: process.cwd() });
|
||||
});
|
||||
|
||||
//Home page
|
||||
app.get('/', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeText
|
||||
};
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Home page v3
|
||||
app.get('/v3_preview', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : welcomeTextV3,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Legacy/Other Document -> v3 Migration Guide
|
||||
app.get('/migrate', async (req, res, next)=>{
|
||||
const brew = {
|
||||
text : migrateText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Changelog page
|
||||
app.get('/changelog', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'Changelog',
|
||||
text : changelogText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//FAQ page
|
||||
app.get('/faq', async (req, res, next)=>{
|
||||
const brew = {
|
||||
title : 'FAQ',
|
||||
text : faqText,
|
||||
renderer : 'V3'
|
||||
};
|
||||
splitTextStyleAndMetadata(brew);
|
||||
req.brew = brew;
|
||||
return next();
|
||||
});
|
||||
|
||||
//Source page
|
||||
app.get('/source/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
|
||||
const replaceStrings = { '&': '&', '<': '<', '>': '>' };
|
||||
let text = brew.text;
|
||||
for (const replaceStr in replaceStrings) {
|
||||
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
|
||||
}
|
||||
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
|
||||
res.status(200).send(text);
|
||||
}));
|
||||
|
||||
//Download brew source page
|
||||
app.get('/download/:id', asyncHandler(async (req, res)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'raw');
|
||||
const prefix = 'HB - ';
|
||||
|
||||
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||
res.set({
|
||||
'Cache-Control' : 'no-cache',
|
||||
'Content-Type' : 'text/plain',
|
||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
||||
});
|
||||
res.status(200).send(brew.text);
|
||||
}));
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', async (req, res, next)=>{
|
||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||
|
||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
if(ownAccount && req?.account?.googleId){
|
||||
const auth = await GoogleActions.authCheck(req.account, res);
|
||||
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
if(googleBrews) {
|
||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||
brews = _.concat(brews, googleBrews);
|
||||
}
|
||||
}
|
||||
|
||||
req.brews = _.map(brews, (brew)=>{
|
||||
return sanitizeBrew(brew, !ownAccount);
|
||||
});
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
|
||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||
const brew = await getBrewFromId(req.params.id, 'edit');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//New Page
|
||||
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
brew.title = `CLONE - ${brew.title}`;
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
|
||||
if(req.params.id.length > 12) {
|
||||
const googleId = req.params.id.slice(0, -12);
|
||||
const shareId = req.params.id.slice(-12);
|
||||
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
|
||||
.catch((err)=>{next(err);});
|
||||
} else {
|
||||
await HomebrewModel.increaseView({ shareId: brew.shareId });
|
||||
}
|
||||
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
|
||||
const brew = await getBrewFromId(req.params.id, 'share');
|
||||
req.brew = brew;
|
||||
return next();
|
||||
}));
|
||||
|
||||
//Render the page
|
||||
const templateFn = require('./../client/template.js');
|
||||
app.use((req, res)=>{
|
||||
const props = {
|
||||
version : require('./../package.json').version,
|
||||
url : req.originalUrl,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
googleBrews : req.googleBrews,
|
||||
account : req.account,
|
||||
enable_v3 : config.get('enable_v3')
|
||||
};
|
||||
const title = req.brew ? req.brew.title : '';
|
||||
templateFn('homebrew', title, props)
|
||||
.then((page)=>{ res.send(page); })
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
});
|
||||
|
||||
//v=====----- Error-Handling Middleware -----=====v//
|
||||
//Format Errors so all fields will be sent
|
||||
const replaceErrors = (key, value)=>{
|
||||
if(value instanceof Error) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach(function (key) {
|
||||
error[key] = value[key];
|
||||
});
|
||||
return error;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const getPureError = (error)=>{
|
||||
return JSON.parse(JSON.stringify(error, replaceErrors));
|
||||
};
|
||||
|
||||
app.use((err, req, res, next)=>{
|
||||
const status = err.status || 500;
|
||||
console.error(err);
|
||||
res.status(status).send(getPureError(err));
|
||||
});
|
||||
//^=====--------------------------------------=====^//
|
||||
|
||||
module.exports = {
|
||||
app : app
|
||||
};
|
||||
5
server/config.js
Normal file
5
server/config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true })
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
37
server/db.js
Normal file
37
server/db.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// The main purpose of this file is to provide an interface for database
|
||||
// connection. Even though the code is quite simple and basically a tiny
|
||||
// wrapper around mongoose package, it works as single point where
|
||||
// database setup/config is performed and the interface provided here can be
|
||||
// reused by both the main application and all tests which require database
|
||||
// connection.
|
||||
|
||||
const Mongoose = require('mongoose');
|
||||
|
||||
const getMongoDBURL = (config)=>{
|
||||
return config.get('mongodb_uri') ||
|
||||
config.get('mongolab_uri') ||
|
||||
'mongodb://localhost/homebrewery';
|
||||
};
|
||||
|
||||
const handleConnectionError = (error)=>{
|
||||
if(error) {
|
||||
console.error('Could not connect to a Mongo database: \n');
|
||||
console.error(error);
|
||||
console.error('\nIf you are running locally, make sure mongodb.exe is running and DB URL is configured properly');
|
||||
process.exit(1); // non-zero exit code to indicate an error
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async ()=>{
|
||||
return await Mongoose.disconnect();
|
||||
};
|
||||
|
||||
const connect = async (config)=>{
|
||||
return await Mongoose.connect(getMongoDBURL(config),
|
||||
{ retryWrites: false }, handleConnectionError);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
connect : connect,
|
||||
disconnect : disconnect
|
||||
};
|
||||
@@ -3,15 +3,24 @@ const _ = require('lodash');
|
||||
const { google } = require('googleapis');
|
||||
const { nanoid } = require('nanoid');
|
||||
const token = require('./token.js');
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true }) // Load environment variables
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
const config = require('./config.js');
|
||||
|
||||
//let oAuth2Client;
|
||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||
JSON.parse(config.get('service_account')) :
|
||||
config.get('service_account');
|
||||
let serviceAuth;
|
||||
try {
|
||||
serviceAuth = google.auth.fromJSON(keys);
|
||||
serviceAuth.scopes = [
|
||||
'https://www.googleapis.com/auth/drive'
|
||||
];
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
|
||||
}
|
||||
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
||||
|
||||
GoogleActions = {
|
||||
const GoogleActions = {
|
||||
|
||||
authCheck : (account, res)=>{
|
||||
if(!account || !account.googleId){ // If not signed into Google
|
||||
@@ -47,7 +56,7 @@ GoogleActions = {
|
||||
},
|
||||
|
||||
getGoogleFolder : async (auth)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
fileMetadata = {
|
||||
'name' : 'Homebrewery',
|
||||
@@ -83,36 +92,27 @@ GoogleActions = {
|
||||
return folderId;
|
||||
},
|
||||
|
||||
listGoogleBrews : async (req, res)=>{
|
||||
|
||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
||||
|
||||
//TODO: Change to service account to allow non-owners to view published files.
|
||||
// Requires a driveId parameter in the drive.files.list command
|
||||
// const keys = JSON.parse(config.get('service_account'));
|
||||
// const auth = google.auth.fromJSON(keys);
|
||||
// auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
||||
listGoogleBrews : async (auth)=>{
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
const obj = await drive.files.list({
|
||||
pageSize : 100,
|
||||
pageSize : 1000,
|
||||
fields : 'nextPageToken, files(id, name, description, createdTime, modifiedTime, properties)',
|
||||
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(`Error Listing Google Brews`);
|
||||
console.log(`Error Listing Google Brews`);
|
||||
console.error(err);
|
||||
throw (err);
|
||||
//TODO: Should break out here, but continues on for some reason.
|
||||
});
|
||||
});
|
||||
|
||||
if(!obj.data.files.length) {
|
||||
console.log('No files found.');
|
||||
}
|
||||
console.log('No files found.');
|
||||
}
|
||||
|
||||
const brews = obj.data.files.map((file)=>{
|
||||
return {
|
||||
return {
|
||||
text : '',
|
||||
shareId : file.properties.shareId,
|
||||
editId : file.properties.editId,
|
||||
@@ -126,65 +126,47 @@ GoogleActions = {
|
||||
views : parseInt(file.properties.views),
|
||||
tags : '',
|
||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||
authors : [req.account.username], //TODO: properly save and load authors to google drive
|
||||
systems : []
|
||||
};
|
||||
});
|
||||
return brews;
|
||||
},
|
||||
|
||||
existsGoogleBrew : async (auth, id)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
updateGoogleBrew : async (brew)=>{
|
||||
const drive = google.drive({ version: 'v3' });
|
||||
|
||||
const result = await drive.files.get({ fileId: id })
|
||||
.catch((err)=>{
|
||||
console.log('error checking file exists...');
|
||||
console.error(err);
|
||||
return false;
|
||||
});
|
||||
|
||||
if(result){return true;}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
updateGoogleBrew : async (auth, brew)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
|
||||
if(await GoogleActions.existsGoogleBrew(auth, brew.googleId) == true) {
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
resource : {
|
||||
name : `${brew.title}.txt`,
|
||||
description : `${brew.description}`,
|
||||
properties : {
|
||||
title : brew.title,
|
||||
published : brew.published,
|
||||
version : brew.version,
|
||||
renderer : brew.renderer,
|
||||
tags : brew.tags,
|
||||
pageCount : brew.pageCount,
|
||||
systems : brew.systems.join()
|
||||
}
|
||||
},
|
||||
media : {
|
||||
mimeType : 'text/plain',
|
||||
body : brew.text
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
resource : {
|
||||
name : `${brew.title}.txt`,
|
||||
description : `${brew.description}`,
|
||||
properties : {
|
||||
title : brew.title,
|
||||
published : brew.published,
|
||||
version : brew.version,
|
||||
renderer : brew.renderer,
|
||||
tags : brew.tags,
|
||||
pageCount : brew.pageCount,
|
||||
systems : brew.systems.join()
|
||||
}
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error saving to google');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
//return res.status(500).send('Error while saving');
|
||||
});
|
||||
}
|
||||
},
|
||||
media : {
|
||||
mimeType : 'text/plain',
|
||||
body : brew.text
|
||||
}
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log('Error saving to google');
|
||||
console.error(err);
|
||||
throw (err);
|
||||
//return res.status(500).send('Error while saving');
|
||||
});
|
||||
|
||||
return (brew);
|
||||
},
|
||||
|
||||
newGoogleBrew : async (auth, brew)=>{
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
const media = {
|
||||
mimeType : 'text/plain',
|
||||
@@ -198,8 +180,8 @@ GoogleActions = {
|
||||
'description' : `${brew.description}`,
|
||||
'parents' : [folderId],
|
||||
'properties' : { //AppProperties is not accessible
|
||||
'shareId' : nanoid(12),
|
||||
'editId' : nanoid(12),
|
||||
'shareId' : brew.shareId || nanoid(12),
|
||||
'editId' : brew.editId || nanoid(12),
|
||||
'title' : brew.title,
|
||||
'views' : '0',
|
||||
'pageCount' : brew.pageCount,
|
||||
@@ -252,9 +234,8 @@ GoogleActions = {
|
||||
return newHomebrew;
|
||||
},
|
||||
|
||||
readFileMetadata : async (auth, id, accessId, accessType)=>{
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||
const drive = google.drive({ version: 'v3' });
|
||||
|
||||
const obj = await drive.files.get({
|
||||
fileId : id,
|
||||
@@ -273,16 +254,7 @@ GoogleActions = {
|
||||
throw ('Share ID does not match');
|
||||
}
|
||||
|
||||
//Access file using service account. Using API key only causes "automated query" lockouts after a while.
|
||||
|
||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||
JSON.parse(config.get('service_account')) :
|
||||
config.get('service_account');
|
||||
|
||||
const serviceAuth = google.auth.fromJSON(keys);
|
||||
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
|
||||
const serviceDrive = google.drive({ version: 'v3', auth: serviceAuth });
|
||||
const serviceDrive = google.drive({ version: 'v3' });
|
||||
|
||||
const file = await serviceDrive.files.get({
|
||||
fileId : id,
|
||||
@@ -323,10 +295,8 @@ GoogleActions = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteGoogleBrew : async (req, res, id)=>{
|
||||
|
||||
oAuth2Client = GoogleActions.authCheck(req.account, res);
|
||||
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
|
||||
deleteGoogleBrew : async (auth, id)=>{
|
||||
const drive = google.drive({ version: 'v3', auth });
|
||||
|
||||
const googleId = id.slice(0, -12);
|
||||
const accessId = id.slice(-12);
|
||||
@@ -338,7 +308,6 @@ GoogleActions = {
|
||||
.catch((err)=>{
|
||||
console.log('Error loading from Google');
|
||||
console.error(err);
|
||||
return;
|
||||
});
|
||||
|
||||
if(obj && obj.data.properties.editId != accessId) {
|
||||
@@ -353,21 +322,10 @@ GoogleActions = {
|
||||
console.log('Can\'t delete Google file');
|
||||
console.error(err);
|
||||
});
|
||||
|
||||
return res.status(200).send();
|
||||
},
|
||||
|
||||
increaseView : async (id, accessId, accessType, brew)=>{
|
||||
//service account because this is modifying another user's file properties
|
||||
//so we need extended scope
|
||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||
JSON.parse(config.get('service_account')) :
|
||||
config.get('service_account');
|
||||
|
||||
const auth = google.auth.fromJSON(keys);
|
||||
auth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||
|
||||
const drive = google.drive({ version: 'v3', auth: auth });
|
||||
const drive = google.drive({ version: 'v3' });
|
||||
|
||||
await drive.files.update({
|
||||
fileId : brew.googleId,
|
||||
@@ -384,8 +342,6 @@ GoogleActions = {
|
||||
console.error(err);
|
||||
//return res.status(500).send('Error while saving');
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -41,154 +41,195 @@ const excludePropsFromUpdate = (brew)=>{
|
||||
const propsToExclude = ['views', 'lastViewed'];
|
||||
for (const prop of propsToExclude) {
|
||||
delete brew[prop];
|
||||
};
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
const newBrew = (req, res)=>{
|
||||
const brew = req.body;
|
||||
|
||||
const beforeNewSave = (account, brew)=>{
|
||||
if(!brew.title) {
|
||||
brew.title = getGoodBrewTitle(brew.text);
|
||||
}
|
||||
|
||||
brew.authors = (req.account) ? [req.account.username] : [];
|
||||
brew.authors = (account) ? [account.username] : [];
|
||||
brew.text = mergeBrewText(brew);
|
||||
};
|
||||
|
||||
delete brew.editId;
|
||||
delete brew.shareId;
|
||||
delete brew.googleId;
|
||||
|
||||
const newLocalBrew = async (brew)=>{
|
||||
const newHomebrew = new HomebrewModel(brew);
|
||||
// Compress brew text to binary before saving
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
newHomebrew.text = undefined;
|
||||
|
||||
newHomebrew.save((err, obj)=>{
|
||||
if(err) {
|
||||
console.error(err, err.toString(), err.stack);
|
||||
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
||||
}
|
||||
|
||||
obj = obj.toObject();
|
||||
obj.gDrive = false;
|
||||
return res.status(200).send(obj);
|
||||
});
|
||||
};
|
||||
|
||||
const updateBrew = (req, res)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
const updateBrew = excludePropsFromUpdate(req.body);
|
||||
brew = _.merge(brew, updateBrew);
|
||||
brew.text = mergeBrewText(brew);
|
||||
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
brew.text = undefined;
|
||||
brew.updatedAt = new Date();
|
||||
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
}
|
||||
|
||||
brew.markModified('authors');
|
||||
brew.markModified('systems');
|
||||
|
||||
brew.save((err, obj)=>{
|
||||
if(err) throw err;
|
||||
return res.status(200).send(obj);
|
||||
});
|
||||
})
|
||||
let saved = await newHomebrew.save()
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
return res.status(500).send('Error while saving');
|
||||
console.error(err, err.toString(), err.stack);
|
||||
throw `Error while creating new brew, ${err.toString()}`;
|
||||
});
|
||||
|
||||
saved = saved.toObject();
|
||||
saved.gDrive = false;
|
||||
return saved;
|
||||
};
|
||||
|
||||
const deleteBrew = (req, res)=>{
|
||||
HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{
|
||||
if(!objs.length || err) {
|
||||
return res.status(404).send('Can not find homebrew with that id');
|
||||
}
|
||||
const newGoogleBrew = async (account, brew, res)=>{
|
||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||
|
||||
const brew = objs[0];
|
||||
|
||||
if(req.account) {
|
||||
// Remove current user as author
|
||||
brew.authors = _.pull(brew.authors, req.account.username);
|
||||
brew.markModified('authors');
|
||||
}
|
||||
|
||||
if(brew.authors.length === 0) {
|
||||
// Delete brew if there are no authors left
|
||||
brew.remove((err)=>{
|
||||
if(err) return res.status(500).send('Error while removing');
|
||||
return res.status(200).send();
|
||||
});
|
||||
} else {
|
||||
// Otherwise, save the brew with updated author list
|
||||
brew.save((err, savedBrew)=>{
|
||||
if(err) throw err;
|
||||
return res.status(200).send(savedBrew);
|
||||
});
|
||||
}
|
||||
});
|
||||
return await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||
};
|
||||
|
||||
const newGoogleBrew = async (req, res, next)=>{
|
||||
let oAuth2Client;
|
||||
|
||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||
|
||||
const newBrew = async (req, res)=>{
|
||||
const brew = req.body;
|
||||
|
||||
if(!brew.title) {
|
||||
brew.title = getGoodBrewTitle(brew.text);
|
||||
}
|
||||
|
||||
brew.authors = (req.account) ? [req.account.username] : [];
|
||||
brew.text = mergeBrewText(brew);
|
||||
const { transferToGoogle } = req.query;
|
||||
|
||||
delete brew.editId;
|
||||
delete brew.shareId;
|
||||
delete brew.googleId;
|
||||
|
||||
req.body = brew;
|
||||
beforeNewSave(req.account, brew);
|
||||
|
||||
try {
|
||||
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew);
|
||||
return res.status(200).send(newBrew);
|
||||
} catch (err) {
|
||||
return res.status(err.response.status).send(err);
|
||||
let saved;
|
||||
if(transferToGoogle) {
|
||||
saved = await newGoogleBrew(req.account, brew, res)
|
||||
.catch((err)=>{
|
||||
res.status(err.status || err.response.status).send(err.message || err);
|
||||
});
|
||||
} else {
|
||||
saved = await newLocalBrew(brew)
|
||||
.catch((err)=>{
|
||||
res.status(500).send(err);
|
||||
});
|
||||
}
|
||||
if(!saved) return;
|
||||
return res.status(200).send(saved);
|
||||
};
|
||||
|
||||
const updateBrew = async (req, res)=>{
|
||||
let brew = excludePropsFromUpdate(req.body);
|
||||
const { transferToGoogle, transferFromGoogle } = req.query;
|
||||
|
||||
let saved;
|
||||
if(brew.googleId && transferFromGoogle) {
|
||||
beforeNewSave(req.account, brew);
|
||||
|
||||
saved = await newLocalBrew(brew)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(500).send(err);
|
||||
});
|
||||
if(!saved) return;
|
||||
|
||||
await deleteGoogleBrew(req.account, `${brew.googleId}${brew.editId}`, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || err.response.status).send(err.message || err);
|
||||
});
|
||||
} else if(!brew.googleId && transferToGoogle) {
|
||||
saved = await newGoogleBrew(req.account, brew, res)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status || err.response.status).send(err.message || err);
|
||||
});
|
||||
if(!saved) return;
|
||||
|
||||
await deleteLocalBrew(req.account, brew.editId)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.status).send(err.message);
|
||||
});
|
||||
} else if(brew.googleId) {
|
||||
brew.text = mergeBrewText(brew);
|
||||
|
||||
saved = await GoogleActions.updateGoogleBrew(brew)
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
res.status(err.response?.status || 500).send(err);
|
||||
});
|
||||
} else {
|
||||
const dbBrew = await HomebrewModel.get({ editId: req.params.id })
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
return res.status(500).send('Error while saving');
|
||||
});
|
||||
|
||||
brew = _.merge(dbBrew, brew);
|
||||
brew.text = mergeBrewText(brew);
|
||||
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||
// Delete the non-binary text field since it's not needed anymore
|
||||
brew.text = undefined;
|
||||
brew.updatedAt = new Date();
|
||||
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
}
|
||||
|
||||
brew.markModified('authors');
|
||||
brew.markModified('systems');
|
||||
|
||||
saved = await brew.save();
|
||||
}
|
||||
if(!saved) return;
|
||||
|
||||
if(!res.headersSent) return res.status(200).send(saved);
|
||||
};
|
||||
|
||||
const deleteBrew = async (req, res)=>{
|
||||
if(req.params.id.length > 12) {
|
||||
const deleted = await deleteGoogleBrew(req.account, req.params.id, res)
|
||||
.catch((err)=>{
|
||||
res.status(500).send(err);
|
||||
});
|
||||
if(deleted) return res.status(200).send();
|
||||
} else {
|
||||
const deleted = await deleteLocalBrew(req.account, req.params.id)
|
||||
.catch((err)=>{
|
||||
res.status(err.status).send(err.message);
|
||||
});
|
||||
if(deleted) return res.status(200).send(deleted);
|
||||
return res.status(200).send();
|
||||
}
|
||||
};
|
||||
|
||||
const updateGoogleBrew = async (req, res, next)=>{
|
||||
let oAuth2Client;
|
||||
|
||||
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
|
||||
|
||||
const brew = excludePropsFromUpdate(req.body);
|
||||
brew.text = mergeBrewText(brew);
|
||||
|
||||
try {
|
||||
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew);
|
||||
return res.status(200).send(updatedBrew);
|
||||
} catch (err) {
|
||||
return res.status(err.response?.status || 500).send(err);
|
||||
const deleteLocalBrew = async (account, id)=>{
|
||||
const brew = await HomebrewModel.findOne({ editId: id });
|
||||
if(!brew) {
|
||||
throw { status: 404, message: 'Can not find homebrew with that id' };
|
||||
}
|
||||
|
||||
if(account) {
|
||||
// Remove current user as author
|
||||
brew.authors = _.pull(brew.authors, account.username);
|
||||
brew.markModified('authors');
|
||||
}
|
||||
|
||||
if(brew.authors.length === 0) {
|
||||
// Delete brew if there are no authors left
|
||||
await brew.remove()
|
||||
.catch((err)=>{
|
||||
console.error(err);
|
||||
throw { status: 500, message: 'Error while removing' };
|
||||
});
|
||||
} else {
|
||||
// Otherwise, save the brew with updated author list
|
||||
return await brew.save()
|
||||
.catch((err)=>{
|
||||
throw { status: 500, message: err };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteGoogleBrew = async (account, id, res)=>{
|
||||
const auth = await GoogleActions.authCheck(account, res);
|
||||
await GoogleActions.deleteGoogleBrew(auth, id);
|
||||
return true;
|
||||
};
|
||||
|
||||
router.post('/api', newBrew);
|
||||
router.post('/api/newGoogle/', newGoogleBrew);
|
||||
router.put('/api/:id', updateBrew);
|
||||
router.put('/api/update/:id', updateBrew);
|
||||
router.put('/api/updateGoogle/:id', updateGoogleBrew);
|
||||
router.delete('/api/:id', deleteBrew);
|
||||
router.get('/api/remove/:id', deleteBrew);
|
||||
router.get('/api/removeGoogle/:id', (req, res)=>{GoogleActions.deleteGoogleBrew(req, res, req.params.id);});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
const jwt = require('jwt-simple');
|
||||
|
||||
// Load configuration values
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true }) // Load environment variables
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
const config = require('./config.js');
|
||||
|
||||
// Generate an Access Token for the given User ID
|
||||
const generateAccessToken = (account)=>{
|
||||
|
||||
@@ -7,6 +7,7 @@ const cx = require('classnames');
|
||||
const DISMISS_KEY = 'dismiss_render_warning';
|
||||
|
||||
const RenderWarnings = createClass({
|
||||
displayName : 'RenderWarnings',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
warnings : {}
|
||||
|
||||
@@ -27,9 +27,9 @@ if(typeof navigator !== 'undefined'){
|
||||
require('codemirror/addon/search/matchesonscrollbar.js');
|
||||
require('codemirror/addon/dialog/dialog.js');
|
||||
//Trailing space highlighting
|
||||
require('codemirror/addon/edit/trailingspace.js');
|
||||
// require('codemirror/addon/edit/trailingspace.js');
|
||||
//Active line highlighting
|
||||
require('codemirror/addon/selection/active-line.js');
|
||||
// require('codemirror/addon/selection/active-line.js');
|
||||
//Auto-closing
|
||||
//XML code folding is a requirement of the auto-closing tag feature and is not enabled
|
||||
require('codemirror/addon/fold/xml-fold.js');
|
||||
@@ -40,12 +40,14 @@ if(typeof navigator !== 'undefined'){
|
||||
}
|
||||
|
||||
const CodeEditor = createClass({
|
||||
displayName : 'CodeEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{}
|
||||
language : '',
|
||||
value : '',
|
||||
wrap : true,
|
||||
onChange : ()=>{},
|
||||
enableFolding : true
|
||||
};
|
||||
},
|
||||
|
||||
@@ -81,6 +83,12 @@ const CodeEditor = createClass({
|
||||
} else if(this.codeMirror?.getValue() != this.props.value) { //update editor contents if brew.text is changed from outside
|
||||
this.codeMirror.setValue(this.props.value);
|
||||
}
|
||||
|
||||
if(this.props.enableFolding) {
|
||||
this.codeMirror.setOption('foldOptions', this.foldOptions(this.codeMirror));
|
||||
} else {
|
||||
this.codeMirror.setOption('foldOptions', false);
|
||||
}
|
||||
},
|
||||
|
||||
buildEditor : function() {
|
||||
@@ -139,39 +147,19 @@ const CodeEditor = createClass({
|
||||
'Ctrl-]' : this.unfoldAllCode,
|
||||
'Cmd-]' : this.unfoldAllCode
|
||||
},
|
||||
foldGutter : true,
|
||||
foldOptions : {
|
||||
scanUp : true,
|
||||
rangeFinder : CodeMirror.fold.homebrewery,
|
||||
widget : (from, to)=>{
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
const maxLength = 50;
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
text += this.codeMirror.getLine(currentLine);
|
||||
if(currentLine < to.line)
|
||||
text += ' ';
|
||||
currentLine += 1;
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
if(text.length > maxLength)
|
||||
text = `${text.substr(0, maxLength)}...`;
|
||||
|
||||
return `\u21A4 ${text} \u21A6`;
|
||||
}
|
||||
},
|
||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : true,
|
||||
specialChars : / /,
|
||||
specialCharPlaceholder : function(char) {
|
||||
const el = document.createElement('span');
|
||||
el.className = 'cm-space';
|
||||
el.innerHTML = ' ';
|
||||
return el;
|
||||
}
|
||||
foldGutter : true,
|
||||
foldOptions : this.foldOptions(this.codeMirror),
|
||||
gutters : ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
autoCloseTags : true,
|
||||
styleActiveLine : true,
|
||||
showTrailingSpace : false,
|
||||
// specialChars : / /,
|
||||
// specialCharPlaceholder : function(char) {
|
||||
// const el = document.createElement('span');
|
||||
// el.className = 'cm-space';
|
||||
// el.innerHTML = ' ';
|
||||
// return el;
|
||||
// }
|
||||
});
|
||||
closeTag.autoCloseCurlyBraces(CodeMirror, this.codeMirror);
|
||||
|
||||
@@ -355,6 +343,30 @@ const CodeEditor = createClass({
|
||||
historySize : function(){
|
||||
return this.codeMirror.doc.historySize();
|
||||
},
|
||||
|
||||
foldOptions : function(cm){
|
||||
return {
|
||||
scanUp : true,
|
||||
rangeFinder : CodeMirror.fold.homebrewery,
|
||||
widget : (from, to)=>{
|
||||
let text = '';
|
||||
let currentLine = from.line;
|
||||
const maxLength = 50;
|
||||
while (currentLine <= to.line && text.length <= maxLength) {
|
||||
text += this.codeMirror.getLine(currentLine);
|
||||
if(currentLine < to.line)
|
||||
text += ' ';
|
||||
currentLine += 1;
|
||||
}
|
||||
|
||||
text = text.trim();
|
||||
if(text.length > maxLength)
|
||||
text = `${text.substr(0, maxLength)}...`;
|
||||
|
||||
return `\u21A4 ${text} \u21A6`;
|
||||
}
|
||||
};
|
||||
},
|
||||
//----------------------//
|
||||
|
||||
render : function(){
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-tab {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
}
|
||||
//.cm-tab {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
|
||||
//}
|
||||
|
||||
.cm-trailingspace {
|
||||
.cm-space {
|
||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
}
|
||||
}
|
||||
//.cm-trailingspace {
|
||||
// .cm-space {
|
||||
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAQAgMAAABW5NbuAAAACVBMVEVHcEwAAAAAAAAWawmTAAAAA3RSTlMAPBJ6PMxpAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAFUlEQVQI12NgwACcCQysASAEZGAAACMuAX06aCQUAAAAAElFTkSuQmCC) no-repeat right;
|
||||
// }
|
||||
//}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
|
||||
const Nav = {
|
||||
base : createClass({
|
||||
render : function(){
|
||||
displayName : 'Nav.base',
|
||||
render : function(){
|
||||
return <nav>
|
||||
<div className='navContent'>
|
||||
{this.props.children}
|
||||
@@ -26,7 +27,8 @@ const Nav = {
|
||||
},
|
||||
|
||||
section : createClass({
|
||||
render : function(){
|
||||
displayName : 'Nav.section',
|
||||
render : function(){
|
||||
return <div className='navSection'>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
@@ -34,6 +36,7 @@ const Nav = {
|
||||
}),
|
||||
|
||||
item : createClass({
|
||||
displayName : 'Nav.item',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
icon : null,
|
||||
@@ -69,6 +72,7 @@ const Nav = {
|
||||
}),
|
||||
|
||||
dropdown : createClass({
|
||||
displayName : 'Nav.dropdown',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@import '../styles/colors';
|
||||
@keyframes glideDropDown {
|
||||
0% {transform : translate(0px, -100%);
|
||||
opacity : 0;
|
||||
@@ -49,6 +50,7 @@ nav{
|
||||
}
|
||||
}
|
||||
.navItem{
|
||||
#backgroundColors;
|
||||
.animate(background-color);
|
||||
padding : 8px 12px;
|
||||
cursor : pointer;
|
||||
@@ -58,30 +60,12 @@ nav{
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
text-transform : uppercase;
|
||||
line-height : 13px;
|
||||
i{
|
||||
margin-left : 5px;
|
||||
font-size : 13px;
|
||||
float : right;
|
||||
}
|
||||
&.tealLight:hover{ background-color : @tealLight };
|
||||
&.teal:hover{ background-color : @teal };
|
||||
&.greenLight:hover{ background-color : @greenLight };
|
||||
&.green:hover{ background-color : @green };
|
||||
&.blueLight:hover{ background-color : @blueLight };
|
||||
&.blue:hover{ background-color : @blue };
|
||||
&.purpleLight:hover{ background-color : @purpleLight };
|
||||
&.purple:hover{ background-color : @purple };
|
||||
&.steelLight:hover{ background-color : @steelLight };
|
||||
&.steel:hover{ background-color : @steel };
|
||||
&.yellowLight:hover{ background-color : @yellowLight };
|
||||
&.yellow:hover{ background-color : @yellow };
|
||||
&.orangeLight:hover{ background-color : @orangeLight };
|
||||
&.orange:hover{ background-color : @orange };
|
||||
&.redLight:hover{ background-color : @redLight };
|
||||
&.red:hover{ background-color : @red };
|
||||
&.silverLight:hover{ background-color : @silverLight };
|
||||
&.silver:hover{ background-color : @silver };
|
||||
&.greyLight:hover{ background-color : @greyLight };
|
||||
&.grey:hover{ background-color : @grey };
|
||||
}
|
||||
.navSection:last-child .navItem{
|
||||
border-left : 1px solid #666;
|
||||
|
||||
@@ -5,47 +5,74 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const SplitPane = createClass({
|
||||
displayName : 'SplitPane',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
storageKey : 'naturalcrit-pane-split',
|
||||
onDragFinish : function(){} //fires when dragging
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
size : null,
|
||||
isDragging : false
|
||||
currentDividerPos : null,
|
||||
windowWidth : 0,
|
||||
isDragging : false
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
const paneSize = window.localStorage.getItem(this.props.storageKey);
|
||||
if(paneSize){
|
||||
const dividerPos = window.localStorage.getItem(this.props.storageKey);
|
||||
if(dividerPos){
|
||||
this.setState({
|
||||
size : paneSize
|
||||
currentDividerPos : this.limitPosition(dividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13)),
|
||||
userSetDividerPos : dividerPos,
|
||||
windowWidth : window.innerWidth
|
||||
});
|
||||
}
|
||||
window.addEventListener('resize', this.handleWindowResize);
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
},
|
||||
|
||||
handleWindowResize : function() {
|
||||
// Allow divider to increase in size to last user-set position
|
||||
// Limit current position to between 10% and 90% of visible space
|
||||
const newLoc = this.limitPosition(this.state.userSetDividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13));
|
||||
|
||||
this.setState({
|
||||
currentDividerPos : newLoc,
|
||||
windowWidth : window.innerWidth
|
||||
});
|
||||
},
|
||||
|
||||
limitPosition : function(x, min = 1, max = window.innerWidth - 13) {
|
||||
const result = Math.round(Math.min(max, Math.max(min, x)));
|
||||
return result;
|
||||
},
|
||||
|
||||
handleUp : function(){
|
||||
if(this.state.isDragging){
|
||||
this.props.onDragFinish(this.state.size);
|
||||
window.localStorage.setItem(this.props.storageKey, this.state.size);
|
||||
this.props.onDragFinish(this.state.currentDividerPos);
|
||||
window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
|
||||
}
|
||||
this.setState({ isDragging: false });
|
||||
},
|
||||
|
||||
handleDown : function(){
|
||||
this.setState({ isDragging: true });
|
||||
//this.unFocus()
|
||||
},
|
||||
|
||||
handleMove : function(e){
|
||||
if(!this.state.isDragging) return;
|
||||
|
||||
const minWidth = 1;
|
||||
const maxWidth = window.innerWidth - 13;
|
||||
const newSize = Math.min(maxWidth, Math.max(minWidth, e.pageX));
|
||||
const newSize = this.limitPosition(e.pageX);
|
||||
this.setState({
|
||||
size : newSize
|
||||
currentDividerPos : newSize,
|
||||
userSetDividerPos : newSize
|
||||
});
|
||||
},
|
||||
/*
|
||||
@@ -69,7 +96,7 @@ const SplitPane = createClass({
|
||||
|
||||
render : function(){
|
||||
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
|
||||
<Pane ref='pane1' width={this.state.size}>{this.props.children[0]}</Pane>
|
||||
<Pane ref='pane1' width={this.state.currentDividerPos}>{this.props.children[0]}</Pane>
|
||||
{this.renderDivider()}
|
||||
<Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
|
||||
</div>;
|
||||
@@ -77,6 +104,7 @@ const SplitPane = createClass({
|
||||
});
|
||||
|
||||
const Pane = createClass({
|
||||
displayName : 'Pane',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
width : null
|
||||
|
||||
@@ -20,4 +20,27 @@
|
||||
@silverLight : #ECF0F1;
|
||||
@silver : #BDC3C7;
|
||||
@greyLight : #95A5A6;
|
||||
@grey : #7F8C8D;
|
||||
@grey : #7F8C8D;
|
||||
|
||||
#backgroundColors {
|
||||
&.tealLight:hover{ background-color : @tealLight };
|
||||
&.teal:hover{ background-color : @teal };
|
||||
&.greenLight:hover{ background-color : @greenLight };
|
||||
&.green:hover{ background-color : @green };
|
||||
&.blueLight:hover{ background-color : @blueLight };
|
||||
&.blue:hover{ background-color : @blue };
|
||||
&.purpleLight:hover{ background-color : @purpleLight };
|
||||
&.purple:hover{ background-color : @purple };
|
||||
&.steelLight:hover{ background-color : @steelLight };
|
||||
&.steel:hover{ background-color : @steel };
|
||||
&.yellowLight:hover{ background-color : @yellowLight };
|
||||
&.yellow:hover{ background-color : @yellow };
|
||||
&.orangeLight:hover{ background-color : @orangeLight };
|
||||
&.orange:hover{ background-color : @orange };
|
||||
&.redLight:hover{ background-color : @redLight };
|
||||
&.red:hover{ background-color : @red };
|
||||
&.silverLight:hover{ background-color : @silverLight };
|
||||
&.silver:hover{ background-color : @silver };
|
||||
&.greyLight:hover{ background-color : @greyLight };
|
||||
&.grey:hover{ background-color : @grey };
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
const test = require('pico-check');
|
||||
|
||||
test('Just setting up a spot for future tests', (t)=>{
|
||||
t.pass();
|
||||
});
|
||||
|
||||
module.exports = test;
|
||||
15
tests/markdown/basic.test.js
Normal file
15
tests/markdown/basic.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Escapes <script> tag', function() {
|
||||
const source = '<script></script>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toMatch('<script></script>');
|
||||
});
|
||||
|
||||
test('Processes the markdown within an HTML block if its just a class wrapper', function() {
|
||||
const source = '<div>*Bold text*</div>';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<div> <p><em>Bold text</em></p>\n </div>');
|
||||
});
|
||||
128
tests/markdown/mustache-span.test.js
Normal file
128
tests/markdown/mustache-span.test.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
test('Renders a mustache span with text only', function() {
|
||||
const source = '{{ text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block ">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text only, but with spaces', function() {
|
||||
const source = '{{ this is a text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block ">this is a text</span>');
|
||||
});
|
||||
|
||||
test('Renders an empty mustache span', function() {
|
||||
const source = '{{}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with just a space', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with a few spaces only', function() {
|
||||
const source = '{{ }}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block "></span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and class', function() {
|
||||
const source = '{{my-class text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two classes', function() {
|
||||
const source = '{{my-class,my-class2 text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class my-class2" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text with spaces and class', function() {
|
||||
const source = '{{my-class this is a text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have those two extra spaces after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block my-class" >this is a text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and id', function() {
|
||||
const source = '{{#my-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: why do we have that one extra space after closing "?
|
||||
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two ids', function() {
|
||||
const source = '{{#my-span,#my-favorite-span text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: do we need to report an error here somehow?
|
||||
expect(rendered).toBe('<span class="inline-block " id="my-span" >text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and css property', function() {
|
||||
const source = '{{color:red text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="color:red;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two css properties', function() {
|
||||
const source = '{{color:red,padding:5px text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="color:red; padding:5px;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and css property which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms" text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
// FIXME: is it correct to remove quotes surrounding css property value?
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text and two css properties which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms",padding:"5px 10px" text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms; padding:5px 10px;">text</span>');
|
||||
});
|
||||
|
||||
|
||||
test('Renders a mustache span with text with quotes and css property which contains quotes', function() {
|
||||
const source = '{{font:"trebuchet ms" text "with quotes"}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block " style="font:trebuchet ms;">text “with quotes”</span>');
|
||||
});
|
||||
|
||||
test('Renders a mustache span with text, id, class and a couple of css properties', function() {
|
||||
const source = '{{pen,#author,color:orange,font-family:"trebuchet ms" text}}';
|
||||
const rendered = Markdown.render(source);
|
||||
expect(rendered).toBe('<span class="inline-block pen" id="author" style="color:orange; font-family:trebuchet ms;">text</span>');
|
||||
});
|
||||
|
||||
// TODO: add tests for ID with accordance to CSS spec:
|
||||
//
|
||||
// From https://drafts.csswg.org/selectors/#id-selectors:
|
||||
//
|
||||
// > An ID selector consists of a “number sign” (U+0023, #) immediately followed by the ID value, which must be a CSS identifier.
|
||||
//
|
||||
// From: https://www.w3.org/TR/CSS21/syndata.html#value-def-identifier:
|
||||
//
|
||||
// > In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9]
|
||||
// > and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_);
|
||||
// > they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
|
||||
// > Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item).
|
||||
// > For instance, the identifier "B&W?" may be written as "B\&W\?" or "B\26 W\3F".
|
||||
// > Note that Unicode is code-by-code equivalent to ISO 10646 (see [UNICODE] and [ISO10646]).
|
||||
|
||||
// TODO: add tests for class with accordance to CSS spec:
|
||||
//
|
||||
// From: https://drafts.csswg.org/selectors/#class-html:
|
||||
//
|
||||
// > The class selector is given as a full stop (. U+002E) immediately followed by an identifier.
|
||||
|
||||
27
tests/routes/static-pages.test.js
Normal file
27
tests/routes/static-pages.test.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const supertest = require('supertest');
|
||||
|
||||
// Mimic https responses to avoid being redirected all the time
|
||||
const app = supertest.agent(require('app.js').app)
|
||||
.set('X-Forwarded-Proto', 'https');
|
||||
|
||||
describe('Tests for static pages', ()=>{
|
||||
it('Home page works', ()=>{
|
||||
return app.get('/').expect(200);
|
||||
});
|
||||
|
||||
it('Home page v3 works', ()=>{
|
||||
return app.get('/v3_preview').expect(200);
|
||||
});
|
||||
|
||||
it('Changelog page works', ()=>{
|
||||
return app.get('/changelog').expect(200);
|
||||
});
|
||||
|
||||
it('FAQ page works', ()=>{
|
||||
return app.get('/faq').expect(200);
|
||||
});
|
||||
|
||||
it('robots.txt works', ()=>{
|
||||
return app.get('/robots.txt').expect(200);
|
||||
});
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
//Set up configs and DB connectiosna nd what not in here
|
||||
@@ -621,6 +621,8 @@ body {
|
||||
}
|
||||
&.decoration {
|
||||
transform-style : preserve-3d;
|
||||
z-index: -1;
|
||||
position:relative;
|
||||
}
|
||||
&.decoration::before {
|
||||
content :'';
|
||||
@@ -656,7 +658,7 @@ body {
|
||||
margin-bottom : 0.3cm;
|
||||
}
|
||||
a{
|
||||
display : table;
|
||||
display : inline;
|
||||
color : inherit;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
|
||||
BIN
themes/assets/discord.png
Normal file
BIN
themes/assets/discord.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
1
themes/assets/discordOfManyThings.svg
Normal file
1
themes/assets/discordOfManyThings.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg id="Layer_3" data-name="Layer 3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 350.028 390.552"><defs><style>.cls-1{opacity:0.9;}.cls-2{fill:#001013;}.cls-3{fill:#fff;}.cls-4{fill:#aa2a29;}</style></defs><title>Discord of Many Things blank</title><g class="cls-1"><path d="M100.991,82.743c-1.649,3.324-3.395,5.768-4.1,8.482-1.49,5.753-3.44,11.644-3.455,17.482q-.332,125.259-.141,250.519c.007,20.122,12.708,36.368,31.407,40.239a38.328,38.328,0,0,0,7.722.761q103.611,0,207.222-.088h6.314c-3.256-11.462-9.664-34.015-9.664-34.015l2.431,2.023s41.561,37.755,62.33,56.6c2.489,2.26,3.694,4.462,3.593,7.885-.241,8.186-.078,16.383-.078,25.492-6.333-5.527-11.928-10.35-17.457-15.247-9.586-8.493-19.2-16.957-28.644-25.6a10.032,10.032,0,0,0-7.39-2.774q-117.228-.1-234.457-.334c-20.632-.031-33.959-9.189-41.021-28.578-.852-2.339-.783-5.091-.784-7.653q-.057-116.5-.031-233.007V119.145C74.791,102.517,84.2,89.068,100.991,82.743Z" transform="translate(-74.79 -67.573)"/></g><path class="cls-2" d="M338.234,371.589s5.556,19.924,8.513,30.53h-5.075c-69.335,0-138.67-.1-208,.074-16.925.043-35.116-9.515-40.505-33.247a26.538,26.538,0,0,1-.379-5.827q-.027-128.172.025-256.344c.021-19.276,15.226-36.749,34.427-38.688a82.669,82.669,0,0,1,8.279-.447q123.288-.034,246.578-.067c13.265-.016,24.7,3.79,33.278,14.235a39.273,39.273,0,0,1,9.447,25.5q-.015,167.477-.012,334.955c0,1.245-.1,2.49-.2,4.645-4.075-3.565-7.6-6.552-11.018-9.655q-35.236-31.992-70.44-64.017c-1.2-1.091-3.713-3.147-3.713-3.147l-2-1.775Z" transform="translate(-74.79 -67.573)"/><path class="cls-3" d="M419.838,435.934c-7.848-6.892-15.026-12.977-21.961-19.329q-28.235-25.86-56.253-51.954c-2.314-2.157-4.59-3.247-7.356-1.784-3.036,1.607-2.77,4.676-2.022,7.359,2.286,8.2,4.817,16.338,7.24,24.5a17.108,17.108,0,0,1,.294,1.851c-1.486.084-2.872.233-4.258.233-67.363.012-134.726-.155-202.088.123-18.267.075-32.955-13.095-34.769-29.353a92.257,92.257,0,0,1-.886-10.108Q97.7,233.488,97.793,109.5c.013-10.518,3.19-20.063,11.156-27.471a31.92,31.92,0,0,1,20.439-8.917c1.948-.109,3.9-.181,5.852-.181q123.5-.014,247-.063c9.957-.012,18.894,2.145,26.329,9.1,7.956,7.441,11.177,16.934,11.214,27.457.142,40.84.057,81.681.057,122.522V435.934Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M311.029,170.975c-11.8,45.005-23.315,88.959-35.123,134.016l-97.168-98.883Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M380.8,265.712l-95.97,40.316c11.438-43.808,22.639-86.709,33.839-129.609l1.08-.272Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.3,322.955c2.91-36.463,5.761-72.179,8.709-109.116l94.711,96.472Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M223.458,107.926l83.127,55.063L178.71,196.919Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M366.285,281.366l-72.5,73.414c-3.524-13.182-6.849-25.617-10.306-38.549Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M274.874,318.478c3.445,12.8,6.765,25.143,10.376,38.559l-98.635-26.556.032-1.153Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M374.888,241.261l-51.71-75.57,26.8-21.529,25.831,96.619Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M165.492,207.523c-2.39,30.748-4.736,60.942-7.083,91.135l-1.063.159c-8.5-31.936-17-63.872-25.705-96.581Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M317.039,159.316l-68.251-45.24.307-.794,93.794,25.212Z" transform="translate(-74.79 -67.573)"/><path class="cls-4" d="M136.517,193.952,203.5,125.03l1.086.832L167.794,198.9Z" transform="translate(-74.79 -67.573)"/></svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
BIN
themes/assets/github.png
Normal file
BIN
themes/assets/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
BIN
themes/assets/patreon.png
Normal file
BIN
themes/assets/patreon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
themes/assets/reddit.png
Normal file
BIN
themes/assets/reddit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
Reference in New Issue
Block a user