mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 01:13:15 +00:00
Compare commits
105 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
825702ee1d | ||
|
|
61b58032ca | ||
|
|
546cc13c1c | ||
|
|
44649d7f51 | ||
|
|
5f9aaba262 | ||
|
|
5d5c0b9773 | ||
|
|
b681edba23 | ||
|
|
f0c412527b | ||
|
|
fcf6b8d764 | ||
|
|
8607b9dba8 | ||
|
|
ef4fa89d9a | ||
|
|
6d38a633ef | ||
|
|
b3376435b9 | ||
|
|
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 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,4 +7,5 @@ storage
|
||||
build/*
|
||||
config/local.*
|
||||
|
||||
todo.md
|
||||
todo.md
|
||||
startDB.bat
|
||||
|
||||
47
README.md
47
README.md
@@ -1,35 +1,48 @@
|
||||
# The Homebrewery
|
||||
The Homebrewery is a tool for making authentic 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 issue [here](/issues).
|
||||
### Installation
|
||||
First, install two programs that the Homebrewery requires to run.
|
||||
|
||||
|
||||
### local dev
|
||||
The 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 will 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.
|
||||
|
||||
### 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). Which means you are free to use The Homebrewery in any way that you want, except for claiming that you made it yourself.
|
||||
## 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,36 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
74
client/admin/brewCleanup/brewCleanup.jsx
Normal file
74
client/admin/brewCleanup/brewCleanup.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
91
client/admin/brewCompress/brewCompress.jsx
Normal file
91
client/admin/brewCompress/brewCompress.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
81
client/admin/brewLookup/brewLookup.jsx
Normal file
81
client/admin/brewLookup/brewLookup.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
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' rel='noopener noreferrer'>/edit/{brew.editId}</a></div>
|
||||
<div><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>/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,66 +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,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
search : function(){
|
||||
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],
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
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,170 +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,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
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;
|
||||
const newCache = _.extend({}, this.state.brewCache);
|
||||
newCache[page] = res.body.brews;
|
||||
this.setState({
|
||||
brewCache : newCache,
|
||||
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' rel='noopener noreferrer'>{brew.editId}</a></td>
|
||||
<td><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>{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;
|
||||
}
|
||||
}
|
||||
45
client/admin/stats/stats.jsx
Normal file
45
client/admin/stats/stats.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,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;
|
||||
@@ -132,7 +133,10 @@ const BrewRenderer = createClass({
|
||||
style={{ height: this.state.height }}>
|
||||
|
||||
<ErrorBar errors={this.props.errors} />
|
||||
<RenderWarnings />
|
||||
<div className='popups'>
|
||||
<RenderWarnings />
|
||||
<NotificationPopup />
|
||||
</div>
|
||||
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderPages()}
|
||||
|
||||
@@ -36,13 +36,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 @@
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,13 @@ 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}`)
|
||||
.send()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -8,9 +8,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' rel='noopener noreferrer'>
|
||||
<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' rel='noopener noreferrer'>
|
||||
<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}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
@@ -10,7 +10,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');
|
||||
@@ -186,14 +186,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>;
|
||||
|
||||
@@ -7,7 +7,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 +33,7 @@ const ErrorPage = createClass({
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<IssueNavItem />
|
||||
<RecentNavItem.both errorId={this.props.errorId} />
|
||||
<RecentNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
|
||||
@@ -8,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 AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
|
||||
|
||||
@@ -59,7 +59,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'>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -9,6 +9,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 +136,7 @@ const NewPage = createClass({
|
||||
{this.renderSaveButton()}
|
||||
{this.renderLocalPrintButton()}
|
||||
<IssueNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
|
||||
@@ -6,8 +6,7 @@ const cx = require('classnames');
|
||||
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');
|
||||
|
||||
|
||||
@@ -52,12 +51,11 @@ const SharePage = createClass({
|
||||
</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;
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,13 @@ 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}`)
|
||||
.send()
|
||||
|
||||
@@ -6,7 +6,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 +42,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 @@
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
3111
package-lock.json
generated
3111
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "2.8.1",
|
||||
"version": "2.8.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/stolksdorf/homebrewery.git"
|
||||
"url": "git://github.com/naturalcrit/homebrewery.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
@@ -36,31 +36,30 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-preset-env": "^1.1.8",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"basic-auth": "^2.0.0",
|
||||
"body-parser": "^1.14.2",
|
||||
"classnames": "^2.2.0",
|
||||
"codemirror": "^5.22.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"body-parser": "^1.19.0",
|
||||
"classnames": "^2.2.6",
|
||||
"codemirror": "^5.52.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"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": "^5.0.13",
|
||||
"express": "^4.17.1",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"marked": "^0.3.19",
|
||||
"moment": "^2.24.0",
|
||||
"mongoose": "^5.9.2",
|
||||
"nconf": "^0.10.0",
|
||||
"pico-router": "^2.1.0",
|
||||
"react": "^16.3.1",
|
||||
"react-dom": "^16.3.1",
|
||||
"shortid": "^2.2.4",
|
||||
"superagent": "^3.8.2",
|
||||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"shortid": "^2.2.15",
|
||||
"superagent": "^5.2.2",
|
||||
"vitreum": "^4.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"pico-check": "^1.0.3"
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.19.0",
|
||||
"pico-check": "^1.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const app = express();
|
||||
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()
|
||||
@@ -15,7 +16,8 @@ const config = require('nconf')
|
||||
|
||||
//DB
|
||||
const mongoose = require('mongoose');
|
||||
mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit');
|
||||
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.');
|
||||
@@ -23,7 +25,7 @@ mongoose.connection.on('error', ()=>{
|
||||
});
|
||||
|
||||
|
||||
//Account MIddleware
|
||||
//Account Middleware
|
||||
app.use((req, res, next)=>{
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try {
|
||||
|
||||
@@ -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)=>{
|
||||
render('admin', templateFn, {
|
||||
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') return next();
|
||||
if(req.header('x-forwarded-proto') !== 'https') {
|
||||
return res.redirect(302, `https://${req.get('Host')}${req.url}`);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
const _ = require('lodash');
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const router = require('express').Router();
|
||||
const zlib = require('zlib');
|
||||
|
||||
// const getTopBrews = (cb)=>{
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
@@ -31,9 +32,14 @@ router.post('/api', (req, res)=>{
|
||||
req.body,
|
||||
{ authors: authors }
|
||||
));
|
||||
|
||||
if(!newHomebrew.title){
|
||||
newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
|
||||
}
|
||||
|
||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text); // Compress brew text to binary before saving
|
||||
newHomebrew.text = undefined; // Delete the non-binary text field since it's not needed anymore
|
||||
|
||||
newHomebrew.save((err, obj)=>{
|
||||
if(err){
|
||||
console.error(err, err.toString(), err.stack);
|
||||
@@ -47,7 +53,10 @@ router.put('/api/update/:id', (req, res)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
brew = _.merge(brew, req.body);
|
||||
brew.textBin = zlib.deflateRawSync(req.body.text); // Compress brew text to binary before saving
|
||||
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore
|
||||
brew.updatedAt = new Date();
|
||||
|
||||
if(req.account) brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
|
||||
brew.markModified('authors');
|
||||
@@ -67,11 +76,26 @@ router.put('/api/update/:id', (req, res)=>{
|
||||
router.get('/api/remove/:id', (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();
|
||||
});
|
||||
const brew = objs[0];
|
||||
|
||||
// Remove current user as author
|
||||
if(req.account){
|
||||
brew.authors = _.pull(brew.authors, req.account.username);
|
||||
brew.markModified('authors');
|
||||
}
|
||||
|
||||
// Delete brew if there are no authors left
|
||||
if(!brew.authors.length)
|
||||
brew.remove((err)=>{
|
||||
if(err) return res.status(500).send('Error while removing');
|
||||
return res.status(200).send();
|
||||
});
|
||||
// Otherwise, save the brew with updated author list
|
||||
else
|
||||
brew.save((err, savedBrew)=>{
|
||||
if(err) throw err;
|
||||
return res.status(200).send(savedBrew);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,4 +151,4 @@ module.exports = function(app){
|
||||
|
||||
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]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ const RenderWarnings = createClass({
|
||||
if(!isChrome){
|
||||
return <li key='chrome'>
|
||||
<em>Built for Chrome </em> <br />
|
||||
Other browsers do not support
|
||||
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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -15,8 +15,8 @@ renderer.html = function (html) {
|
||||
|
||||
const sanatizeScriptTags = (content)=>{
|
||||
return content
|
||||
.replace(/<script/g, '<script')
|
||||
.replace(/<\/script>/g, '</script>');
|
||||
.replace(/<script/ig, '<script')
|
||||
.replace(/<\/script>/ig, '</script>');
|
||||
};
|
||||
|
||||
const tagTypes = ['div', 'span', 'a'];
|
||||
|
||||
Reference in New Issue
Block a user