mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-23 18:43:02 +00:00
Compare commits
248 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f754ecd6c3 | ||
|
|
62a827ce49 | ||
|
|
e971da2b59 | ||
|
|
2d092cb290 | ||
|
|
5b66ecb06f | ||
|
|
22a9799674 | ||
|
|
7c813be13a | ||
|
|
e94148b2f0 | ||
|
|
ec4f6e4327 | ||
|
|
fcbd117784 | ||
|
|
dab716a9e0 | ||
|
|
9265e25c73 | ||
|
|
4ad63d5bce | ||
|
|
f89c897488 | ||
|
|
521ff5e7e3 | ||
|
|
89d8cb3b0a | ||
|
|
a0e92b54d0 | ||
|
|
62f549f038 | ||
|
|
e8f3b0c8d0 | ||
|
|
587ce78f4e | ||
|
|
7ca1dd3c68 | ||
|
|
58543f0b4d | ||
|
|
e88253f364 | ||
|
|
bdf37d8fe7 | ||
|
|
22908207a3 | ||
|
|
7239b89108 | ||
|
|
0ea80bd758 | ||
|
|
f6d623ace3 | ||
|
|
63ad8b3411 | ||
|
|
50cc757a5c | ||
|
|
70430f84e1 | ||
|
|
80db261c88 | ||
|
|
5631ef7be7 | ||
|
|
2745a4d6c1 | ||
|
|
33190b5c89 | ||
|
|
354a5832e4 | ||
|
|
f57c0f0886 | ||
|
|
954a393fce | ||
|
|
01dbac78ce | ||
|
|
594ea8ab59 | ||
|
|
033493a31b | ||
|
|
e79b099633 | ||
|
|
5bc948ab0a | ||
|
|
28c5d7d84a | ||
|
|
825702ee1d | ||
|
|
61b58032ca | ||
|
|
546cc13c1c | ||
|
|
44649d7f51 | ||
|
|
5f9aaba262 | ||
|
|
5d5c0b9773 | ||
|
|
b681edba23 | ||
|
|
f0c412527b | ||
|
|
fcf6b8d764 | ||
|
|
8607b9dba8 | ||
|
|
ef4fa89d9a | ||
|
|
6d38a633ef | ||
|
|
b3376435b9 | ||
|
|
b9cfc2e6af | ||
|
|
3259836964 | ||
|
|
b3387c363f | ||
|
|
f0d9fcf942 | ||
|
|
928b553b19 | ||
|
|
508f87f117 | ||
|
|
03b389761b | ||
|
|
36d0f15960 | ||
|
|
b56d4fb773 | ||
|
|
272b336cd8 | ||
|
|
ac58833adf | ||
|
|
c3432a9263 | ||
|
|
f8f1c99266 | ||
|
|
0e9b50d4e8 | ||
|
|
e6e995d7f4 | ||
|
|
c9d18be0cb | ||
|
|
b75063f936 | ||
|
|
6ef88e0f1f | ||
|
|
59e0118d8b | ||
|
|
09e6766e0d | ||
|
|
ce8cfde211 | ||
|
|
7fbb51b3f4 | ||
|
|
1e9b8e679d | ||
|
|
53a1c4f85d | ||
|
|
b1c252495b | ||
|
|
5fce35edd7 | ||
|
|
debe58ff0b | ||
|
|
0018627f82 | ||
|
|
e50f0a1f3b | ||
|
|
e862f65166 | ||
|
|
347575b0ec | ||
|
|
4ac922482e | ||
|
|
27e7af870a | ||
|
|
474b2552fd | ||
|
|
a06b29c6f5 | ||
|
|
4128670a9f | ||
|
|
94d090277f | ||
|
|
0b8889d0b8 | ||
|
|
2efb24d692 | ||
|
|
bc81e09686 | ||
|
|
aeffec1763 | ||
|
|
462a5608d2 | ||
|
|
09ae750eec | ||
|
|
e8dcb042f8 | ||
|
|
ecd25ca49f | ||
|
|
3e8551bad6 | ||
|
|
ef325e2617 | ||
|
|
81c361bfb8 | ||
|
|
37eb0d0889 | ||
|
|
8adc04a565 | ||
|
|
486841084f | ||
|
|
399a6d82f6 | ||
|
|
becc6b8df0 | ||
|
|
1ff3f96f6c | ||
|
|
b289cb1003 | ||
|
|
3ea3d273a5 | ||
|
|
a007c5f85f | ||
|
|
06d970e61a | ||
|
|
f28ed3d52e | ||
|
|
c5867dab91 | ||
|
|
df4bacf890 | ||
|
|
6a57542216 | ||
|
|
72db7fedfb | ||
|
|
892a5f9f1e | ||
|
|
2839846ec0 | ||
|
|
502ef6ad7c | ||
|
|
5b8aa5bb19 | ||
|
|
a26e828f00 | ||
|
|
f572e671cf | ||
|
|
d18bd500b1 | ||
|
|
14f721d209 | ||
|
|
1219f64cb3 | ||
|
|
b82aac4a5a | ||
|
|
b53b5ccf43 | ||
|
|
9e7981f05c | ||
|
|
18a238786e | ||
|
|
507f8e0852 | ||
|
|
0c70162a78 | ||
|
|
077511dfa7 | ||
|
|
69b25eb03a | ||
|
|
a56a999920 | ||
|
|
c1d8796807 | ||
|
|
71af97e489 | ||
|
|
facbc5f6dc | ||
|
|
63a1ff454f | ||
|
|
c634192289 | ||
|
|
bf21c3d351 | ||
|
|
52c0462a4f | ||
|
|
1184fe86a5 | ||
|
|
7656e53606 | ||
|
|
4241052952 | ||
|
|
21b83ead88 | ||
|
|
448ea5cf5c | ||
|
|
20b719d0de | ||
|
|
8b04cc9269 | ||
|
|
96466211c7 | ||
|
|
34741291c7 | ||
|
|
fcb0cd8ee7 | ||
|
|
aafac16af2 | ||
|
|
911d1d4f9c | ||
|
|
2cd7b44e12 | ||
|
|
bfccd1d9e4 | ||
|
|
bf1bf6c191 | ||
|
|
cc5cb677a1 | ||
|
|
2dcd7101f3 | ||
|
|
f4e7e46a04 | ||
|
|
8b3b7cb5aa | ||
|
|
77081b39b4 | ||
|
|
a06236e3ff | ||
|
|
c8585775be | ||
|
|
439cdfa1ea | ||
|
|
4c36f254c4 | ||
|
|
b6815593fd | ||
|
|
84054d1aae | ||
|
|
24aec1c649 | ||
|
|
8caae18a12 | ||
|
|
3532d75365 | ||
|
|
ea81bb5ebc | ||
|
|
8d217f8785 | ||
|
|
d35db1f702 | ||
|
|
f6b058f3c9 | ||
|
|
5ab7c29b9d | ||
|
|
c5ecb9d57d | ||
|
|
d0c473878a | ||
|
|
796df9a1ac | ||
|
|
9e8e403195 | ||
|
|
a369871a06 | ||
|
|
f2f45f3657 | ||
|
|
e86ce5cf06 | ||
|
|
8d73ff6833 | ||
|
|
c4397d34f8 | ||
|
|
66c0c96a4f | ||
|
|
838b64c589 | ||
|
|
730dde730c | ||
|
|
d867aa7ce1 | ||
|
|
761597e71f | ||
|
|
4bde5fcbf8 | ||
|
|
ff0aa56ddc | ||
|
|
74f92f3e44 | ||
|
|
84285f7359 | ||
|
|
ec0de7a408 | ||
|
|
9ea99236ff | ||
|
|
f806328e75 | ||
|
|
15ac397b63 | ||
|
|
b8105eb147 | ||
|
|
31cbd9ef40 | ||
|
|
eaab6de691 | ||
|
|
d417c76c56 | ||
|
|
2f15cc5611 | ||
|
|
eb08172fb1 | ||
|
|
0cc87a4f0f | ||
|
|
004dc79eb2 | ||
|
|
a8a70c2d70 | ||
|
|
825c259fba | ||
|
|
cbbb7292d9 | ||
|
|
9519a0b4e4 | ||
|
|
9dd16b6dd5 | ||
|
|
d693301c37 | ||
|
|
fd5d142c16 | ||
|
|
ee63d2d857 | ||
|
|
7c3946fb03 | ||
|
|
3e69e8c1aa | ||
|
|
1a0bc1952c | ||
|
|
7e2c3381ae | ||
|
|
4f4ef908e0 | ||
|
|
3fe2360d92 | ||
|
|
13a2a7efd2 | ||
|
|
073fb73bde | ||
|
|
847615ef8e | ||
|
|
3583c2e776 | ||
|
|
2a753ccc7c | ||
|
|
55c529473a | ||
|
|
164f646e08 | ||
|
|
6f4962926c | ||
|
|
f18a181e2e | ||
|
|
c4bff6afa0 | ||
|
|
63e8a0d9b7 | ||
|
|
ce3fda683b | ||
|
|
9594b73b0d | ||
|
|
78ad4bea09 | ||
|
|
ed1b5252be | ||
|
|
bf27250990 | ||
|
|
bab11d692e | ||
|
|
3350b04f64 | ||
|
|
5ebba25183 | ||
|
|
8f77ac9e56 | ||
|
|
3f728e7993 | ||
|
|
3f22572f98 | ||
|
|
a52f628fdf | ||
|
|
fe63133d7c | ||
|
|
3247cab214 |
7
.bithoundrc
Normal file
7
.bithoundrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"unused-ignores": [
|
||||
"react-dom"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,8 @@ version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:8.9
|
||||
- image: circleci/mongo:3.4.4
|
||||
- image: circleci/node:12.16.3
|
||||
- image: circleci/mongo:3.4-jessie
|
||||
|
||||
working_directory: ~/repo
|
||||
|
||||
@@ -30,4 +30,3 @@ jobs:
|
||||
|
||||
# run tests!
|
||||
- run: npm run circleci
|
||||
|
||||
|
||||
6
.dockerignore
Normal file
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
tests
|
||||
10
.eslintrc.js
10
.eslintrc.js
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
root : true,
|
||||
parserOptions : {
|
||||
ecmaVersion : 9,
|
||||
sourceType : 'module',
|
||||
@@ -9,6 +9,7 @@ module.exports = {
|
||||
},
|
||||
env : {
|
||||
browser : true,
|
||||
node : true
|
||||
},
|
||||
plugins : ['react'],
|
||||
rules : {
|
||||
@@ -44,11 +45,10 @@ module.exports = {
|
||||
'arrow-parens' : ['warn', 'always'],
|
||||
'brace-style' : ['warn', '1tbs', { allowSingleLine: true }],
|
||||
'jsx-quotes' : ['warn', 'prefer-single'],
|
||||
'linebreak-style' : ['warn', 'unix'],
|
||||
'no-var' : 'warn',
|
||||
'prefer-const' : 'warn',
|
||||
'prefer-template' : 'warn',
|
||||
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true } ],
|
||||
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true }],
|
||||
'semi' : ['warn', 'always'],
|
||||
|
||||
/** Whitespace **/
|
||||
@@ -67,7 +67,7 @@ module.exports = {
|
||||
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
|
||||
singleLine : { beforeColon: false, afterColon: true }
|
||||
}],
|
||||
'linebreak-style' : ['warn', 'unix'],
|
||||
'linebreak-style' : 'off',
|
||||
'no-trailing-spaces' : 'warn',
|
||||
'no-whitespace-before-property' : 'warn',
|
||||
'object-curly-spacing' : ['warn', 'always'],
|
||||
@@ -75,4 +75,4 @@ module.exports = {
|
||||
'space-in-parens' : ['warn', 'never'],
|
||||
'template-curly-spacing' : ['warn', 'never'],
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,4 +7,6 @@ storage
|
||||
build/*
|
||||
config/local.*
|
||||
|
||||
todo.md
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:8
|
||||
|
||||
ENV NODE_ENV=docker
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy package.json into the image, then run yarn install
|
||||
# This improves caching so we don't have to download the dependencies every time the code changes
|
||||
COPY package.json ./
|
||||
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
|
||||
RUN yarn install --ignore-scripts
|
||||
|
||||
# Bundle app source and build application
|
||||
COPY . .
|
||||
RUN yarn build
|
||||
|
||||
EXPOSE 8000
|
||||
CMD [ "yarn", "start" ]
|
||||
12
README.DOCKER.md
Normal file
12
README.DOCKER.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Running Homebrewery via Docker
|
||||
|
||||
The repo includes a Dockerfile and a docker-compose.yml file.
|
||||
|
||||
To run the application via docker-compose.yml:
|
||||
`docker-compose up -d`
|
||||
|
||||
To stop the application:
|
||||
`docker-compose down`
|
||||
|
||||
To stop the application and remove all data:
|
||||
`docker-compose down -v`
|
||||
53
README.md
53
README.md
@@ -1,33 +1,52 @@
|
||||
# The Homebrewery
|
||||
The Homebrewery is a tool for making authnetic looking [D&D content](http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook) using only [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). Check it out [here](http://homebrewery.naturalcrit.com).
|
||||
The Homebrewery is a tool for making authentic looking [D&D content](https://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook) using [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). It is distributed under the terms of the [MIT License](./license).
|
||||
|
||||
## Quick Start
|
||||
The easiest way to get started using the Homebrewery is to use it [on our website](https://homebrewery.naturalcrit.com). 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 your own local version for testing by following the installation instructions below.
|
||||
|
||||
### issues, suggestions, bugs
|
||||
If you run into any issues using The Homebrewery, please submit an issues [here](/issues)
|
||||
### Installation
|
||||
First, install two programs that the Homebrewery requires to run.
|
||||
|
||||
|
||||
### local dev
|
||||
Homebrewery is open source, so feel free to clone it, tinker with it, or run your own local version.
|
||||
|
||||
#### pre-reqs
|
||||
1. install [node](https://nodejs.org/en/)
|
||||
1. install [mongodb](https://www.mongodb.com/)
|
||||
|
||||
#### getting started
|
||||
1. clone it
|
||||
Second, download a copy of the repository. If you have git you can do so with
|
||||
```
|
||||
git clone https://github.com/naturalcrit/homebrewery.git
|
||||
```
|
||||
|
||||
Third, you will need to add the environment variable `NODE_ENV = local` to allow 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`
|
||||
|
||||
Fourth, you will need to install the program and run it using the two commands:
|
||||
|
||||
1. `npm install`
|
||||
1. `npm build`
|
||||
1. `npm start`
|
||||
|
||||
#### standalone PHB stylesheet
|
||||
If you just want the stylesheet that is generated to make pages look like they are from the PLayer's Handbook, you have find it [here](https://github.com/stolksdorf/homebrewery/blob/master/phb.standalone.css)
|
||||
You should now be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use the Homebrewery offline.
|
||||
|
||||
### Running the application via Docker
|
||||
|
||||
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
||||
|
||||
### Standalone PHB Stylesheet
|
||||
If you just want the stylesheet that is generated to make pages look like they are from the Player's Handbook, you will find it in the [phb.standalone.css](./phb.standalone.css) file.
|
||||
|
||||
If you are developing locally and would like to generate your own, follow the above steps and then run `npm run phb`.
|
||||
|
||||
### changelog
|
||||
## Issues, Suggestions, and Bugs
|
||||
If you run into any issues using The Homebrewery or have suggestions for improvement, please submit an issue [on GitHub](/issues). You can also get help for issues on the subreddit [r/homebrewery](https://www.reddit.com/r/homebrewery)
|
||||
|
||||
You can check out the changelog [here](https://github.com/stolksdorf/homebrewery/blob/master/changelog.md)
|
||||
## Changelog
|
||||
|
||||
### license
|
||||
You can check out the [changelog](./changelog.md).
|
||||
|
||||
This project is licensed under [MIT](./license)
|
||||
## License
|
||||
|
||||
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, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
27
changelog.md
27
changelog.md
@@ -1,8 +1,31 @@
|
||||
# changelog
|
||||
|
||||
### Saturday, 22/04/217 - v2.7.4
|
||||
- Give ability to hide the render warning notification
|
||||
### Wednesday, 11/03/2020 - v2.8.2
|
||||
- Fixed delete button removing everyone's copy for brews with multiple authors
|
||||
- Compressed homebrew text in database
|
||||
|
||||
### Monday, 26/11/2018 - v2.8.1
|
||||
- Fixed some SSL issues with images in the example page so they appear now
|
||||
- Fixed duplicate scrollbars in Edit Page
|
||||
- Fixed issue of being unable to change brew metadata
|
||||
- Sanitized script tags-javascript typed into the editor was crashing brews
|
||||
|
||||
### Sunday, 08/04/2018 - v2.8.0
|
||||
- Re-enabled box shadows for PDF output
|
||||
- Added a "contributing guide" for the GitHub
|
||||
- "Report Issue" navbar button now links to the subreddit
|
||||
- Refactored background code
|
||||
|
||||
### Sunday, 04/06/2017 - v2.7.5
|
||||
- Fixed the class feature snippet duplicating the entire brew
|
||||
- Fixed headers in tables being duplicated
|
||||
- Fixed border-image being scrambled on class tables and descriptive text boxes
|
||||
- Fixed pages going out of sync in large brews, causing them to be rendered off-page
|
||||
- Improved performance in the preview window when scrolling through large brews
|
||||
- Text in the "view source" page now wraps
|
||||
|
||||
### Saturday, 22/04/2017 - v2.7.4
|
||||
- Give ability to hide the render warning notification
|
||||
|
||||
### Friday, 03/03/2017 - v2.7.3
|
||||
- Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews.
|
||||
|
||||
@@ -1,38 +1,37 @@
|
||||
require('./admin.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const HomebrewAdmin = require('./homebrewAdmin/homebrewAdmin.jsx');
|
||||
|
||||
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
||||
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
||||
const BrewCompress = require ('./brewCompress/brewCompress.jsx');
|
||||
const Stats = require('./stats/stats.jsx');
|
||||
|
||||
const Admin = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
url : '',
|
||||
admin_key : '',
|
||||
homebrews : [],
|
||||
};
|
||||
return {};
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return (
|
||||
<div className='admin'>
|
||||
|
||||
<header>
|
||||
<div className='container'>
|
||||
<i className='fa fa-rocket' />
|
||||
naturalcrit admin
|
||||
</div>
|
||||
</header>
|
||||
return <div className='admin'>
|
||||
|
||||
<header>
|
||||
<div className='container'>
|
||||
|
||||
<HomebrewAdmin homebrews={this.props.homebrews} admin_key={this.props.admin_key} />
|
||||
<i className='fa fa-rocket' />
|
||||
homebrewery admin
|
||||
</div>
|
||||
|
||||
|
||||
</header>
|
||||
<div className='container'>
|
||||
<Stats />
|
||||
<hr />
|
||||
<BrewLookup />
|
||||
<hr />
|
||||
<BrewCleanup />
|
||||
<hr />
|
||||
<BrewCompress />
|
||||
</div>
|
||||
);
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -36,4 +36,9 @@ body{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
hr{
|
||||
margin : 30px 0px;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
75
client/admin/brewCleanup/brewCleanup.jsx
Normal file
75
client/admin/brewCleanup/brewCleanup.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
require('./brewCleanup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
|
||||
|
||||
const BrewCleanup = createClass({
|
||||
displayName : 'BrewCleanup',
|
||||
getDefaultProps(){
|
||||
return {};
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
count : 0,
|
||||
|
||||
pending : false,
|
||||
primed : false,
|
||||
err : null
|
||||
};
|
||||
},
|
||||
prime(){
|
||||
this.setState({ pending: true });
|
||||
|
||||
request.get('/admin/cleanup')
|
||||
.then((res)=>this.setState({ count: res.body.count, primed: true }))
|
||||
.catch((err)=>this.setState({ error: err }))
|
||||
.finally(()=>this.setState({ pending: false }));
|
||||
},
|
||||
cleanup(){
|
||||
this.setState({ pending: true });
|
||||
|
||||
request.post('/admin/cleanup')
|
||||
.then((res)=>this.setState({ count: res.body.count }))
|
||||
.catch((err)=>this.setState({ error: err }))
|
||||
.finally(()=>this.setState({ pending: false, primed: false }));
|
||||
},
|
||||
renderPrimed(){
|
||||
if(!this.state.primed) return;
|
||||
|
||||
if(!this.state.count){
|
||||
return <div className='removeBox'>No Matching Brews found.</div>;
|
||||
}
|
||||
return <div className='removeBox'>
|
||||
<button onClick={this.cleanup} className='remove'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
: <span><i className='fa fa-times' /> Remove</span>
|
||||
}
|
||||
</button>
|
||||
<span>Found {this.state.count} Brews that could be removed. </span>
|
||||
</div>;
|
||||
},
|
||||
render(){
|
||||
return <div className='BrewCleanup'>
|
||||
<h2> Brew Cleanup </h2>
|
||||
<p>Removes very short brews to tidy up the database</p>
|
||||
|
||||
<button onClick={this.prime} className='query'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
: 'Query Brews'
|
||||
}
|
||||
</button>
|
||||
{this.renderPrimed()}
|
||||
|
||||
{this.state.error
|
||||
&& <div className='error'>{this.state.error.toString()}</div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewCleanup;
|
||||
10
client/admin/brewCleanup/brewCleanup.less
Normal file
10
client/admin/brewCleanup/brewCleanup.less
Normal file
@@ -0,0 +1,10 @@
|
||||
.BrewCleanup{
|
||||
.removeBox{
|
||||
margin-top: 20px;
|
||||
button{
|
||||
background-color: @red;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
92
client/admin/brewCompress/brewCompress.jsx
Normal file
92
client/admin/brewCompress/brewCompress.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
require('./brewCompress.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
|
||||
|
||||
const BrewCompress = createClass({
|
||||
displayName : 'BrewCompress',
|
||||
getDefaultProps(){
|
||||
return {};
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
count : 0,
|
||||
batchRange : 0,
|
||||
|
||||
pending : false,
|
||||
primed : false,
|
||||
err : null,
|
||||
ids : null
|
||||
};
|
||||
},
|
||||
prime(){
|
||||
this.setState({ pending: true });
|
||||
|
||||
request.get('/admin/finduncompressed')
|
||||
.then((res)=>this.setState({ count: res.body.count, primed: true, ids: res.body.ids }))
|
||||
.catch((err)=>this.setState({ error: err }))
|
||||
.finally(()=>this.setState({ pending: false }));
|
||||
},
|
||||
cleanup(){
|
||||
const brews = this.state.ids;
|
||||
const compressBatches = ()=>{
|
||||
if(brews.length == 0){
|
||||
this.setState({ pending: false, primed: false });
|
||||
return;
|
||||
}
|
||||
const batch = brews.splice(0, 1000); // Process brews in batches of 1000
|
||||
this.setState({ batchRange: this.state.count - brews.length });
|
||||
batch.forEach((id, idx)=>{
|
||||
request.put(`/admin/compress/${id}`)
|
||||
.catch((err)=>this.setState({ error: err }));
|
||||
});
|
||||
setTimeout(compressBatches, 10000); //Wait 10 seconds between batches
|
||||
};
|
||||
|
||||
this.setState({ pending: true });
|
||||
|
||||
compressBatches();
|
||||
},
|
||||
renderPrimed(){
|
||||
if(!this.state.primed) return;
|
||||
|
||||
if(!this.state.count){
|
||||
return <div className='removeBox'>No Matching Brews found.</div>;
|
||||
}
|
||||
return <div className='removeBox'>
|
||||
<button onClick={this.cleanup} className='remove'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
: <span><i className='fa fa-compress' /> compress </span>
|
||||
}
|
||||
</button>
|
||||
{this.state.pending
|
||||
? <span>Compressing {this.state.batchRange} brews. </span>
|
||||
: <span>Found {this.state.count} Brews that could be compressed. </span>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
render(){
|
||||
return <div className='BrewCompress'>
|
||||
<h2> Brew Compression </h2>
|
||||
<p>Compresses the text in brews to binary</p>
|
||||
|
||||
<button onClick={this.prime} className='query'>
|
||||
{this.state.pending
|
||||
? <i className='fa fa-spin fa-spinner' />
|
||||
: 'Query Brews'
|
||||
}
|
||||
</button>
|
||||
{this.renderPrimed()}
|
||||
|
||||
{this.state.error
|
||||
&& <div className='error'>{this.state.error.toString()}</div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewCompress;
|
||||
10
client/admin/brewCompress/brewCompress.less
Normal file
10
client/admin/brewCompress/brewCompress.less
Normal file
@@ -0,0 +1,10 @@
|
||||
.BrewCompress{
|
||||
.removeBox{
|
||||
margin-top: 20px;
|
||||
button{
|
||||
background-color: @red;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
82
client/admin/brewLookup/brewLookup.jsx
Normal file
82
client/admin/brewLookup/brewLookup.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
require('./brewLookup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
const Moment = require('moment');
|
||||
|
||||
|
||||
const BrewLookup = createClass({
|
||||
getDefaultProps() {
|
||||
return {};
|
||||
},
|
||||
getInitialState() {
|
||||
return {
|
||||
query : '',
|
||||
foundBrew : null,
|
||||
searching : false,
|
||||
error : null
|
||||
};
|
||||
},
|
||||
handleChange(e){
|
||||
this.setState({ query: e.target.value });
|
||||
},
|
||||
lookup(){
|
||||
this.setState({ searching: true, error: null });
|
||||
|
||||
request.get(`/admin/lookup/${this.state.query}`)
|
||||
.then((res)=>this.setState({ foundBrew: res.body }))
|
||||
.catch((err)=>this.setState({ error: err }))
|
||||
.finally(()=>this.setState({ searching: false }));
|
||||
},
|
||||
|
||||
renderFoundBrew(){
|
||||
const brew = this.state.foundBrew;
|
||||
return <div className='foundBrew'>
|
||||
<dl>
|
||||
<dt>Title</dt>
|
||||
<dd>{brew.title}</dd>
|
||||
|
||||
<dt>Authors</dt>
|
||||
<dd>{brew.authors.join(', ')}</dd>
|
||||
|
||||
<dt>Edit Link</dt>
|
||||
<dd><a href={`/edit/${brew.editId}`} target='_blank' rel='noopener noreferrer'>/edit/{brew.editId}</a></dd>
|
||||
|
||||
<dt>Share Link</dt>
|
||||
<dd><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>/share/{brew.shareId}</a></dd>
|
||||
|
||||
<dt>Last Updated</dt>
|
||||
<dd>{Moment(brew.updatedAt).fromNow()}</dd>
|
||||
|
||||
<dt>Num of Views</dt>
|
||||
<dd>{brew.views}</dd>
|
||||
</dl>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render(){
|
||||
return <div className='brewLookup'>
|
||||
<h2>Brew Lookup</h2>
|
||||
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id' />
|
||||
<button onClick={this.lookup}>
|
||||
<i className={cx('fa', {
|
||||
'fa-search' : !this.state.searching,
|
||||
'fa-spin fa-spinner' : this.state.searching,
|
||||
})} />
|
||||
</button>
|
||||
|
||||
{this.state.error
|
||||
&& <div className='error'>{this.state.error.toString()}</div>
|
||||
}
|
||||
|
||||
{this.state.foundBrew
|
||||
? this.renderFoundBrew()
|
||||
: <div className='noBrew'>No brew found.</div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewLookup;
|
||||
30
client/admin/brewLookup/brewLookup.less
Normal file
30
client/admin/brewLookup/brewLookup.less
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
.brewLookup{
|
||||
input{
|
||||
height : 33px;
|
||||
margin-bottom : 20px;
|
||||
padding : 0px 10px;
|
||||
font-family : monospace;
|
||||
}
|
||||
button{
|
||||
vertical-align : middle;
|
||||
height : 37px;
|
||||
}
|
||||
dl{
|
||||
@maxItemWidth : 132px;
|
||||
dt{
|
||||
float : left;
|
||||
clear : left;
|
||||
width : @maxItemWidth;
|
||||
text-align : right;
|
||||
&::after {
|
||||
content: " : ";
|
||||
}
|
||||
}
|
||||
dd{
|
||||
height : 1em;
|
||||
margin-left : @maxItemWidth + 6px;
|
||||
padding : 0 0 0.5em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
const Moment = require('moment');
|
||||
|
||||
|
||||
const BrewLookup = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
adminKey : '',
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
query : '',
|
||||
resultBrew : null,
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
handleChange : function(e){
|
||||
this.setState({
|
||||
query : e.target.value
|
||||
});
|
||||
},
|
||||
lookup : function(){
|
||||
this.setState({ searching: true });
|
||||
|
||||
request.get(`/admin/lookup/${this.state.query}`)
|
||||
.query({ admin_key: this.props.adminKey })
|
||||
.end((err, res)=>{
|
||||
this.setState({
|
||||
searching : false,
|
||||
resultBrew : (err ? null : res.body)
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
renderFoundBrew : function(){
|
||||
if(this.state.searching) return <div className='searching'><i className='fa fa-spin fa-spinner' /></div>;
|
||||
if(!this.state.resultBrew) return <div className='noBrew'>No brew found.</div>;
|
||||
|
||||
const brew = this.state.resultBrew;
|
||||
return <div className='brewRow'>
|
||||
<div>{brew.title}</div>
|
||||
<div>{brew.authors.join(', ')}</div>
|
||||
<div><a href={`/edit/${brew.editId}`} target='_blank'>/edit/{brew.editId}</a></div>
|
||||
<div><a href={`/share/${brew.shareId}`} target='_blank'>/share/{brew.shareId}</a></div>
|
||||
<div>{Moment(brew.updatedAt).fromNow()}</div>
|
||||
<div>{brew.views}</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='brewLookup'>
|
||||
<h1>Brew Lookup</h1>
|
||||
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id...' />
|
||||
<button onClick={this.lookup}><i className='fa fa-search'/></button>
|
||||
|
||||
{this.renderFoundBrew()}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewLookup;
|
||||
@@ -1,8 +0,0 @@
|
||||
.brewLookup{
|
||||
height : 200px;
|
||||
input{
|
||||
height : 33px;
|
||||
padding : 0px 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
|
||||
const BrewSearch = createClass({
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
admin_key : ''
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
searchTerm : '',
|
||||
brew : null,
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
search : function(){
|
||||
this.setState({
|
||||
searching : true
|
||||
});
|
||||
|
||||
request.get(`/homebrew/api/search?id=${this.state.searchTerm}`)
|
||||
.query({
|
||||
admin_key : this.props.admin_key,
|
||||
})
|
||||
.end((err, res)=>{
|
||||
console.log(err, res, res.body.brews[0]);
|
||||
this.setState({
|
||||
brew : res.body.brews[0],
|
||||
|
||||
searching : false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleChange : function(e){
|
||||
this.setState({
|
||||
searchTerm : e.target.value
|
||||
});
|
||||
},
|
||||
handleSearchClick : function(){
|
||||
this.search();
|
||||
},
|
||||
|
||||
renderBrew : function(){
|
||||
if(!this.state.brew) return null;
|
||||
return <div className='brew'>
|
||||
<div>Edit id : {this.state.brew.editId}</div>
|
||||
<div>Share id : {this.state.brew.shareId}</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='search'>
|
||||
<input type='text' value={this.state.searchTerm} onChange={this.handleChange} />
|
||||
|
||||
<button onClick={this.handleSearchClick}>Search</button>
|
||||
|
||||
{this.renderBrew()}
|
||||
</div>;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = BrewSearch;
|
||||
@@ -1,171 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
|
||||
const Moment = require('moment');
|
||||
|
||||
|
||||
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
||||
|
||||
|
||||
const HomebrewAdmin = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
admin_key : ''
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
page : 0,
|
||||
count : 20,
|
||||
brewCache : {},
|
||||
total : 0,
|
||||
|
||||
processingOldBrews : false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
fetchBrews : function(page){
|
||||
request.get('/api/search')
|
||||
.query({
|
||||
admin_key : this.props.admin_key,
|
||||
count : this.state.count,
|
||||
page : page
|
||||
})
|
||||
.end((err, res)=>{
|
||||
if(err || !res.body || !res.body.brews) return;
|
||||
this.state.brewCache[page] = res.body.brews;
|
||||
this.setState({
|
||||
brewCache : this.state.brewCache,
|
||||
total : res.body.total,
|
||||
count : res.body.count
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
this.fetchBrews(this.state.page);
|
||||
},
|
||||
|
||||
changePageTo : function(page){
|
||||
if(!this.state.brewCache[page]){
|
||||
this.fetchBrews(page);
|
||||
}
|
||||
this.setState({
|
||||
page : page
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
clearInvalidBrews : function(){
|
||||
request.get('/api/invalid')
|
||||
.query({ admin_key: this.props.admin_key })
|
||||
.end((err, res)=>{
|
||||
if(!confirm(`This will remove ${res.body.count} brews. Are you sure?`)) return;
|
||||
request.get('/api/invalid')
|
||||
.query({ admin_key: this.props.admin_key, do_it: true })
|
||||
.end((err, res)=>{
|
||||
alert('Done!');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
deleteBrew : function(brewId){
|
||||
if(!confirm(`Are you sure you want to delete '${brewId}'?`)) return;
|
||||
request.get(`/api/remove/${brewId}`)
|
||||
.query({ admin_key: this.props.admin_key })
|
||||
.end(function(err, res){
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
handlePageChange : function(dir){
|
||||
this.changePageTo(this.state.page + dir);
|
||||
},
|
||||
|
||||
|
||||
renderPagnination : function(){
|
||||
let outOf;
|
||||
if(this.state.total){
|
||||
outOf = `${this.state.page} / ${Math.round(this.state.total/this.state.count)}`;
|
||||
}
|
||||
return <div className='pagnination'>
|
||||
<i className='fa fa-chevron-left' onClick={()=>this.handlePageChange(-1)}/>
|
||||
{outOf}
|
||||
<i className='fa fa-chevron-right' onClick={()=>this.handlePageChange(1)}/>
|
||||
</div>;
|
||||
},
|
||||
|
||||
|
||||
renderBrews : function(){
|
||||
const brews = this.state.brewCache[this.state.page] || _.times(this.state.count);
|
||||
return _.map(brews, (brew)=>{
|
||||
return <tr className={cx('brewRow', { 'isEmpty': brew.text == 'false' })} key={brew.shareId || brew}>
|
||||
<td><a href={`/edit/${brew.editId}`} target='_blank'>{brew.editId}</a></td>
|
||||
<td><a href={`/share/${brew.shareId}`} target='_blank'>{brew.shareId}</a></td>
|
||||
<td>{Moment(brew.createdAt).fromNow()}</td>
|
||||
<td>{Moment(brew.updatedAt).fromNow()}</td>
|
||||
<td>{Moment(brew.lastViewed).fromNow()}</td>
|
||||
<td>{brew.views}</td>
|
||||
<td>
|
||||
<div className='deleteButton' onClick={()=>this.deleteBrew(brew.editId)}>
|
||||
<i className='fa fa-trash' />
|
||||
</div>
|
||||
</td>
|
||||
</tr>;
|
||||
});
|
||||
},
|
||||
|
||||
renderBrewTable : function(){
|
||||
return <div className='brewTable'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Edit Id</th>
|
||||
<th>Share Id</th>
|
||||
<th>Created At</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Last Viewed</th>
|
||||
<th>Views</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderBrews()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='homebrewAdmin'>
|
||||
|
||||
<BrewLookup adminKey={this.props.admin_key} />
|
||||
|
||||
{/*
|
||||
<h2>
|
||||
Homebrews - {this.state.total}
|
||||
</h2>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{this.renderPagnination()}
|
||||
{this.renderBrewTable()}
|
||||
|
||||
<button className='clearOldButton' onClick={this.clearInvalidBrews}>
|
||||
Clear Old
|
||||
</button>
|
||||
|
||||
<BrewSearch admin_key={this.props.admin_key} />
|
||||
*/}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = HomebrewAdmin;
|
||||
@@ -1,53 +0,0 @@
|
||||
|
||||
.homebrewAdmin{
|
||||
margin-bottom: 80px;
|
||||
.brewTable{
|
||||
table{
|
||||
|
||||
th{
|
||||
padding : 10px;
|
||||
font-weight : 800;
|
||||
}
|
||||
tr:nth-child(even){
|
||||
background-color : fade(@green, 10%);
|
||||
}
|
||||
tr.isEmpty{
|
||||
background-color : fade(@red, 30%);
|
||||
}
|
||||
td{
|
||||
min-width : 100px;
|
||||
padding : 10px;
|
||||
text-align : center;
|
||||
|
||||
&.preview{
|
||||
position : relative;
|
||||
&:hover{
|
||||
.content{
|
||||
display : block;
|
||||
}
|
||||
}
|
||||
.content{
|
||||
position : absolute;
|
||||
display : none;
|
||||
top : 100%;
|
||||
left : 0px;
|
||||
z-index : 1000;
|
||||
max-height : 500px;
|
||||
width : 300px;
|
||||
padding : 30px;
|
||||
background-color : white;
|
||||
font-family : monospace;
|
||||
text-align : left;
|
||||
pointer-events : none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.deleteButton{
|
||||
cursor: pointer;
|
||||
}
|
||||
button.clearOldButton{
|
||||
float : right;
|
||||
}
|
||||
}
|
||||
46
client/admin/stats/stats.jsx
Normal file
46
client/admin/stats/stats.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
require('./stats.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
|
||||
|
||||
const Stats = createClass({
|
||||
displayName : 'Stats',
|
||||
getDefaultProps(){
|
||||
return {};
|
||||
},
|
||||
getInitialState(){
|
||||
return {
|
||||
stats : {
|
||||
totalBrews : 0
|
||||
},
|
||||
fetching : false
|
||||
};
|
||||
},
|
||||
componentDidMount(){
|
||||
this.fetchStats();
|
||||
},
|
||||
fetchStats(){
|
||||
this.setState({ fetching: true });
|
||||
request.get('/admin/stats')
|
||||
.then((res)=>this.setState({ stats: res.body }))
|
||||
.finally(()=>this.setState({ fetching: false }));
|
||||
},
|
||||
render(){
|
||||
return <div className='Stats'>
|
||||
<h2> Stats </h2>
|
||||
<dl>
|
||||
<dt>Total Brew Count</dt>
|
||||
<dd>{this.state.stats.totalBrews}</dd>
|
||||
</dl>
|
||||
|
||||
{this.state.fetching
|
||||
&& <div className='pending'><i className='fa fa-spin fa-spinner' /></div>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Stats;
|
||||
28
client/admin/stats/stats.less
Normal file
28
client/admin/stats/stats.less
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
.Stats{
|
||||
position : relative;
|
||||
.pending{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
left : 0px;
|
||||
height : 100%;
|
||||
width : 100%;
|
||||
background-color : rgba(238,238,238, 0.5);
|
||||
}
|
||||
dl{
|
||||
@maxItemWidth : 132px;
|
||||
dt{
|
||||
float : left;
|
||||
clear : left;
|
||||
width : @maxItemWidth;
|
||||
text-align : right;
|
||||
&::after {
|
||||
content: " : ";
|
||||
}
|
||||
}
|
||||
dd{
|
||||
margin : 0 0 0 @maxItemWidth + 10px;
|
||||
padding : 0 0 0.5em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./brewRenderer.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -8,6 +9,7 @@ const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||
|
||||
//TODO: move to the brew renderer
|
||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
|
||||
|
||||
const PAGE_HEIGHT = 1056;
|
||||
const PPR_THRESHOLD = 50;
|
||||
@@ -27,16 +29,11 @@ const BrewRenderer = createClass({
|
||||
height : 0,
|
||||
isMounted : false,
|
||||
|
||||
usePPR : true,
|
||||
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD,
|
||||
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
height : 0,
|
||||
pageHeight : PAGE_HEIGHT,
|
||||
lastRender : <div></div>,
|
||||
|
||||
componentDidMount : function() {
|
||||
@@ -48,8 +45,6 @@ const BrewRenderer = createClass({
|
||||
},
|
||||
|
||||
componentWillReceiveProps : function(nextProps) {
|
||||
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
|
||||
|
||||
const pages = nextProps.text.split('\\page');
|
||||
this.setState({
|
||||
pages : pages,
|
||||
@@ -58,10 +53,6 @@ const BrewRenderer = createClass({
|
||||
},
|
||||
|
||||
updateSize : function() {
|
||||
setTimeout(()=>{
|
||||
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
|
||||
}, 1);
|
||||
|
||||
this.setState({
|
||||
height : this.refs.main.parentNode.clientHeight,
|
||||
isMounted : true
|
||||
@@ -69,9 +60,10 @@ const BrewRenderer = createClass({
|
||||
},
|
||||
|
||||
handleScroll : function(e){
|
||||
this.setState({
|
||||
viewablePageNumber : Math.floor(e.target.scrollTop / this.pageHeight)
|
||||
});
|
||||
const target = e.target;
|
||||
this.setState((prevState)=>({
|
||||
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
|
||||
}));
|
||||
},
|
||||
|
||||
shouldRender : function(pageText, index){
|
||||
@@ -134,20 +126,27 @@ const BrewRenderer = createClass({
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='brewRenderer'
|
||||
onScroll={this.handleScroll}
|
||||
ref='main'
|
||||
style={{ height: this.state.height }}>
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className='brewRenderer'
|
||||
onScroll={this.handleScroll}
|
||||
ref='main'
|
||||
style={{ height: this.state.height }}>
|
||||
|
||||
<ErrorBar errors={this.props.errors} />
|
||||
<RenderWarnings />
|
||||
<ErrorBar errors={this.props.errors} />
|
||||
<div className='popups'>
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
{this.renderPageInfo()}
|
||||
{this.renderPPRmsg()}
|
||||
</div>;
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
</div>;
|
||||
{this.renderPageInfo()}
|
||||
{this.renderPPRmsg()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -4,29 +4,8 @@
|
||||
position : relative;
|
||||
}
|
||||
.brewRenderer{
|
||||
overflow-y : scroll;
|
||||
.pageInfo{
|
||||
position : absolute;
|
||||
right : 17px;
|
||||
bottom : 0;
|
||||
z-index : 1000;
|
||||
padding : 8px 10px;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
}
|
||||
.ppr_msg{
|
||||
position : absolute;
|
||||
left : 0px;
|
||||
bottom : 0;
|
||||
z-index : 1000;
|
||||
padding : 8px 10px;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
}
|
||||
will-change : transform;
|
||||
overflow-y : scroll;
|
||||
.pages{
|
||||
margin : 30px 0px;
|
||||
&>.phb{
|
||||
@@ -36,4 +15,26 @@
|
||||
box-shadow : 1px 4px 14px #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
.pageInfo{
|
||||
position : absolute;
|
||||
right : 17px;
|
||||
bottom : 0;
|
||||
z-index : 1000;
|
||||
padding : 8px 10px;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
}
|
||||
.ppr_msg{
|
||||
position : absolute;
|
||||
left : 0px;
|
||||
bottom : 0;
|
||||
z-index : 1000;
|
||||
padding : 8px 10px;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./errorBar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -36,13 +37,13 @@ const ErrorBar = createClass({
|
||||
const msg = [];
|
||||
if(this.hasOpenError){
|
||||
msg.push(<div>
|
||||
An unmatched opening tag means there's an opened tag that isn't closed, you need to close a tag, like this {'</div>'}. Make sure to match types!
|
||||
An unmatched opening tag means there's an opened tag that isn't closed. You need to close your tags, like this {'</div>'}. Make sure to match types!
|
||||
</div>);
|
||||
}
|
||||
|
||||
if(this.hasCloseError){
|
||||
msg.push(<div>
|
||||
An unmatched closing tag means you closed a tag without opening it. Either remove it, you check to where you think you opened it.
|
||||
An unmatched closing tag means you closed a tag without opening it. Either remove it, or check to where you think you opened it.
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
require('./notificationPopup.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames'); //Unused variable
|
||||
|
||||
const DISMISS_KEY = 'dismiss_notification7-24-19';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
getInitialState : function() {
|
||||
return {
|
||||
notifications : {}
|
||||
};
|
||||
},
|
||||
componentDidMount : function() {
|
||||
this.checkNotifications();
|
||||
window.addEventListener('resize', this.checkNotifications);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.checkNotifications);
|
||||
},
|
||||
notifications : {
|
||||
psa : function(){
|
||||
return <li key='psa'>
|
||||
<em>Known bug: Grey Shadow Boxes </em> <br />
|
||||
The shadows around certain brew elements such as notes and statblocks might appear as a solid grey box when generating a PDF.
|
||||
<a target='_blank' href='https://old.reddit.com/r/homebrewery/comments/ch3v0d/psa_grey_boxesshadows_around_notes_stat_blocks_etc/'>
|
||||
See this Reddit post
|
||||
</a> for updates and possible workarounds.
|
||||
</li>;
|
||||
},
|
||||
faq : function(){
|
||||
return <li key='faq'>
|
||||
<em>Protect your work! </em> <br />
|
||||
At the moment we do not save a history of your projects, so please make frequent backups of your brews!
|
||||
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||
See the FAQ
|
||||
</a> to learn how to avoid losing your work!
|
||||
</li>;
|
||||
},
|
||||
},
|
||||
checkNotifications : function(){
|
||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
||||
if(hideDismiss) return this.setState({ notifications: {} });
|
||||
|
||||
this.setState({
|
||||
notifications : _.mapValues(this.notifications, (fn)=>{ return fn(); }) //Convert notification functions into their return text value
|
||||
});
|
||||
},
|
||||
dismiss : function(){
|
||||
localStorage.setItem(DISMISS_KEY, true);
|
||||
this.checkNotifications();
|
||||
},
|
||||
render : function(){
|
||||
if(_.isEmpty(this.state.notifications)) return null;
|
||||
|
||||
return <div className='notificationPopup'>
|
||||
<i className='fa fa-times dismiss' onClick={this.dismiss}/>
|
||||
<i className='fa fa-info-circle info' />
|
||||
<h3>Notice</h3>
|
||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||
<ul>{_.values(this.state.notifications)}</ul>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NotificationPopup;
|
||||
@@ -0,0 +1,62 @@
|
||||
.popups{
|
||||
position : fixed;
|
||||
top : @navbarHeight;
|
||||
right : 15px;
|
||||
z-index : 10001;
|
||||
width : 350px;
|
||||
}
|
||||
|
||||
.notificationPopup{
|
||||
position : relative;
|
||||
float : right;
|
||||
display : inline-block;
|
||||
width : 350px;
|
||||
padding : 20px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 85px;
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
a{
|
||||
color : @steel;
|
||||
font-weight : 800;
|
||||
}
|
||||
i.info{
|
||||
position : absolute;
|
||||
top : 24px;
|
||||
left : 24px;
|
||||
opacity : 0.8;
|
||||
font-size : 2.5em;
|
||||
}
|
||||
i.dismiss{
|
||||
position : absolute;
|
||||
top : 10px;
|
||||
right : 10px;
|
||||
cursor : pointer;
|
||||
opacity : 0.6;
|
||||
&:hover{
|
||||
opacity : 1;
|
||||
}
|
||||
}
|
||||
small{
|
||||
opacity : 0.7;
|
||||
font-size : 0.6em;
|
||||
}
|
||||
h3{
|
||||
font-size : 1.1em;
|
||||
font-weight : 800;
|
||||
}
|
||||
ul{
|
||||
margin-top : 15px;
|
||||
font-size : 0.8em;
|
||||
list-style-position : outside;
|
||||
list-style-type : disc;
|
||||
li{
|
||||
font-size : 0.8em;
|
||||
line-height : 1.4em;
|
||||
margin-top : 1.4em;
|
||||
em{
|
||||
font-weight : 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./editor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -138,9 +139,3 @@ const Editor = createClass({
|
||||
});
|
||||
|
||||
module.exports = Editor;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./metadataEditor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -42,10 +43,15 @@ const MetadataEditor = createClass({
|
||||
},
|
||||
|
||||
handleDelete : function(){
|
||||
if(!confirm('are you sure you want to delete this brew?')) return;
|
||||
if(!confirm('are you REALLY sure? You will not be able to recover it')) return;
|
||||
if(this.props.metadata.authors.length <= 1){
|
||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||
} else {
|
||||
if(!confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
request.get(`/api/remove/${this.props.metadata.editId}`)
|
||||
request.delete(`/api/${this.props.metadata.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
window.location.href = '/';
|
||||
@@ -68,7 +74,7 @@ const MetadataEditor = createClass({
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={_.includes(this.props.metadata.systems, val)}
|
||||
onChange={()=>this.handleSystem(val)} />
|
||||
onChange={(e)=>this.handleSystem(val, e)} />
|
||||
{val}
|
||||
</label>;
|
||||
});
|
||||
@@ -118,7 +124,7 @@ const MetadataEditor = createClass({
|
||||
return <div className='field reddit'>
|
||||
<label>reddit</label>
|
||||
<div className='value'>
|
||||
<a href={this.getRedditLink()} target='_blank'>
|
||||
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
|
||||
<button className='publish'>
|
||||
<i className='fa fa-reddit-alien' /> share to reddit
|
||||
</button>
|
||||
@@ -133,18 +139,18 @@ const MetadataEditor = createClass({
|
||||
<label>title</label>
|
||||
<input type='text' className='value'
|
||||
value={this.props.metadata.title}
|
||||
onChange={()=>this.handleFieldChange('title')} />
|
||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||
</div>
|
||||
<div className='field description'>
|
||||
<label>description</label>
|
||||
<textarea value={this.props.metadata.description} className='value'
|
||||
onChange={()=>this.handleFieldChange('description')} />
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
{/*}
|
||||
<div className='field tags'>
|
||||
<label>tags</label>
|
||||
<textarea value={this.props.metadata.tags}
|
||||
onChange={()=>this.handleFieldChange('tags')} />
|
||||
onChange={(e)=>this.handleFieldChange('tags', e)} />
|
||||
</div>
|
||||
*/}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./snippetbar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -92,4 +93,4 @@ const SnippetGroup = createClass({
|
||||
</div>;
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ module.exports = {
|
||||
let slots = 2;
|
||||
return `<div class='classTable wide'>\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
|
||||
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
|
||||
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
levelName,
|
||||
@@ -111,4 +111,4 @@ module.exports = {
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n</div>\n\n`;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -96,7 +96,11 @@ const getStats = function(){
|
||||
const genAbilities = function(){
|
||||
return _.sample([
|
||||
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
|
||||
'> ***False Appearance. *** While the armor reamin motionless, it is indistinguishable from a normal suit of armor.',
|
||||
'> ***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
|
||||
'> ***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
|
||||
'> ***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
|
||||
'> ***Sassiness.*** When questioned, this creature will talk back instead of answering.',
|
||||
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -184,7 +188,7 @@ module.exports = {
|
||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||
'> ___',
|
||||
_.times(_.random(0, 2), function(){
|
||||
_.times(_.random(2, 3), function(){
|
||||
return genAbilities();
|
||||
}).join('\n>\n'),
|
||||
'> ### Actions',
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
require('./homebrew.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const CreateRouter = require('pico-router').createRouter;
|
||||
const { StaticRouter:Router, Switch, Route } = require('react-router-dom');
|
||||
const queryString = require('query-string');
|
||||
|
||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
|
||||
let Router;
|
||||
const Homebrew = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
@@ -36,55 +36,36 @@ const Homebrew = createClass({
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
|
||||
|
||||
Router = CreateRouter({
|
||||
'/edit/:id' : (args)=>{
|
||||
if(!this.props.brew.editId){
|
||||
return <ErrorPage errorId={args.id}/>;
|
||||
}
|
||||
|
||||
return <EditPage
|
||||
id={args.id}
|
||||
brew={this.props.brew} />;
|
||||
},
|
||||
|
||||
'/share/:id' : (args)=>{
|
||||
if(!this.props.brew.shareId){
|
||||
return <ErrorPage errorId={args.id}/>;
|
||||
}
|
||||
|
||||
return <SharePage
|
||||
id={args.id}
|
||||
brew={this.props.brew} />;
|
||||
},
|
||||
'/user/:username' : (args)=>{
|
||||
return <UserPage
|
||||
username={args.username}
|
||||
brews={this.props.brews}
|
||||
/>;
|
||||
},
|
||||
'/print/:id' : (args, query)=>{
|
||||
return <PrintPage brew={this.props.brew} query={query}/>;
|
||||
},
|
||||
'/print' : (args, query)=>{
|
||||
return <PrintPage query={query}/>;
|
||||
},
|
||||
'/new' : (args)=>{
|
||||
return <NewPage />;
|
||||
},
|
||||
'/changelog' : (args)=>{
|
||||
return <SharePage
|
||||
brew={{ title: 'Changelog', text: this.props.changelog }} />;
|
||||
},
|
||||
'*' : <HomePage
|
||||
welcomeText={this.props.welcomeText} />,
|
||||
});
|
||||
},
|
||||
render : function(){
|
||||
return <div className='homebrew'>
|
||||
<Router initialUrl={this.props.url}/>
|
||||
</div>;
|
||||
render : function (){
|
||||
return (
|
||||
<Router location={this.props.url}>
|
||||
<div className='homebrew'>
|
||||
<Switch>
|
||||
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
|
||||
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
|
||||
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} /> } />
|
||||
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} /> } />
|
||||
<Route path='/new' exact component={NewPage}/>
|
||||
<Route path='/changelog' exact component={()=><SharePage brew={{ title: 'Changelog', text: this.props.changelog }} />}/>
|
||||
<Route path='/' component={()=><HomePage welcomeText={this.props.welcomeText}/>}/>
|
||||
</Switch>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Homebrew;
|
||||
|
||||
//TODO: Nicer Error page instead of just "cant get that"
|
||||
// '/share/:id' : (args)=>{
|
||||
// if(!this.props.brew.shareId){
|
||||
// return <ErrorPage errorId={args.id}/>;
|
||||
// }
|
||||
//
|
||||
// return <SharePage
|
||||
// id={args.id}
|
||||
// brew={this.props.brew} />;
|
||||
// },
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.homebrew{
|
||||
height : 100%;
|
||||
@@ -8,9 +7,9 @@
|
||||
background-color : @steel;
|
||||
flex-direction : column;
|
||||
.content{
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@ const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item newTab={true} href='https://www.reddit.com/r/homebrewery/submit?selftext=true&title=[Issue]' color='red' icon='fa-bug'>
|
||||
return <Nav.item
|
||||
newTab={true}
|
||||
color='red'
|
||||
icon='fa-bug'
|
||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} >
|
||||
report issue
|
||||
</Nav.item>;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./navbar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
|
||||
@@ -1,51 +1,72 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const Moment = require('moment');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const VIEW_KEY = 'homebrewery-recently-viewed';
|
||||
const EDIT_KEY = 'homebrewery-recently-edited';
|
||||
const VIEW_KEY = 'homebrewery-recently-viewed';
|
||||
|
||||
|
||||
const RecentItems = createClass({
|
||||
|
||||
const BaseItem = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
storageKey : '',
|
||||
text : '',
|
||||
currentBrew : {
|
||||
title : '',
|
||||
id : '',
|
||||
url : ''
|
||||
}
|
||||
storageKey : '',
|
||||
showEdit : false,
|
||||
showView : false
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false,
|
||||
brews : []
|
||||
edit : [],
|
||||
view : []
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
let brews = JSON.parse(localStorage.getItem(this.props.storageKey) || '[]');
|
||||
|
||||
brews = _.filter(brews, (brew)=>{
|
||||
return brew.id !== this.props.currentBrew.id;
|
||||
});
|
||||
if(this.props.currentBrew.id){
|
||||
brews.unshift({
|
||||
id : this.props.currentBrew.id,
|
||||
url : this.props.currentBrew.url,
|
||||
title : this.props.currentBrew.title,
|
||||
//== Load recent items list ==//
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||
|
||||
//== Add current brew to appropriate recent items list (depending on storageKey) ==//
|
||||
if(this.props.storageKey == 'edit'){
|
||||
edited = _.filter(edited, (brew)=>{
|
||||
return brew.id !== this.props.brew.editId;
|
||||
});
|
||||
edited.unshift({
|
||||
id : this.props.brew.editId,
|
||||
title : this.props.brew.title,
|
||||
url : `/edit/${this.props.brew.editId}`,
|
||||
ts : Date.now()
|
||||
});
|
||||
}
|
||||
brews = _.slice(brews, 0, 8);
|
||||
localStorage.setItem(this.props.storageKey, JSON.stringify(brews));
|
||||
if(this.props.storageKey == 'view'){
|
||||
viewed = _.filter(viewed, (brew)=>{
|
||||
return brew.id !== this.props.brew.shareId;
|
||||
});
|
||||
viewed.unshift({
|
||||
id : this.props.brew.shareId,
|
||||
title : this.props.brew.title,
|
||||
url : `/share/${this.props.brew.shareId}`,
|
||||
ts : Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
//== Store the updated lists (up to 8 items each) ==//
|
||||
edited = _.slice(edited, 0, 8);
|
||||
viewed = _.slice(viewed, 0, 8);
|
||||
|
||||
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
|
||||
|
||||
this.setState({
|
||||
brews : brews
|
||||
edit : edited,
|
||||
view : viewed
|
||||
});
|
||||
},
|
||||
|
||||
@@ -58,14 +79,25 @@ const BaseItem = createClass({
|
||||
renderDropdown : function(){
|
||||
if(!this.state.showDropdown) return null;
|
||||
|
||||
const items = _.map(this.state.brews, (brew)=>{
|
||||
return <a href={brew.url} className='item' key={brew.id} target='_blank'>
|
||||
<span className='title'>{brew.title}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
</a>;
|
||||
});
|
||||
const makeItems = (brews)=>{
|
||||
return _.map(brews, (brew)=>{
|
||||
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer'>
|
||||
<span className='title'>{brew.title || '[ no title ]'}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
</a>;
|
||||
});
|
||||
};
|
||||
|
||||
return <div className='dropdown'>{items}</div>;
|
||||
return <div className='dropdown'>
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>edited</h4> : null }
|
||||
{this.props.showEdit ?
|
||||
makeItems(this.state.edit) : null }
|
||||
{(this.props.showEdit && this.props.showView) ?
|
||||
<h4>viewed</h4> : null }
|
||||
{this.props.showView ?
|
||||
makeItems(this.state.view) : null }
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
@@ -75,126 +107,37 @@ const BaseItem = createClass({
|
||||
{this.props.text}
|
||||
{this.renderDropdown()}
|
||||
</Nav.item>;
|
||||
},
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
viewed : createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
shareId : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
render : function(){
|
||||
return <BaseItem text='recently viewed' storageKey={VIEW_KEY}
|
||||
currentBrew={{
|
||||
id : this.props.brew.shareId,
|
||||
title : this.props.brew.title,
|
||||
url : `/share/${this.props.brew.shareId}`
|
||||
}}
|
||||
/>;
|
||||
},
|
||||
}),
|
||||
|
||||
edited : createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
editId : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
render : function(){
|
||||
return <BaseItem text='recently edited' storageKey={EDIT_KEY}
|
||||
currentBrew={{
|
||||
id : this.props.brew.editId,
|
||||
title : this.props.brew.title,
|
||||
url : `/edit/${this.props.brew.editId}`
|
||||
}}
|
||||
/>;
|
||||
},
|
||||
}),
|
||||
edited : (props)=>{
|
||||
return <RecentItems
|
||||
brew={props.brew}
|
||||
storageKey={props.storageKey}
|
||||
text='recently edited'
|
||||
showEdit={true}
|
||||
/>;
|
||||
},
|
||||
|
||||
both : createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
errorId : null
|
||||
};
|
||||
},
|
||||
viewed : (props)=>{
|
||||
return <RecentItems
|
||||
brew={props.brew}
|
||||
storageKey={props.storageKey}
|
||||
text='recently viewed'
|
||||
showView={true}
|
||||
/>;
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showDropdown : false,
|
||||
edit : [],
|
||||
view : []
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||
|
||||
if(this.props.errorId){
|
||||
edited = _.filter(edited, (edit)=>{
|
||||
return edit.id !== this.props.errorId;
|
||||
});
|
||||
viewed = _.filter(viewed, (view)=>{
|
||||
return view.id !== this.props.errorId;
|
||||
});
|
||||
|
||||
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
|
||||
}
|
||||
|
||||
|
||||
this.setState({
|
||||
edit : edited,
|
||||
view : viewed
|
||||
});
|
||||
},
|
||||
|
||||
handleDropdown : function(show){
|
||||
this.setState({
|
||||
showDropdown : show
|
||||
});
|
||||
},
|
||||
|
||||
renderDropdown : function(){
|
||||
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'>
|
||||
<span className='title'>{brew.title}</span>
|
||||
<span className='time'>{Moment(brew.ts).fromNow()}</span>
|
||||
</a>;
|
||||
});
|
||||
};
|
||||
|
||||
return <div className='dropdown'>
|
||||
<h4>edited</h4>
|
||||
{makeItems(this.state.edit)}
|
||||
<h4>viewed</h4>
|
||||
{makeItems(this.state.view)}
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fa-clock-o' color='grey' className='recent'
|
||||
onMouseEnter={()=>this.handleDropdown(true)}
|
||||
onMouseLeave={()=>this.handleDropdown(false)}>
|
||||
Recent brews
|
||||
{this.renderDropdown()}
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
})
|
||||
both : (props)=>{
|
||||
return <RecentItems
|
||||
brew={props.brew}
|
||||
storageKey={props.storageKey}
|
||||
text='recent brews'
|
||||
showEdit={true}
|
||||
showView={true}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,10 @@
|
||||
require('./editPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
@@ -10,7 +12,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
//const RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
@@ -45,11 +47,10 @@ const EditPage = createClass({
|
||||
return {
|
||||
brew : this.props.brew,
|
||||
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
lastUpdated : this.props.brew.updatedAt
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||
};
|
||||
},
|
||||
savedBrew : null,
|
||||
@@ -62,9 +63,9 @@ const EditPage = createClass({
|
||||
}
|
||||
};
|
||||
|
||||
this.setState({
|
||||
htmlErrors : Markdown.validate(this.state.brew.text)
|
||||
});
|
||||
this.setState((prevState)=>({
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
@@ -91,12 +92,10 @@ const EditPage = createClass({
|
||||
},
|
||||
|
||||
handleMetadataChange : function(metadata){
|
||||
this.setState({
|
||||
brew : _.merge({}, this.state.brew, metadata),
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, metadata),
|
||||
isPending : true,
|
||||
}, ()=>{
|
||||
this.trySave();
|
||||
});
|
||||
}), ()=>this.trySave());
|
||||
|
||||
},
|
||||
|
||||
@@ -106,22 +105,16 @@ const EditPage = createClass({
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
|
||||
this.setState({
|
||||
brew : _.merge({}, this.state.brew, { text: text }),
|
||||
this.setState((prevState)=>({
|
||||
brew : _.merge({}, prevState.brew, { text: text }),
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors
|
||||
});
|
||||
|
||||
this.trySave();
|
||||
}), ()=>this.trySave());
|
||||
},
|
||||
|
||||
hasChanges : function(){
|
||||
if(this.savedBrew){
|
||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||
} else {
|
||||
return !_.isEqual(this.state.brew, this.props.brew);
|
||||
}
|
||||
return false;
|
||||
const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew;
|
||||
return !_.isEqual(this.state.brew, savedBrew);
|
||||
},
|
||||
|
||||
trySave : function(){
|
||||
@@ -136,14 +129,14 @@ const EditPage = createClass({
|
||||
save : function(){
|
||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||
|
||||
this.setState({
|
||||
this.setState((prevState)=>({
|
||||
isSaving : true,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.state.brew.text)
|
||||
});
|
||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||
}));
|
||||
|
||||
request
|
||||
.put(`/api/update/${this.props.brew.editId}`)
|
||||
.put(`/api/${this.props.brew.editId}`)
|
||||
.send(this.state.brew)
|
||||
.end((err, res)=>{
|
||||
if(err){
|
||||
@@ -153,9 +146,8 @@ const EditPage = createClass({
|
||||
} else {
|
||||
this.savedBrew = res.body;
|
||||
this.setState({
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
lastUpdated : res.body.updatedAt
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -173,7 +165,8 @@ const EditPage = createClass({
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' href={`https://github.com/stolksdorf/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
Report the issue <a target='_blank' rel='noopener noreferrer'
|
||||
href={`https://github.com/stolksdorf/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
@@ -195,14 +188,15 @@ const EditPage = createClass({
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.renderSaveButton()}
|
||||
{/*<RecentlyEdited brew={this.props.brew} />*/}
|
||||
<ReportIssue />
|
||||
<Nav.item newTab={true} href={`/share/${this.props.brew.shareId}`} color='teal' icon='fa-share-alt'>
|
||||
Share
|
||||
</Nav.item>
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<RecentNavItem brew={this.props.brew} storageKey='edit' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
@@ -210,6 +204,7 @@ const EditPage = createClass({
|
||||
|
||||
render : function(){
|
||||
return <div className='editPage page'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./errorPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -7,7 +8,7 @@ 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');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
@@ -33,7 +34,7 @@ const ErrorPage = createClass({
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<IssueNavItem />
|
||||
<RecentNavItem.both errorId={this.props.errorId} />
|
||||
<RecentNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
require('./homePage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
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');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
|
||||
|
||||
@@ -59,7 +61,7 @@ const HomePage = createClass({
|
||||
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'>
|
||||
Changelog
|
||||
</Nav.item>
|
||||
<RecentNavItem.both />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
{/*}
|
||||
<Nav.item href='/new' color='green' icon='fa-external-link'>
|
||||
@@ -72,6 +74,7 @@ const HomePage = createClass({
|
||||
|
||||
render : function(){
|
||||
return <div className='homePage page'>
|
||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
|
||||
@@ -43,10 +43,10 @@ With the next major release of Homebrewery, v3.0.0, this tool *will no longer su
|
||||
What's new in the latest update? Check out the full changelog [here](/changelog)
|
||||
|
||||
### Bugs, Issues, Suggestions?
|
||||
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://github.com/stolksdorf/homebrewery/issues/new) and let me know!.
|
||||
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let me know!.
|
||||
|
||||
### Legal Junk
|
||||
The Homebrewery is licensed using the [MIT License](https://github.com/stolksdorf/homebrewery/blob/master/license). Which means you are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself.
|
||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself.
|
||||
|
||||
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
@@ -55,7 +55,7 @@ If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](
|
||||
|
||||
|
||||
|
||||
<img src='http://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:50px;right:30px;width:280px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./newPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -9,6 +10,7 @@ const Markdown = require('naturalcrit/markdown.js');
|
||||
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 SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
@@ -135,6 +137,7 @@ const NewPage = createClass({
|
||||
{this.renderSaveButton()}
|
||||
{this.renderLocalPrintButton()}
|
||||
<IssueNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
require('./printPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const PrintPage = createClass({
|
||||
@@ -22,7 +24,9 @@ const PrintPage = createClass({
|
||||
|
||||
componentDidMount : function() {
|
||||
if(this.props.query.local){
|
||||
this.setState({ brewText: localStorage.getItem(this.props.query.local) });
|
||||
this.setState((prevState, prevProps)=>({
|
||||
brewText : localStorage.getItem(prevProps.query.local)
|
||||
}));
|
||||
}
|
||||
|
||||
if(this.props.query.dialog) window.print();
|
||||
@@ -40,6 +44,7 @@ const PrintPage = createClass({
|
||||
|
||||
render : function(){
|
||||
return <div>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
{this.renderPages()}
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
require('./sharePage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const { Meta } = require('vitreum/headtags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
//const RecentlyViewed = require('../../navbar/recent.navitem.jsx').viewed;
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
|
||||
|
||||
@@ -46,18 +47,18 @@ const SharePage = createClass({
|
||||
|
||||
render : function(){
|
||||
return <div className='sharePage page'>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<ReportIssue />
|
||||
{/*<RecentlyViewed brew={this.props.brew} />*/}
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<Nav.item href={`/source/${this.props.brew.shareId}`} color='teal' icon='fa-code'>
|
||||
source
|
||||
</Nav.item>
|
||||
<RecentNavItem brew={this.props.brew} storageKey='view' />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.sharePage{
|
||||
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./brewItem.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -18,10 +19,15 @@ const BrewItem = createClass({
|
||||
},
|
||||
|
||||
deleteBrew : function(){
|
||||
if(!confirm('are you sure you want to delete this brew?')) return;
|
||||
if(!confirm('are you REALLY sure? You will not be able to recover it')) return;
|
||||
if(this.props.brew.authors.length <= 1){
|
||||
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
|
||||
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
|
||||
} else {
|
||||
if(!confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
|
||||
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
|
||||
}
|
||||
|
||||
request.get(`/api/remove/${this.props.brew.editId}`)
|
||||
request.delete(`/api/${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
@@ -38,7 +44,7 @@ const BrewItem = createClass({
|
||||
renderEditLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
return <a href={`/edit/${this.props.brew.editId}`} target='_blank'>
|
||||
return <a href={`/edit/${this.props.brew.editId}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fa fa-pencil' />
|
||||
</a>;
|
||||
},
|
||||
@@ -63,7 +69,7 @@ const BrewItem = createClass({
|
||||
</div>
|
||||
|
||||
<div className='links'>
|
||||
<a href={`/share/${brew.shareId}`} target='_blank'>
|
||||
<a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fa fa-share-alt' />
|
||||
</a>
|
||||
{this.renderEditLink()}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./userPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -6,7 +7,7 @@ const cx = require('classnames');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx');
|
||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||
|
||||
@@ -42,31 +43,27 @@ const UserPage = createClass({
|
||||
});
|
||||
},
|
||||
|
||||
renderPrivateBrews : function(privateBrews){
|
||||
if(!privateBrews || !privateBrews.length) return;
|
||||
|
||||
return [
|
||||
<h1>{this.props.username}'s unpublished brews</h1>,
|
||||
this.renderBrews(privateBrews)
|
||||
];
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const brews = this.getSortedBrews();
|
||||
|
||||
return <div className='userPage page'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<RecentNavItem.both />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<div className='phb'>
|
||||
<h1>{this.props.username}'s brews</h1>
|
||||
{this.renderBrews(brews.published)}
|
||||
{this.renderPrivateBrews(brews.private)}
|
||||
<div>
|
||||
<h1>{this.props.username}'s brews</h1>
|
||||
{this.renderBrews(brews.published)}
|
||||
</div>
|
||||
<div>
|
||||
<h1>{this.props.username}'s unpublished brews</h1>
|
||||
{this.renderBrews(brews.private)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
.phb{
|
||||
.noColumns();
|
||||
height : auto;
|
||||
min-height : 279.4mm;
|
||||
min-height : 279.4mm;
|
||||
margin : 20px auto;
|
||||
&::after{
|
||||
display : none;
|
||||
@@ -30,4 +30,4 @@
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
@import (less) 'shared/naturalcrit/styles/reset.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
|
||||
@import (less) './client/homebrew/phbStyle/phb.assets.less';
|
||||
@@ -156,6 +155,7 @@ body {
|
||||
margin-bottom : 1em;
|
||||
font-size : 10pt;
|
||||
thead{
|
||||
display: table-row-group;
|
||||
font-weight : 800;
|
||||
th{
|
||||
vertical-align : bottom;
|
||||
@@ -337,8 +337,8 @@ body {
|
||||
p,blockquote,table{
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
column-break-inside : avoid;
|
||||
overflow: hidden; /* Firefox fix */
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
//Better spacing for spell blocks
|
||||
h4+p+hr+ul{
|
||||
@@ -355,7 +355,8 @@ body {
|
||||
}
|
||||
li{
|
||||
-webkit-column-break-inside : avoid;
|
||||
column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
@@ -380,7 +381,8 @@ body {
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
-webkit-column-break-inside : auto;
|
||||
column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
@@ -449,7 +451,8 @@ body {
|
||||
// *****************************/
|
||||
.phb .toc{
|
||||
-webkit-column-break-inside : avoid;
|
||||
column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
a{
|
||||
color : black;
|
||||
text-decoration : none;
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
module.exports = function(vitreum){
|
||||
module.exports = async (name, props={})=>{
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet'></link>
|
||||
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
|
||||
<title>The Homebrewery - NaturalCrit</title>
|
||||
${vitreum.head}
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${vitreum.body}</main>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
</body>
|
||||
${vitreum.js}
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,10 +16,10 @@ Some issues are created with missing information, not reproducible, or plain inv
|
||||
|
||||
You can use issue labels to discover issues you could help out with:
|
||||
|
||||
* [`blocked` issues](https://github.com/stolksdorf/homebrewery/labels/blocked) need help getting unstuck
|
||||
* [`bug` issues](https://github.com/stolksdorf/homebrewery/labels/bug) are known bugs we'd like to fix
|
||||
* [`feature` issues](https://github.com/stolksdorf/homebrewery/labels/feature) are features we're open to including
|
||||
* [`help wanted`](https://github.com/stolksdorf/homebrewery/labels/help%20wanted) labels are especially useful.
|
||||
* [`blocked` issues](https://github.com/naturalcrit/homebrewery/labels/blocked) need help getting unstuck
|
||||
* [`bug` issues](https://github.com/naturalcrit/homebrewery/labels/bug) are known bugs we'd like to fix
|
||||
* [`feature` issues](https://github.com/naturalcrit/homebrewery/labels/feature) are features we're open to including
|
||||
* [`help wanted`](https://github.com/naturalcrit/homebrewery/labels/help%20wanted) labels are especially useful.
|
||||
|
||||
If you're updating dependencies, please make sure you use npm@5.6.0 and commit the updated `package-lock.json` file.
|
||||
|
||||
|
||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
version: '2'
|
||||
services:
|
||||
mongodb:
|
||||
image: mongo:latest
|
||||
volumes:
|
||||
- mongodata:/data/db
|
||||
homebrewery:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: homebrewery
|
||||
environment:
|
||||
MONGODB_URI: mongodb://mongodb/homebrewery
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
mongodata:
|
||||
10262
package-lock.json
generated
10262
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -1,15 +1,19 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "2.8.0",
|
||||
"version": "2.8.2",
|
||||
"engines": {
|
||||
"node": "12.16.x"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/stolksdorf/homebrewery.git"
|
||||
"url": "git://github.com/naturalcrit/homebrewery.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"quick": "node scripts/quick.js",
|
||||
"build": "node scripts/build.js",
|
||||
"build": "node scripts/buildHomebrew.js",
|
||||
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||
"lint": "eslint --fix **/*.{js,jsx}",
|
||||
"lint:dry": "eslint **/*.{js,jsx}",
|
||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||
@@ -18,7 +22,7 @@
|
||||
"test:dev": "pico-check -v -w",
|
||||
"phb": "node scripts/phb.js",
|
||||
"prod": "set NODE_ENV=production && npm run build",
|
||||
"postinstall": "npm run build",
|
||||
"postinstall": "npm run buildall",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "stolksdorf",
|
||||
@@ -36,33 +40,35 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-preset-env": "^1.1.8",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"basic-auth": "^1.0.3",
|
||||
"body-parser": "^1.14.2",
|
||||
"classnames": "^2.2.0",
|
||||
"codemirror": "^5.22.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.6",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"body-parser": "^1.19.0",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.54.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"create-react-class": "^15.6.3",
|
||||
"express": "^4.13.3",
|
||||
"jwt-simple": "^0.5.1",
|
||||
"lodash": "^4.11.2",
|
||||
"marked": "^0.3.5",
|
||||
"moment": "^2.11.0",
|
||||
"mongoose": "^4.3.3",
|
||||
"nconf": "^0.8.4",
|
||||
"pico-flux": "^1.1.0",
|
||||
"pico-router": "^1.1.0",
|
||||
"react": "^16.3.1",
|
||||
"react-dom": "^16.3.1",
|
||||
"shortid": "^2.2.4",
|
||||
"striptags": "^2.1.1",
|
||||
"superagent": "^3.8.2",
|
||||
"vitreum": "^4.10.1"
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "9.0.0",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"less": "^3.11.1",
|
||||
"lodash": "^4.17.15",
|
||||
"marked": "^0.3.19",
|
||||
"moment": "^2.26.0",
|
||||
"mongoose": "^5.9.15",
|
||||
"nconf": "^0.10.0",
|
||||
"prop-types": "15.7.2",
|
||||
"query-string": "6.12.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "5.2.0",
|
||||
"shortid": "^2.2.15",
|
||||
"superagent": "^5.2.2",
|
||||
"vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"pico-check": "^1.0.3"
|
||||
"eslint": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.20.0",
|
||||
"pico-check": "^1.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
2
robots.txt
Normal file
2
robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
# Notes
|
||||
User-agent: *
|
||||
@@ -1,20 +0,0 @@
|
||||
const label = 'build';
|
||||
console.time(label);
|
||||
|
||||
const clean = require('vitreum/steps/clean.js');
|
||||
const jsx = require('vitreum/steps/jsx.js');
|
||||
const lib = require('vitreum/steps/libs.js');
|
||||
const less = require('vitreum/steps/less.js');
|
||||
const asset = require('vitreum/steps/assets.js');
|
||||
|
||||
const Proj = require('./project.json');
|
||||
|
||||
clean()
|
||||
.then(lib(Proj.libs))
|
||||
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] }))
|
||||
.then((deps)=>less('homebrew', { shared: ['./shared'] }, deps))
|
||||
.then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] }))
|
||||
.then((deps)=>less('admin', { shared: ['./shared'] }, deps))
|
||||
.then(()=>asset(Proj.assets, ['./shared', './client']))
|
||||
.then(console.timeEnd.bind(console, label))
|
||||
.catch(console.error);
|
||||
31
scripts/buildAdmin.js
Normal file
31
scripts/buildAdmin.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const fs = require('fs-extra');
|
||||
const Proj = require('./project.json');
|
||||
|
||||
const { pack } = require('vitreum');
|
||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||
|
||||
const lessTransform = require('vitreum/transforms/less.js');
|
||||
const assetTransform = require('vitreum/transforms/asset.js');
|
||||
//const Meta = require('vitreum/headtags');
|
||||
|
||||
const transforms = {
|
||||
'.less' : lessTransform,
|
||||
'*' : assetTransform('./build')
|
||||
};
|
||||
|
||||
const build = async ({ bundle, render, ssr })=>{
|
||||
await fs.outputFile('./build/admin/bundle.css', await lessTransform.generate({ paths: './shared' }));
|
||||
await fs.outputFile('./build/admin/bundle.js', bundle);
|
||||
await fs.outputFile('./build/admin/ssr.js', ssr);
|
||||
await fs.outputFile('./build/admin/render.js', render);
|
||||
};
|
||||
|
||||
fs.emptyDirSync('./build/admin');
|
||||
pack('./client/admin/admin.jsx', {
|
||||
paths : ['./shared'],
|
||||
libs : Proj.libs,
|
||||
dev : isDev && build,
|
||||
transforms
|
||||
})
|
||||
.then(build)
|
||||
.catch(console.error);
|
||||
31
scripts/buildHomebrew.js
Normal file
31
scripts/buildHomebrew.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const fs = require('fs-extra');
|
||||
const Proj = require('./project.json');
|
||||
|
||||
const { pack } = require('vitreum');
|
||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||
|
||||
const lessTransform = require('vitreum/transforms/less.js');
|
||||
const assetTransform = require('vitreum/transforms/asset.js');
|
||||
//const Meta = require('vitreum/headtags');
|
||||
|
||||
const transforms = {
|
||||
'.less' : lessTransform,
|
||||
'*' : assetTransform('./build')
|
||||
};
|
||||
|
||||
const build = async ({ bundle, render, ssr })=>{
|
||||
await fs.outputFile('./build/homebrew/bundle.css', await lessTransform.generate({ paths: './shared' }));
|
||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||
await fs.outputFile('./build/homebrew/render.js', render);
|
||||
};
|
||||
|
||||
fs.emptyDirSync('./build/homebrew');
|
||||
pack('./client/homebrew/homebrew.jsx', {
|
||||
paths : ['./shared'],
|
||||
libs : Proj.libs,
|
||||
dev : isDev && build,
|
||||
transforms
|
||||
})
|
||||
.then(build)
|
||||
.catch(console.error);
|
||||
@@ -13,8 +13,6 @@
|
||||
"codemirror/mode/javascript/javascript.js",
|
||||
"moment",
|
||||
"superagent",
|
||||
"marked",
|
||||
"pico-router",
|
||||
"pico-flux"
|
||||
"marked"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
49
server.js
49
server.js
@@ -3,9 +3,10 @@ const jwt = require('jwt-simple');
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
|
||||
app.use(express.static(`${__dirname}/build`));'';
|
||||
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()
|
||||
@@ -14,15 +15,17 @@ const config = require('nconf')
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
|
||||
//DB
|
||||
require('mongoose')
|
||||
.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/naturalcrit')
|
||||
.connection.on('error', ()=>{
|
||||
console.log('Error : Could not connect to a Mongo Database.');
|
||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||
});
|
||||
const mongoose = require('mongoose');
|
||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit',
|
||||
{ retryWrites: false, useNewUrlParser: true });
|
||||
mongoose.connection.on('error', ()=>{
|
||||
console.log('Error : Could not connect to a Mongo Database.');
|
||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||
throw 'Can not connect to Mongo';
|
||||
});
|
||||
|
||||
|
||||
//Account MIddleware
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try {
|
||||
@@ -41,14 +44,19 @@ const HomebrewModel = require('./server/homebrew.model.js').model;
|
||||
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||
const changelogText = require('fs').readFileSync('./changelog.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`);
|
||||
});
|
||||
|
||||
//Source page
|
||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||
app.get('/source/:id', (req, res)=>{
|
||||
HomebrewModel.get({ shareId: req.params.id })
|
||||
.then((brew)=>{
|
||||
const text = brew.text.replaceAll('<', '<').replaceAll('>', '>');
|
||||
return res.send(`<code><pre>${text}</pre></code>`);
|
||||
return res.send(`<code><pre style="white-space: pre-wrap;">${text}</pre></code>`);
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
@@ -56,7 +64,7 @@ app.get('/source/:id', (req, res)=>{
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//User Page
|
||||
app.get('/user/:username', (req, res, next)=>{
|
||||
const fullAccess = req.account && (req.account.username == req.params.username);
|
||||
HomebrewModel.getByUser(req.params.username, fullAccess)
|
||||
@@ -69,7 +77,7 @@ app.get('/user/:username', (req, res, next)=>{
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//Edit Page
|
||||
app.get('/edit/:id', (req, res, next)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
@@ -112,22 +120,21 @@ app.get('/print/:id', (req, res, next)=>{
|
||||
});
|
||||
|
||||
|
||||
//Render Page
|
||||
const render = require('vitreum/steps/render');
|
||||
//Render the page
|
||||
//const render = require('.build/render');
|
||||
const templateFn = require('./client/template.js');
|
||||
app.use((req, res)=>{
|
||||
render('homebrew', templateFn, {
|
||||
const props = {
|
||||
version : require('./package.json').version,
|
||||
url : req.originalUrl,
|
||||
welcomeText : welcomeText,
|
||||
changelog : changelogText,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
account : req.account
|
||||
})
|
||||
.then((page)=>{
|
||||
return res.send(page);
|
||||
})
|
||||
account : req.account,
|
||||
};
|
||||
templateFn('homebrew', props)
|
||||
.then((page)=>res.send(page))
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
@@ -137,4 +144,4 @@ app.use((req, res)=>{
|
||||
|
||||
const PORT = process.env.PORT || 8000;
|
||||
app.listen(PORT);
|
||||
console.log(`server on port:${PORT}`);
|
||||
console.log(`server on port:${PORT}`);
|
||||
|
||||
@@ -1,51 +1,61 @@
|
||||
const _ = require('lodash');
|
||||
const auth = require('basic-auth');
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const router = require('express').Router();
|
||||
const Moment = require('moment');
|
||||
//const render = require('vitreum/steps/render');
|
||||
const templateFn = require('../client/template.js');
|
||||
const zlib = require('zlib');
|
||||
|
||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
||||
|
||||
const mw = {
|
||||
adminOnly : (req, res, next)=>{
|
||||
if(req.query && req.query.admin_key == process.env.ADMIN_KEY) return next();
|
||||
if(!req.get('authorization')){
|
||||
return res
|
||||
.set('WWW-Authenticate', 'Basic realm="Authorization Required"')
|
||||
.status(401)
|
||||
.send('Authorization Required');
|
||||
}
|
||||
const [username, password] = new Buffer(req.get('authorization').split(' ').pop(), 'base64')
|
||||
.toString('ascii')
|
||||
.split(':');
|
||||
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
||||
return next();
|
||||
}
|
||||
return res.status(401).send('Access denied');
|
||||
}
|
||||
};
|
||||
|
||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password';
|
||||
process.env.ADMIN_KEY = process.env.ADMIN_KEY || 'admin_key';
|
||||
|
||||
|
||||
|
||||
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
|
||||
router.get('/api/invalid', mw.adminOnly, (req, res)=>{
|
||||
const invalidBrewQuery = HomebrewModel.find({
|
||||
'$where' : 'this.text.length < 140',
|
||||
createdAt : {
|
||||
$lt : Moment().subtract(3, 'days').toDate()
|
||||
}
|
||||
});
|
||||
|
||||
if(req.query.do_it){
|
||||
invalidBrewQuery.remove().exec((err, objs)=>{
|
||||
refreshCount();
|
||||
return res.send(200);
|
||||
});
|
||||
} else {
|
||||
invalidBrewQuery.exec((err, objs)=>{
|
||||
if(err) console.log(err);
|
||||
return res.json({
|
||||
count : objs.length
|
||||
});
|
||||
});
|
||||
/* Search for brews that are older than 3 days and that are shorter than a tweet */
|
||||
const junkBrewQuery = HomebrewModel.find({
|
||||
'$where' : 'this.text.length < 140',
|
||||
createdAt : {
|
||||
$lt : Moment().subtract(30, 'days').toDate()
|
||||
}
|
||||
}).limit(100).maxTime(60000);
|
||||
|
||||
/* Search for brews that aren't compressed (missing the compressed text field) */
|
||||
const uncompressedBrewQuery = HomebrewModel.find({
|
||||
'textBin' : null
|
||||
}).lean().limit(10000).select('_id');
|
||||
|
||||
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||
junkBrewQuery.exec((err, objs)=>{
|
||||
if(err) return res.status(500).send(err);
|
||||
return res.json({ count: objs.length });
|
||||
});
|
||||
});
|
||||
/* Removes all empty brews that are older than 3 days and that are shorter than a tweet */
|
||||
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
||||
junkBrewQuery.remove().exec((err, objs)=>{
|
||||
if(err) return res.status(500).send(err);
|
||||
return res.json({ count: objs.length });
|
||||
});
|
||||
});
|
||||
|
||||
/* Searches for matching edit or share id, also attempts to partial match */
|
||||
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
|
||||
//search for mathcing edit id
|
||||
//search for matching share id
|
||||
// search for partial match
|
||||
|
||||
HomebrewModel.findOne({ $or : [
|
||||
{ editId: { '$regex': req.params.id, '$options': 'i' } },
|
||||
{ shareId: { '$regex': req.params.id, '$options': 'i' } },
|
||||
@@ -54,32 +64,47 @@ router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
|
||||
});
|
||||
});
|
||||
|
||||
/* Find 50 brews that aren't compressed yet */
|
||||
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
||||
uncompressedBrewQuery.exec((err, objs)=>{
|
||||
if(err) return res.status(500).send(err);
|
||||
objs = objs.map((obj)=>{return obj._id;});
|
||||
return res.json({ count: objs.length, ids: objs });
|
||||
});
|
||||
});
|
||||
|
||||
/* Compresses the "text" field of a brew to binary */
|
||||
router.put('/admin/compress/:id', (req, res)=>{
|
||||
HomebrewModel.get({ _id: req.params.id })
|
||||
.then((brew)=>{
|
||||
brew.textBin = zlib.deflateRawSync(brew.text); // Compress brew text to binary before saving
|
||||
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore
|
||||
|
||||
//Admin route
|
||||
|
||||
const render = require('vitreum/steps/render');
|
||||
const templateFn = require('../client/template.js');
|
||||
router.get('/admin', function(req, res){
|
||||
const credentials = auth(req);
|
||||
if(!credentials || credentials.name !== process.env.ADMIN_USER || credentials.pass !== process.env.ADMIN_PASS) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="example"');
|
||||
return res.status(401).send('Access denied');
|
||||
}
|
||||
render('admin', templateFn, {
|
||||
url : req.originalUrl,
|
||||
admin_key : process.env.ADMIN_KEY,
|
||||
})
|
||||
.then((page)=>{
|
||||
return res.send(page);
|
||||
brew.save((err, obj)=>{
|
||||
if(err) throw err;
|
||||
return res.status(200).send(obj);
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
return res.status(500).send('Error while saving');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/admin/stats', mw.adminOnly, (req, res)=>{
|
||||
HomebrewModel.count({}, (err, count)=>{
|
||||
return res.json({
|
||||
totalBrews : count
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/admin', mw.adminOnly, (req, res)=>{
|
||||
templateFn('admin', {
|
||||
url : req.originalUrl
|
||||
})
|
||||
.then((page)=>res.send(page))
|
||||
.catch((err)=>res.sendStatus(500));
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
7
server/forcessl.mw.js
Normal file
7
server/forcessl.mw.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = (req, res, next)=>{
|
||||
if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next();
|
||||
if(req.header('x-forwarded-proto') !== 'https') {
|
||||
return res.redirect(302, `https://${req.get('Host')}${req.url}`);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
const _ = require('lodash');
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const router = require('express').Router();
|
||||
const zlib = require('zlib');
|
||||
|
||||
// const getTopBrews = (cb)=>{
|
||||
// const getTopBrews = (cb) => {
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
// });
|
||||
@@ -10,45 +11,53 @@ const router = require('express').Router();
|
||||
|
||||
const getGoodBrewTitle = (text)=>{
|
||||
const titlePos = text.indexOf('# ');
|
||||
if(titlePos !== -1){
|
||||
if(titlePos !== -1) {
|
||||
const ending = text.indexOf('\n', titlePos);
|
||||
return text.substring(titlePos + 2, ending);
|
||||
} else {
|
||||
return _.find(text.split('\n'), (line)=>{
|
||||
return line;
|
||||
});
|
||||
return _.find(text.split('\n'), (line)=>line);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
router.post('/api', (req, res)=>{
|
||||
|
||||
let authors = [];
|
||||
if(req.account) authors = [req.account.username];
|
||||
const newBrew = (req, res)=>{
|
||||
const authors = (req.account) ? [req.account.username] : [];
|
||||
|
||||
const newHomebrew = new HomebrewModel(_.merge({},
|
||||
req.body,
|
||||
{ authors: authors }
|
||||
));
|
||||
if(!newHomebrew.title){
|
||||
|
||||
if(!newHomebrew.title) {
|
||||
newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
|
||||
}
|
||||
|
||||
// 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){
|
||||
if(err) {
|
||||
console.error(err, err.toString(), err.stack);
|
||||
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
||||
}
|
||||
return res.json(obj);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
router.put('/api/update/:id', (req, res)=>{
|
||||
const updateBrew = (req, res)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
brew = _.merge(brew, req.body);
|
||||
// Compress brew text to binary before saving
|
||||
brew.textBin = zlib.deflateRawSync(req.body.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));
|
||||
|
||||
if(req.account) {
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
}
|
||||
|
||||
brew.markModified('authors');
|
||||
brew.markModified('systems');
|
||||
@@ -59,47 +68,63 @@ router.put('/api/update/:id', (req, res)=>{
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
console.error(err);
|
||||
return res.status(500).send('Error while saving');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
router.get('/api/remove/:id', (req, res)=>{
|
||||
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 resEntry = objs[0];
|
||||
resEntry.remove((err)=>{
|
||||
if(err) return res.status(500).send('Error while removing');
|
||||
return res.status(200).send();
|
||||
});
|
||||
});
|
||||
});
|
||||
if(!objs.length || err) {
|
||||
return res.status(404).send('Can not find homebrew with that id');
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
router.post('/api', newBrew);
|
||||
router.put('/api/:id', updateBrew);
|
||||
router.put('/api/update/:id', updateBrew);
|
||||
router.delete('/api/:id', deleteBrew);
|
||||
router.get('/api/remove/:id', deleteBrew);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
|
||||
module.exports = function(app){
|
||||
|
||||
module.exports = function(app) {
|
||||
app;
|
||||
|
||||
|
||||
|
||||
|
||||
app.get('/api/search', mw.adminOnly, function(req, res){
|
||||
|
||||
app.get('/api/search', mw.adminOnly, function(req, res) {
|
||||
var page = req.query.page || 0;
|
||||
var count = req.query.count || 20;
|
||||
|
||||
var query = {};
|
||||
if(req.query && req.query.id){
|
||||
if (req.query && req.query.id) {
|
||||
query = {
|
||||
"$or" : [{
|
||||
"$or": [{
|
||||
editId : req.query.id
|
||||
},{
|
||||
}, {
|
||||
shareId : req.query.id
|
||||
}]
|
||||
};
|
||||
@@ -110,21 +135,17 @@ module.exports = function(app){
|
||||
}, {
|
||||
skip: page*count,
|
||||
limit: count*1
|
||||
}, function(err, objs){
|
||||
if(err) console.log(err);
|
||||
}, function(err, objs) {
|
||||
if (err) console.error(err);
|
||||
return res.json({
|
||||
page : page,
|
||||
count : count,
|
||||
total : homebrewTotal,
|
||||
brews : objs
|
||||
});
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
return app;
|
||||
}
|
||||
*/
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
const mongoose = require('mongoose');
|
||||
const shortid = require('shortid');
|
||||
const _ = require('lodash');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const HomebrewSchema = mongoose.Schema({
|
||||
shareId : { type: String, default: shortid.generate, index: { unique: true } },
|
||||
editId : { type: String, default: shortid.generate, index: { unique: true } },
|
||||
title : { type: String, default: '' },
|
||||
text : { type: String, default: '' },
|
||||
textBin : { type: Buffer },
|
||||
|
||||
description : { type: String, default: '' },
|
||||
tags : { type: String, default: '' },
|
||||
@@ -51,6 +53,10 @@ HomebrewSchema.statics.get = function(query){
|
||||
return new Promise((resolve, reject)=>{
|
||||
Homebrew.find(query, (err, brews)=>{
|
||||
if(err || !brews.length) return reject('Can not find brew');
|
||||
if(!_.isNil(brews[0].textBin)) { // Uncompress zipped text field
|
||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||
brews[0].text = unzipped.toString();
|
||||
}
|
||||
return resolve(brews[0]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
require('./renderWarnings.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -25,22 +25,13 @@ const RenderWarnings = createClass({
|
||||
if(!isChrome){
|
||||
return <li key='chrome'>
|
||||
<em>Built for Chrome </em> <br />
|
||||
Other browsers do not support
|
||||
<a target='_blank' href='https://developer.mozilla.org/en-US/docs/Web/CSS/column-span#Browser_compatibility'>
|
||||
key features
|
||||
</a> this site uses.
|
||||
Other browsers have not been tested for compatiblilty. If you
|
||||
experience issues with your document not rendering or printing
|
||||
properly, please try using the latest version of Chrome before
|
||||
submitting a bug report.
|
||||
</li>;
|
||||
}
|
||||
},
|
||||
zoom : function(){
|
||||
return false;
|
||||
if(window.devicePixelRatio !== 1){
|
||||
return <li key='zoom'>
|
||||
<em>Your browser is zoomed. </em> <br />
|
||||
This can cause content to jump columns.
|
||||
</li>;
|
||||
}
|
||||
}
|
||||
},
|
||||
checkWarnings : function(){
|
||||
const hideDismiss = localStorage.getItem(DISMISS_KEY);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
|
||||
.renderWarnings{
|
||||
position : fixed;
|
||||
position : relative;
|
||||
float : right;
|
||||
display : inline-block;
|
||||
top : @navbarHeight;
|
||||
right : 15px;
|
||||
z-index : 10001;
|
||||
width : 350px;
|
||||
padding : 20px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 85px;
|
||||
margin-bottom : 10px;
|
||||
background-color : @yellow;
|
||||
color : white;
|
||||
a{
|
||||
font-weight : 800;
|
||||
}
|
||||
i.ohno{
|
||||
position : absolute;
|
||||
top : 24px;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./codeEditor.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -30,7 +31,11 @@ const CodeEditor = createClass({
|
||||
value : this.props.value,
|
||||
lineNumbers : true,
|
||||
lineWrapping : this.props.wrap,
|
||||
mode : this.props.language
|
||||
mode : this.props.language,
|
||||
extraKeys : {
|
||||
'Ctrl-B' : this.makeBold,
|
||||
'Ctrl-I' : this.makeItalic
|
||||
}
|
||||
});
|
||||
|
||||
this.codeMirror.on('change', this.handleChange);
|
||||
@@ -38,6 +43,16 @@ const CodeEditor = createClass({
|
||||
this.updateSize();
|
||||
},
|
||||
|
||||
makeBold : function() {
|
||||
const selection = this.codeMirror.getSelection();
|
||||
this.codeMirror.replaceSelection(`**${selection}**`, 'around');
|
||||
},
|
||||
|
||||
makeItalic : function() {
|
||||
const selection = this.codeMirror.getSelection();
|
||||
this.codeMirror.replaceSelection(`*${selection}*`, 'around');
|
||||
},
|
||||
|
||||
componentWillReceiveProps : function(nextProps){
|
||||
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
|
||||
this.codeMirror.setValue(nextProps.value);
|
||||
|
||||
@@ -13,6 +13,11 @@ renderer.html = function (html) {
|
||||
return html;
|
||||
};
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
const tagRegex = new RegExp(`(${
|
||||
@@ -24,7 +29,10 @@ const tagRegex = new RegExp(`(${
|
||||
module.exports = {
|
||||
marked : Markdown,
|
||||
render : (rawBrewText)=>{
|
||||
return Markdown(rawBrewText, { renderer: renderer });
|
||||
return Markdown(
|
||||
sanatizeScriptTags(rawBrewText),
|
||||
{ renderer: renderer }
|
||||
);
|
||||
},
|
||||
|
||||
validate : (rawBrewText)=>{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./nav.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
@@ -70,4 +71,4 @@ const Nav = {
|
||||
};
|
||||
|
||||
|
||||
module.exports = Nav;
|
||||
module.exports = Nav;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
//@import (less) 'naturalcrit/styles/style.fonts.css';
|
||||
nav{
|
||||
background-color : #333;
|
||||
.navContent{
|
||||
@@ -77,4 +78,4 @@ nav{
|
||||
.navSection:last-child .navItem{
|
||||
border-left : 1px solid #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./splitPane.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
flex-direction : row;
|
||||
.pane{
|
||||
overflow-x : hidden;
|
||||
overflow-y : hidden;
|
||||
flex : 1;
|
||||
}
|
||||
.divider{
|
||||
@@ -28,4 +29,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@import 'naturalcrit/styles/tooltip.less';
|
||||
@font-face {
|
||||
font-family : CodeLight;
|
||||
src : url('/assets/naturalcrit/styles/CODE Light.otf');
|
||||
font-family : 'CodeLight';
|
||||
src : data-uri('naturalcrit/styles/CODE Light.otf') format('opentype');
|
||||
}
|
||||
@font-face {
|
||||
font-family : CodeBold;
|
||||
src : url('/assets/naturalcrit/styles/CODE Bold.otf');
|
||||
font-family : 'CodeBold';
|
||||
src : data-uri('naturalcrit/styles/CODE Bold.otf') format('opentype');
|
||||
}
|
||||
html,body, #reactRoot{
|
||||
height : 100vh;
|
||||
@@ -47,4 +47,4 @@ button{
|
||||
&:disabled{
|
||||
background-color : @silver !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user