0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-26 22:43:07 +00:00

Compare commits

..

54 Commits

Author SHA1 Message Date
Scott Tolksdorf
cbab4f4959 added todo 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
22d9982888 Added support for title description and thumbnail images 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
76ced9ca49 Added comma parsing to the block code 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
b1db8040a4 Added a todo for generic line styling 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
c8b089f7fb Added new lexer for handling the new block syntax 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
97c0443c76 Fixed bug where new page was storing null brews 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
c470bed591 'Created 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
4593099914 Merge branch 'newAdmin' into v3 2017-01-28 16:36:25 -05:00
Scott Tolksdorf
6030134de2 New brew search finished on admin page 2017-01-28 16:35:48 -05:00
Scott Tolksdorf
7e6f42f062 Merge branch 'search' into newAdmin 2017-01-28 15:39:05 -05:00
Scott Tolksdorf
75111acefb All tests should be done, phew 2017-01-28 12:25:26 -05:00
Scott Tolksdorf
26bcb3395a Fixing edge cases in the search tests 2017-01-27 19:47:45 -05:00
Scott Tolksdorf
a826aaffd9 created a brew generator and chai plugin for easier testing 2017-01-27 18:38:51 -05:00
Scott Tolksdorf
8018442f25 Upgrading the brew generation for testing 2017-01-27 10:47:38 -05:00
Scott Tolksdorf
8e58e5aca9 MOved the brews search to its own file, writing out more tests 2017-01-27 10:36:07 -05:00
Scott Tolksdorf
2f82d3875e Adding a new script from populating the DB with a bunch of random brews 2017-01-27 09:56:44 -05:00
Scott Tolksdorf
efee8ff05c Basic search is working 2017-01-23 00:35:30 -05:00
Scott Tolksdorf
a405c7cfb2 Stubbing out tests for searching 2017-01-22 13:42:40 -05:00
Scott Tolksdorf
dfcb04fd09 Setting up search tests 2017-01-22 13:42:32 -05:00
Scott Tolksdorf
728277f861 Removing invalid brews is working 2017-01-22 13:41:16 -05:00
Scott Tolksdorf
0878439750 Fixed issue with arrays not being saved 2017-01-22 13:41:08 -05:00
Scott Tolksdorf
e77532acef Added tests for admin routes, lookup is working 2017-01-22 13:41:00 -05:00
Scott Tolksdorf
cd2eb5fdce Adding in lookup route 2017-01-22 13:40:39 -05:00
Scott Tolksdorf
37de888f03 Creating a brand new admin page 2017-01-22 13:40:29 -05:00
Scott Tolksdorf
1aa79b32d9 Remove chrome warnings from after rebase 2017-01-22 12:55:00 -05:00
Scott Tolksdorf
894e345a44 Fixed bug where new page loaded null brews sometimes 2017-01-22 12:49:35 -05:00
Scott Tolksdorf
07f249b23e Local login now working great 2017-01-22 12:49:35 -05:00
Scott Tolksdorf
baaa82ed34 Added in a logout to the user page 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
0d0f0d8eb0 Adding in env configs and aextra protection on dev routes 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
d77fa0a3dc Backend of local login working 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
a26c4e2092 Cleaned up the admin routes 2017-01-22 12:48:48 -05:00
Scott Tolksdorf
ca40ec5a2d Added in full test coverage of current spec 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
987363ed41 Fixing the pathing to the build folder 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
7b38bccec1 config file for tests 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
174c2973f7 Split off app into own file 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
66ca09b36d Both types of tests are now working 2017-01-22 12:46:16 -05:00
Scott Tolksdorf
5820564894 added nodemon'd test npm task 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
3dc4c13178 Something is up 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
537a75b2ab Triyng to setup api tests 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
a0bc4fddf8 moving the db setup out 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
25e0a1607a whatever 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
68ecf749ea Stubbing out test files 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
10f4759471 adding in some api tests 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
5ba3f98696 Finally testing, things should be working a bit better now 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
95c09ba7ad moved welcome message and adding in egads errors 2017-01-22 12:45:16 -05:00
Scott Tolksdorf
1173af5803 'Created 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
924b398768 Added new navitems 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
41303e6918 'Share 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
f75f60aa1e Edit page finally converted over 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
f4cf288f27 newPage is now working, working on editpage 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
8abf6abf99 Updating pico-flux and vitreum to latest 2017-01-22 12:39:42 -05:00
Scott Tolksdorf
95aa803c61 Trying to fix prod builds breaking 2017-01-22 12:39:27 -05:00
Scott Tolksdorf
47396e5c7e Added smart componenets, page line number highlighting 2017-01-22 12:39:06 -05:00
Scott Tolksdorf
7581d155a6 Updating libs and adding basic flux 2017-01-22 12:36:45 -05:00
131 changed files with 5051 additions and 11908 deletions

View File

@@ -1,33 +0,0 @@
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
build:
docker:
- image: circleci/node:8.9
- image: circleci/mongo:3.4.4
working_directory: ~/repo
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "package.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: npm install
- save_cache:
paths:
- node_modules
key: v1-dependencies-{{ checksum "package.json" }}
# run tests!
- run: npm run circleci

View File

@@ -1,78 +0,0 @@
module.exports = {
root: true,
parserOptions : {
ecmaVersion : 9,
sourceType : 'module',
ecmaFeatures : {
jsx : true
}
},
env : {
browser : true,
},
plugins : ['react'],
rules : {
/** Errors **/
'camelcase' : ['error', { properties: 'never' }],
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'no-array-constructor' : 'error',
'no-iterator' : 'error',
'no-nested-ternary' : 'error',
'no-new-object' : 'error',
'no-proto' : 'error',
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
'react/jsx-uses-react' : 'error',
'react/prefer-es6-class' : ['error', 'never'],
/** Warnings **/
'max-lines' : ['warn', {
max : 200,
skipComments : true,
skipBlankLines : true,
}],
'max-depth' : ['warn', { max: 4 }],
'max-params' : ['warn', { max: 4 }],
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
'no-unused-vars' : ['warn', {
vars : 'all',
args : 'none',
varsIgnorePattern : 'config|_|cx|createClass'
}],
'react/jsx-uses-vars' : 'warn',
/** Fixable **/
'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 } ],
'semi' : ['warn', 'always'],
/** Whitespace **/
'array-bracket-spacing' : ['warn', 'never'],
'arrow-spacing' : ['warn', { before: false, after: false }],
'comma-spacing' : ['warn', { before: false, after: true }],
'indent' : ['warn', 'tab'],
'keyword-spacing' : ['warn', {
before : true,
after : true,
overrides : {
if : { 'before': false, 'after': false }
}
}],
'key-spacing' : ['warn', {
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
singleLine : { beforeColon: false, afterColon: true }
}],
'linebreak-style' : ['warn', 'unix'],
'no-trailing-spaces' : 'warn',
'no-whitespace-before-property' : 'warn',
'object-curly-spacing' : ['warn', 'always'],
'react/jsx-indent-props' : ['warn', 'tab'],
'space-in-parens' : ['warn', 'never'],
'template-curly-spacing' : ['warn', 'never'],
}
};

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
package-lock.json binary

View File

@@ -1,21 +1,9 @@
<!-- CLICK "Preview" FOR INSTRUCTIONS IN A MORE READABLE FORMAT -->
## Before you submit
- Support questions are better asked on the subreddit [r/homebrewery](https://www.reddit.com/r/homebrewery/)
- Read the [contributing guidelines](https://github.com/stolksdorf/homebrewery/blob/master/contributing.md).
- If it's an issue, please make sure it's reproducible
- Ensure the issue isn't already reported.
*Delete the above section and the instructions in the sections below before submitting*
## Description
If this is a *feature request*, explain why it should be added. Specific use-cases are best. ### Additional Details
For *bug reports*, please provide as much *relevant* info as possible.
**Share Link** : **Share Link** :

14
.gitignore vendored
View File

@@ -1,10 +1,16 @@
# Logs
logs
*.log
#Ignore our built files
build/*
# Ignore sensitive stuff
config/local.json
node_modules node_modules
storage storage
.idea .idea
*.swp *.swp
*.log
build/*
config/local.*
todo.md todo.md

View File

@@ -1,15 +1,5 @@
# changelog # changelog
### Saturday, 22/04/217 - 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.
### Saturday, 18/02/2017 - v2.7.2
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
## BIG NEWS ## BIG NEWS
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues. With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues.
@@ -36,7 +26,8 @@ All brews made previous to the release of v3.0.0 will still render normally.
- Fixed realtime renderer not functioning if loaded with malformed html on load (thanks u/RattiganIV re:247) - Fixed realtime renderer not functioning if loaded with malformed html on load (thanks u/RattiganIV re:247)
- Removed a lot of unused files in shared - Removed a lot of unused files in shared
- vitreum v4 now lets me use codemirror as a pure node dependacy - vitreum v4 now lets me use codemirror as a pure node dependacy
- Moved the brew editor and renderer into shared, and made a new hybrid component for them
- Added a line highlighter to lines with new pages
### Saturday, 03/12/2016 - v2.6.0 ### Saturday, 03/12/2016 - v2.6.0

View File

@@ -1,39 +1,42 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash');
const cx = require('classnames'); const Nav = require('naturalcrit/nav/nav.jsx');
const HomebrewAdmin = require('./homebrewAdmin/homebrewAdmin.jsx'); const BrewLookup = require('./brewLookup/brewLookup.jsx');
const AdminSearch = require('./adminSearch/adminSearch.jsx');
const Admin = createClass({ const InvalidBrew = require('./invalidBrew/invalidBrew.jsx');
getDefaultProps : function() {
return { const Admin = React.createClass({
url : '', getDefaultProps: function() {
admin_key : '', return {
homebrews : [], admin_key : '',
}; };
}, },
render : function(){ renderNavbar : function(){
return ( return <Nav.base>
<div className='admin'> <Nav.section>
<Nav.item icon='fa-magic' className='homebreweryLogo'>
<header> Homebrewery Admin
<div className='container'> </Nav.item>
<i className='fa fa-rocket' /> </Nav.section>
naturalcrit admin </Nav.base>
</div> },
</header>
render : function(){
<div className='container'> return <div className='admin'>
{this.renderNavbar()}
<HomebrewAdmin homebrews={this.props.homebrews} admin_key={this.props.admin_key} /> <main className='content'>
</div> <BrewLookup adminKey={this.props.admin_key} />
<AdminSearch adminKey={this.props.admin_key} />
</div> <div className='dangerZone'>Danger Zone</div>
);
} <InvalidBrew adminKey={this.props.admin_key} />
}); </main>
</div>
module.exports = Admin; }
});
module.exports = Admin;

View File

@@ -1,39 +1,53 @@
@import 'naturalcrit/styles/reset.less';
@import 'naturalcrit/styles/elements.less';
@import 'naturalcrit/styles/animations.less';
@import 'naturalcrit/styles/colors.less';
@import 'naturalcrit/styles/tooltip.less';
@import 'font-awesome/css/font-awesome.css'; @import 'naturalcrit/styles/core.less';
html,body, #reactRoot{
html,body, #reactContainer, .naturalCrit{
min-height : 100%; min-height : 100%;
} }
@sidebarWidth : 250px;
body{ body{
background-color : #eee; height : 100%;
font-family : 'Open Sans', sans-serif;
color : #4b5055;
font-weight : 100;
text-rendering : optimizeLegibility;
margin : 0; margin : 0;
padding : 0; padding : 0;
height : 100%; background-color : #ddd;
font-family : 'Open Sans', sans-serif;
font-weight : 100;
color : #4b5055;
text-rendering : optimizeLegibility;
} }
.admin {
.admin{ nav {
header{
background-color : @red; background-color : @red;
font-size: 2em; .navItem{
padding : 20px 0px; background-color : @red;
color : white; }
margin-bottom: 30px; .homebreweryLogo{
i{ font-family : CodeBold;
margin-right: 30px; font-size : 12px;
color : white;
div{
margin-top : 2px;
margin-bottom : -2px;
}
}
}
h1{
margin-bottom : 10px;
font-size : 2em;
font-weight : 800;
border-bottom : 1px solid #ddd;
}
main.content{
width : 1000px;
margin : 0 auto;
padding : 50px 20px;
background-color : white;
.dangerZone{
margin : 30px 0px;
padding : 10px 20px;
background : repeating-linear-gradient(45deg, @yellow, @yellow 10px, darken(#333, 10%) 10px, darken(#333, 10%) 20px);
font-size : 1em;
font-weight : 800;
color : white;
text-transform : uppercase;
} }
} }
} }

View File

@@ -0,0 +1,86 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const BrewTable = require('../brewTable/brewTable.jsx');
const LIMIT = 10;
const AdminSearch = React.createClass({
getDefaultProps: function() {
return {
adminKey : '',
};
},
getInitialState: function() {
return {
totalBrews : 1,
brews: [],
searching : false,
error : null,
page : 1,
searchTerms : ''
};
},
handleSearch : function(e){
this.setState({
searchTerms : e.target.value
});
},
handlePage : function(e){
this.setState({
page : e.target.value
});
},
search : function(){
this.setState({ searching : true, error : null });
request.get(`/api/brew`)
.query({
terms : this.state.searchTerms,
limit : LIMIT,
page : this.state.page - 1
})
.set('x-homebrew-admin', this.props.adminKey)
.end((err, res) => {
if(err){
this.setState({
searching : false,
error : err && err.toString()
});
}else{
this.setState({
brews : res.body.brews,
totalBrews : res.body.total
});
}
});
},
render: function(){
return <div className='adminSearch'>
<h1>Admin Search</h1>
<div className='controls'>
<input className='search' type='text' value={this.state.searchTerms} onChange={this.handleSearch} />
<button onClick={this.search}> <i className='fa fa-search' /> search </button>
<div className='page'>
page:
<input type='text' value={this.state.page} onChange={this.handlePage} />
/ {Math.ceil(this.state.totalBrews / LIMIT)}
</div>
</div>
<BrewTable brews={this.state.brews} />
</div>
}
});
module.exports = AdminSearch;

View File

@@ -0,0 +1,17 @@
.adminSearch{
.controls{
margin-bottom : 20px;
input.search{
height : 33px;
padding : 10px;
}
.page {
float : right;
font-weight : 800;
input{
width : 20px;
}
}
}
}

View File

@@ -1,67 +1,85 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require('superagent'); const request = require('superagent');
const Moment = require('moment'); const BrewTable = require('../brewTable/brewTable.jsx');
const BrewLookup = React.createClass({
const BrewLookup = createClass({ getDefaultProps: function() {
getDefaultProps : function() {
return { return {
adminKey : '', adminKey : '',
}; };
}, },
getInitialState : function() { getInitialState: function() {
return { return {
query : '', query:'',
resultBrew : null, resultBrew : null,
searching : false searching : false,
error : null
}; };
}, },
handleChange : function(e){ handleChange : function(e){
this.setState({ this.setState({
query : e.target.value query : e.target.value
}); })
}, },
lookup : function(){ lookup : function(){
this.setState({ searching: true }); this.setState({ searching : true, error : null });
request.get(`/admin/lookup/${this.state.query}`) request.get(`/admin/lookup/${this.state.query}`)
.query({ admin_key: this.props.adminKey }) .set('x-homebrew-admin', this.props.adminKey)
.end((err, res)=>{ .end((err, res) => {
this.setState({ this.setState({
searching : false, searching : false,
error : err && err.toString(),
resultBrew : (err ? null : res.body) resultBrew : (err ? null : res.body)
}); });
}); })
}, },
renderFoundBrew : function(){ renderFoundBrew : function(){
if(this.state.searching) return <div className='searching'><i className='fa fa-spin fa-spinner' /></div>; 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>; if(!this.state.resultBrew) return <div className='noBrew'>No brew found.</div>;
return <BrewTable brews={[this.state.resultBrew ]} />
/*
const brew = this.state.resultBrew; const brew = this.state.resultBrew;
return <div className='brewRow'> return <div className='brewRow'>
<div>{brew.title}</div> <div>{brew.title}</div>
<div>{brew.authors.join(', ')}</div> <div>{brew.authors.join(', ')}</div>
<div><a href={`/edit/${brew.editId}`} target='_blank'>/edit/{brew.editId}</a></div> <div><a href={'/edit/' + brew.editId} target='_blank'>{brew.editId}</a></div>
<div><a href={`/share/${brew.shareId}`} target='_blank'>/share/{brew.shareId}</a></div> <div><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></div>
<div>{Moment(brew.updatedAt).fromNow()}</div> <div>{Moment(brew.updatedAt).fromNow()}</div>
<div>{brew.views}</div> <div>{brew.views}</div>
</div>; <div>
<div className='deleteButton'>
<i className='fa fa-trash' />
</div>
</div>
</div>
*/
}, },
render : function(){ renderError : function(){
if(!this.state.error) return;
return <div className='error'>
{this.state.error}
</div>
},
render: function(){
return <div className='brewLookup'> return <div className='brewLookup'>
<h1>Brew Lookup</h1> <h1>Brew Lookup</h1>
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id...' /> <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> <button onClick={this.lookup}><i className='fa fa-search'/></button>
{this.renderFoundBrew()} {this.renderFoundBrew()}
</div>; {this.renderError()}
</div>
} }
}); });

View File

@@ -0,0 +1,13 @@
.brewLookup{
height : 200px;
input{
height : 33px;
margin-bottom : 20px;
padding : 0px 10px;
}
.error{
font-weight : 800;
color : @red;
}
}

View File

@@ -0,0 +1,54 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Moment = require('moment');
//TODO: Add in delete
const BrewTable = React.createClass({
getDefaultProps: function() {
return {
brews : []
};
},
renderRows : function(){
return _.map(this.props.brews, (brew) => {
let authors = 'None.';
if(brew.authors && brew.authors.length) authors = brew.authors.join(', ');
return <tr className={cx('brewRow', {'isEmpty' : brew.text == "false"})} key={brew.shareId || brew}>
<td>{brew.title}</td>
<td>{authors}</td>
<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.updatedAt).fromNow()}</td>
<td>{brew.views}</td>
<td className='deleteButton'>
<i className='fa fa-trash' />
</td>
</tr>
});
},
render: function(){
return <table className='brewTable'>
<thead>
<tr>
<th>Title</th>
<th>Authors</th>
<th>Edit Link</th>
<th>Share Link</th>
<th>Last Updated</th>
<th>Views</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
{this.renderRows()}
</tbody>
</table>
}
});
module.exports = BrewTable;

View File

@@ -0,0 +1,44 @@
table.brewTable{
th{
padding : 10px;
background-color : fade(@blue, 20%);
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;
}
}
*/
}
}

View File

@@ -1,8 +0,0 @@
.brewLookup{
height : 200px;
input{
height : 33px;
padding : 0px 10px;
margin-bottom: 20px;
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,54 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const BrewTable = require('../brewTable/brewTable.jsx');
const InvalidBrew = React.createClass({
getDefaultProps: function() {
return {
adminKey : '',
};
},
getInitialState: function() {
return {
brews: []
};
},
getInvalid : function(){
request.get(`/admin/invalid`)
.set('x-homebrew-admin', this.props.adminKey)
.end((err, res) => {
this.setState({
brews : res.body
});
})
},
removeInvalid : function(){
if(!this.state.brews.length) return;
if(!confirm(`Are you sure you want to remove ${this.state.brews.length} brews`)) return;
if(!confirm('Sure you are sure?')) return;
request.delete(`/admin/invalid`)
.set('x-homebrew-admin', this.props.adminKey)
.end((err, res) => {
console.log(err, res.body);
alert('Invalid brews removed!');
this.getInvalid();
})
},
render: function(){
return <div className='invalidBrew'>
<h1>Remove Invalid Brews</h1>
<div>This will removes all brews older than 3 days and shorter than a tweet.</div>
<button className='get' onClick={this.getInvalid}> Get Invalid Brews</button>
<button className='remove' disabled={this.state.brews.length == 0} onClick={this.removeInvalid}> Remove invalid Brews</button>
<BrewTable brews={this.state.brews} />
</div>
}
});
module.exports = InvalidBrew;

View File

@@ -0,0 +1,5 @@
.invalidBrew{
button{
margin: 10px 4px;
}
}

View File

@@ -1,42 +0,0 @@
const _ = require('lodash');
module.exports = function(classname){
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']);
classname = classname.toLowerCase();
const hitDie = _.sample([4, 6, 8, 10, 12]);
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
return [
'## Class Features',
`As a ${classname}, you gain the following class features`,
'#### Hit Points',
'___',
`- **Hit Dice:** 1d${hitDie} per ${classname} level`,
`- **Hit Points at 1st Level:** ${hitDie} + your Constitution modifier`,
`- **Hit Points at Higher Levels:** 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st`,
'',
'#### Proficiencies',
'___',
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
'',
'___',
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
'',
'#### Equipment',
'You start with the following equipment, in addition to the equipment granted by your background:',
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',
'- *(a)* five javelins or *(b)* any simple melee weapon',
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
'\n\n\n'
].join('\n');
};

View File

@@ -1,114 +0,0 @@
const _ = require('lodash');
const features = [
'Astrological Botany',
'Astrological Chemistry',
'Biochemical Sorcery',
'Civil Alchemy',
'Consecrated Biochemistry',
'Demonic Anthropology',
'Divinatory Mineralogy',
'Genetic Banishing',
'Hermetic Geography',
'Immunological Incantations',
'Nuclear Illusionism',
'Ritual Astronomy',
'Seismological Divination',
'Spiritual Biochemistry',
'Statistical Occultism',
'Police Necromancer',
'Sixgun Poisoner',
'Pharmaceutical Gunslinger',
'Infernal Banker',
'Spell Analyst',
'Gunslinger Corruptor',
'Torque Interfacer',
'Exo Interfacer',
'Gunpowder Torturer',
'Orbital Gravedigger',
'Phased Linguist',
'Mathematical Pharmacist',
'Plasma Outlaw',
'Malefic Chemist',
'Police Cultist'
];
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
const levels = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th', '11th', '12th', '13th', '14th', '15th', '16th', '17th', '18th', '19th', '20th'];
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
const getFeature = (level)=>{
let res = [];
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
res = ['Ability Score Improvement'];
}
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
if(!res.length) return '─';
return res.join(', ');
};
module.exports = {
full : function(){
const classname = _.sample(classnames);
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
const drawSlots = function(Slots){
let slots = Number(Slots);
return _.times(9, function(i){
const max = maxes[i];
if(slots < 1) return '—';
const res = _.min([max, slots]);
slots -= res;
return res;
}).join(' | ');
};
let cantrips = 3;
let spells = 1;
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${
_.map(levels, function(levelName, level){
const res = [
levelName,
`+${profBonus[level]}`,
getFeature(level),
cantrips,
spells,
drawSlots(slots)
].join(' | ');
cantrips += _.random(0, 1);
spells += _.random(0, 1);
slots += _.random(0, 2);
return `| ${res} |`;
}).join('\n')}\n</div>\n\n`;
},
half : function(){
const classname = _.sample(classnames);
let featureScore = 1;
return `<div class='classTable'>\n##### The ${classname}\n` +
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
`|:---:|:---:|:---|:---:|\n${
_.map(levels, function(levelName, level){
const res = [
levelName,
`+${profBonus[level]}`,
getFeature(level),
`+${featureScore}`
].join(' | ');
featureScore += _.random(0, 1);
return `| ${res} |`;
}).join('\n')}\n</div>\n\n`;
}
};

View File

@@ -1,117 +0,0 @@
const _ = require('lodash');
const titles = [
'The Burning Gallows',
'The Ring of Nenlast',
'Below the Blind Tavern',
'Below the Hungering River',
'Before Bahamut\'s Land',
'The Cruel Grave from Within',
'The Strength of Trade Road',
'Through The Raven Queen\'s Worlds',
'Within the Settlement',
'The Crown from Within',
'The Merchant Within the Battlefield',
'Ioun\'s Fading Traveler',
'The Legion Ingredient',
'The Explorer Lure',
'Before the Charming Badlands',
'The Living Dead Above the Fearful Cage',
'Vecna\'s Hidden Sage',
'Bahamut\'s Demonspawn',
'Across Gruumsh\'s Elemental Chaos',
'The Blade of Orcus',
'Beyond Revenge',
'Brain of Insanity',
'Breed Battle!, A New Beginning',
'Evil Lake, A New Beginning',
'Invasion of the Gigantic Cat, Part II',
'Kraken War 2020',
'The Body Whisperers',
'The Diabolical Tales of the Ape-Women',
'The Doctor Immortal',
'The Doctor from Heaven',
'The Graveyard',
'Azure Core',
'Core Battle',
'Core of Heaven: The Guardian of Amazement',
'Deadly Amazement III',
'Dry Chaos IX',
'Gate Thunder',
'Guardian: Skies of the Dark Wizard',
'Lute of Eternity',
'Mercury\'s Planet: Brave Evolution',
'Ruby of Atlantis: The Quake of Peace',
'Sky of Zelda: The Thunder of Force',
'Vyse\'s Skies',
'White Greatness III',
'Yellow Divinity',
'Zidane\'s Ghost'
];
const subtitles = [
'In an ominous universe, a botanist opposes terrorism.',
'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.',
'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.',
'In an evil empire of horror, two rangers battle the forces of hell.',
'In a lost city, in an age of sorcery, a librarian quests for revenge.',
'In a universe of illusions and danger, three time travellers and an adventurer search for justice.',
'In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.',
'In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.',
'In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.',
'In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.',
'In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.',
'In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.',
'In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.',
'In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.',
'In a kingdom of deception, a reporter searches for fame.',
'In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.',
'In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.',
'In a dark city of confusion, three swordswomen and a singer battle lawlessness.',
'In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.',
'In a kingdom of panic, six adventurers oppose lawlessness.',
'In a land of dreams and hopelessness, three hackers and a cyborg search for justice.',
'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.',
'In a wicked universe, five seers fight lawlessness.',
'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.',
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.',
'In a cursed empire, five inventors oppose terrorism.',
'On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.',
'In a forgotten land, a reporter and a spy try to stop the apocalypse.',
'In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.',
'On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.',
'In a galaxy of dark magic, four fighters seek freedom.',
'In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.',
'In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.',
'In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.',
'In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.',
'On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.',
'In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.',
'In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.',
'In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.',
'In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.',
'In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.',
'In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.',
'In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.',
'In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.',
'In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime.'
];
module.exports = ()=>{
return `<style>
.phb#p1{ text-align:center; }
.phb#p1:after{ display:none; }
</style>
<div style='margin-top:450px;'></div>
# ${_.sample(titles)}
<div style='margin-top:25px'></div>
<div class='wide'>
##### ${_.sample(subtitles)}
</div>
\\page`;
};

View File

@@ -1,43 +0,0 @@
const _ = require('lodash');
const ClassFeatureGen = require('./classfeature.gen.js');
const ClassTableGen = require('./classtable.gen.js');
module.exports = function(){
const classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge']);
const image = _.sample(_.map([
'http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png',
'http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png',
'http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png',
'http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png',
'http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png',
'http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png',
'http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png',
'http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png',
], function(url){
return `<img src = '${url}' style='max-width:8cm;max-height:25cm' />`;
}));
return `${[
image,
'',
'```',
'```',
'<div style=\'margin-top:240px\'></div>\n\n',
`## ${classname}`,
'Cool intro stuff will go here',
'\\page',
ClassTableGen(classname),
ClassFeatureGen(classname),
].join('\n')}\n\n\n`;
};

View File

@@ -1,91 +0,0 @@
const _ = require('lodash');
const spellNames = [
'Astral Rite of Acne',
'Create Acne',
'Cursed Ramen Erruption',
'Dark Chant of the Dentists',
'Erruption of Immaturity',
'Flaming Disc of Inconvenience',
'Heal Bad Hygene',
'Heavenly Transfiguration of the Cream Devil',
'Hellish Cage of Mucus',
'Irritate Peanut Butter Fairy',
'Luminous Erruption of Tea',
'Mystic Spell of the Poser',
'Sorcerous Enchantment of the Chimneysweep',
'Steak Sauce Ray',
'Talk to Groupie',
'Astonishing Chant of Chocolate',
'Astounding Pasta Puddle',
'Ball of Annoyance',
'Cage of Yarn',
'Control Noodles Elemental',
'Create Nervousness',
'Cure Baldness',
'Cursed Ritual of Bad Hair',
'Dispell Piles in Dentist',
'Eliminate Florists',
'Illusionary Transfiguration of the Babysitter',
'Necromantic Armor of Salad Dressing',
'Occult Transfiguration of Foot Fetish',
'Protection from Mucus Giant',
'Tinsel Blast',
'Alchemical Evocation of the Goths',
'Call Fangirl',
'Divine Spell of Crossdressing',
'Dominate Ramen Giant',
'Eliminate Vindictiveness in Gym Teacher',
'Extra-Planar Spell of Irritation',
'Induce Whining in Babysitter',
'Invoke Complaining',
'Magical Enchantment of Arrogance',
'Occult Globe of Salad Dressing',
'Overwhelming Enchantment of the Chocolate Fairy',
'Sorcerous Dandruff Globe',
'Spiritual Invocation of the Costumers',
'Ultimate Rite of the Confetti Angel',
'Ultimate Ritual of Mouthwash',
];
module.exports = {
spellList : function(){
const levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
const content = _.map(levels, (level)=>{
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
return `- ${spell}`;
}).join('\n');
return `##### ${level} \n${spells} \n`;
}).join('\n');
return `<div class='spellList'>\n${content}\n</div>`;
},
spell : function(){
const level = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th'];
const spellSchools = ['abjuration', 'conjuration', 'divination', 'enchantment', 'evocation', 'illusion', 'necromancy', 'transmutation'];
let components = _.sampleSize(['V', 'S', 'M'], _.random(1, 3)).join(', ');
if(components.indexOf('M') !== -1){
components += ` (${_.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1, 3)).join(', ')})`;
}
return [
`#### ${_.sample(spellNames)}`,
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
'___',
'- **Casting Time:** 1 action',
`- **Range:** ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
`- **Components:** ${components}`,
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
'',
'A flame, equivalent in brightness to a torch, springs from from an object that you touch. ',
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
'A *continual flame* can be covered or hidden but not smothered or quenched.',
'\n\n\n'
].join('\n');
}
};

View File

@@ -1,196 +0,0 @@
const _ = require('lodash');
const genList = function(list, max){
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
};
const getMonsterName = function(){
return _.sample([
'All-devouring Baseball Imp',
'All-devouring Gumdrop Wraith',
'Chocolate Hydra',
'Devouring Peacock',
'Economy-sized Colossus of the Lemonade Stand',
'Ghost Pigeon',
'Gibbering Duck',
'Sparklemuffin Peacock Spider',
'Gum Elemental',
'Illiterate Construct of the Candy Store',
'Ineffable Chihuahua',
'Irritating Death Hamster',
'Irritating Gold Mouse',
'Juggernaut Snail',
'Juggernaut of the Sock Drawer',
'Koala of the Cosmos',
'Mad Koala of the West',
'Milk Djinni of the Lemonade Stand',
'Mind Ferret',
'Mystic Salt Spider',
'Necrotic Halitosis Angel',
'Pinstriped Famine Sheep',
'Ritalin Leech',
'Shocker Kangaroo',
'Stellar Tennis Juggernaut',
'Wailing Quail of the Sun',
'Angel Pigeon',
'Anime Sphinx',
'Bored Avalanche Sheep of the Wasteland',
'Devouring Nougat Sphinx of the Sock Drawer',
'Djinni of the Footlocker',
'Ectoplasmic Jazz Devil',
'Flatuent Angel',
'Gelatinous Duck of the Dream-Lands',
'Gelatinous Mouse',
'Golem of the Footlocker',
'Lich Wombat',
'Mechanical Sloth of the Past',
'Milkshake Succubus',
'Puffy Bone Peacock of the East',
'Rainbow Manatee',
'Rune Parrot',
'Sand Cow',
'Sinister Vanilla Dragon',
'Snail of the North',
'Spider of the Sewer',
'Stellar Sawdust Leech',
'Storm Anteater of Hell',
'Stupid Spirit of the Brewery',
'Time Kangaroo',
'Tomb Poodle',
]);
};
const getType = function(){
return `${_.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'])} ${_.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])}`;
};
const getAlignment = function(){
return _.sample([
'annoying evil',
'chaotic gossipy',
'chaotic sloppy',
'depressed neutral',
'lawful bogus',
'lawful coy',
'manic-depressive evil',
'narrow-minded neutral',
'neutral annoying',
'neutral ignorant',
'oedpipal neutral',
'silly neutral',
'unoriginal neutral',
'weird neutral',
'wordy evil',
'unaligned'
]);
};
const getStats = function(){
return `>|${_.times(6, function(){
const num = _.random(1, 20);
const mod = Math.ceil(num/2 - 5);
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
}).join('|')}|`;
};
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.',
]);
};
const genAction = function(){
const name = _.sample([
'Abdominal Drop',
'Airplane Hammer',
'Atomic Death Throw',
'Bulldog Rake',
'Corkscrew Strike',
'Crossed Splash',
'Crossface Suplex',
'DDT Powerbomb',
'Dual Cobra Wristlock',
'Dual Throw',
'Elbow Hold',
'Gory Body Sweep',
'Heel Jawbreaker',
'Jumping Driver',
'Open Chin Choke',
'Scorpion Flurry',
'Somersault Stump Fists',
'Suffering Wringer',
'Super Hip Submission',
'Super Spin',
'Team Elbow',
'Team Foot',
'Tilt-a-whirl Chin Sleeper',
'Tilt-a-whirl Eye Takedown',
'Turnbuckle Roll'
]);
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
};
module.exports = {
full : function(){
return `${[
'___',
'___',
`> ## ${getMonsterName()}`,
`>*${getType()}, ${getAlignment()}*`,
'> ___',
`> - **Armor Class** ${_.random(10, 20)}`,
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
`> - **Speed** ${_.random(0, 50)}ft.`,
'>___',
'>|STR|DEX|CON|INT|WIS|CHA|',
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
getStats(),
'>___',
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
`> - **Senses** passive Perception ${_.random(3, 20)}`,
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
'> ___',
_.times(_.random(3, 6), function(){
return genAbilities();
}).join('\n>\n'),
'> ### Actions',
_.times(_.random(4, 6), function(){
return genAction();
}).join('\n>\n'),
].join('\n')}\n\n\n`;
},
half : function(){
return `${[
'___',
`> ## ${getMonsterName()}`,
`>*${getType()}, ${getAlignment()}*`,
'> ___',
`> - **Armor Class** ${_.random(10, 20)}`,
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
`> - **Speed** ${_.random(0, 50)}ft.`,
'>___',
'>|STR|DEX|CON|INT|WIS|CHA|',
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
getStats(),
'>___',
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
`> - **Senses** passive Perception ${_.random(3, 20)}`,
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
'> ___',
_.times(_.random(0, 2), function(){
return genAbilities();
}).join('\n>\n'),
'> ### Actions',
_.times(_.random(1, 2), function(){
return genAction();
}).join('\n>\n'),
].join('\n')}\n\n\n`;
}
};

View File

@@ -1,268 +0,0 @@
/* eslint-disable max-lines */
const MagicGen = require('./magic.gen.js');
const ClassTableGen = require('./classtable.gen.js');
const MonsterBlockGen = require('./monsterblock.gen.js');
const ClassFeatureGen = require('./classfeature.gen.js');
const CoverPageGen = require('./coverpage.gen.js');
const TableOfContentsGen = require('./tableOfContents.gen.js');
module.exports = [
{
groupName : 'Editor',
icon : 'fa-pencil',
snippets : [
{
name : 'Column Break',
icon : 'fa-columns',
gen : '```\n```\n\n'
},
{
name : 'New Page',
icon : 'fa-file-text',
gen : '\\page\n\n'
},
{
name : 'Vertical Spacing',
icon : 'fa-arrows-v',
gen : '<div style=\'margin-top:140px\'></div>\n\n'
},
{
name : 'Wide Block',
icon : 'fa-arrows-h',
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n'
},
{
name : 'Image',
icon : 'fa-image',
gen : [
'<img ',
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
' style=\'width:325px\' />',
'Credit: Kyounghwan Kim'
].join('\n')
},
{
name : 'Background Image',
icon : 'fa-tree',
gen : [
'<img ',
' src=\'http://i.imgur.com/hMna6G0.png\' ',
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
].join('\n')
},
{
name : 'Page Number',
icon : 'fa-bookmark',
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
},
{
name : 'Auto-incrementing Page Number',
icon : 'fa-sort-numeric-asc',
gen : '<div class=\'pageNumber auto\'></div>\n'
},
{
name : 'Link to page',
icon : 'fa-link',
gen : '[Click here](#p3) to go to page 3\n'
},
{
name : 'Table of Contents',
icon : 'fa-book',
gen : TableOfContentsGen
},
]
},
/************************* PHB ********************/
{
groupName : 'PHB',
icon : 'fa-book',
snippets : [
{
name : 'Spell',
icon : 'fa-magic',
gen : MagicGen.spell,
},
{
name : 'Spell List',
icon : 'fa-list',
gen : MagicGen.spellList,
},
{
name : 'Class Feature',
icon : 'fa-trophy',
gen : ClassFeatureGen,
},
{
name : 'Note',
icon : 'fa-sticky-note',
gen : function(){
return [
'> ##### Time to Drop Knowledge',
'> Use notes to point out some interesting information. ',
'> ',
'> **Tables and lists** both work within a note.'
].join('\n');
},
},
{
name : 'Descriptive Text Box',
icon : 'fa-sticky-note-o',
gen : function(){
return [
'<div class=\'descriptive\'>',
'##### Time to Drop Knowledge',
'Use notes to point out some interesting information. ',
'',
'**Tables and lists** both work within a note.',
'</div>'
].join('\n');
},
},
{
name : 'Monster Stat Block',
icon : 'fa-bug',
gen : MonsterBlockGen.half,
},
{
name : 'Wide Monster Stat Block',
icon : 'fa-paw',
gen : MonsterBlockGen.full,
},
{
name : 'Cover Page',
icon : 'fa-file-word-o',
gen : CoverPageGen,
},
]
},
/********************* TABLES *********************/
{
groupName : 'Tables',
icon : 'fa-table',
snippets : [
{
name : 'Class Table',
icon : 'fa-table',
gen : ClassTableGen.full,
},
{
name : 'Half Class Table',
icon : 'fa-list-alt',
gen : ClassTableGen.half,
},
{
name : 'Table',
icon : 'fa-th-list',
gen : function(){
return [
'##### Cookie Tastiness',
'| Tastiness | Cookie Type |',
'|:----:|:-------------|',
'| -5 | Raisin |',
'| 8th | Chocolate Chip |',
'| 11th | 2 or lower |',
'| 14th | 3 or lower |',
'| 17th | 4 or lower |\n\n',
].join('\n');
},
},
{
name : 'Wide Table',
icon : 'fa-list',
gen : function(){
return [
'<div class=\'wide\'>',
'##### Cookie Tastiness',
'| Tastiness | Cookie Type |',
'|:----:|:-------------|',
'| -5 | Raisin |',
'| 8th | Chocolate Chip |',
'| 11th | 2 or lower |',
'| 14th | 3 or lower |',
'| 17th | 4 or lower |',
'</div>\n\n'
].join('\n');
},
},
{
name : 'Split Table',
icon : 'fa-th-large',
gen : function(){
return [
'<div style=\'column-count:2\'>',
'| d10 | Damage Type |',
'|:---:|:------------|',
'| 1 | Acid |',
'| 2 | Cold |',
'| 3 | Fire |',
'| 4 | Force |',
'| 5 | Lightning |',
'',
'```',
'```',
'',
'| d10 | Damage Type |',
'|:---:|:------------|',
'| 6 | Necrotic |',
'| 7 | Poison |',
'| 8 | Psychic |',
'| 9 | Radiant |',
'| 10 | Thunder |',
'</div>\n\n',
].join('\n');
},
}
]
},
/**************** PRINT *************/
{
groupName : 'Print',
icon : 'fa-print',
snippets : [
{
name : 'A4 PageSize',
icon : 'fa-file-o',
gen : ['<style>',
' .phb{',
' width : 210mm;',
' height : 296.8mm;',
' }',
'</style>'
].join('\n')
},
{
name : 'Ink Friendly',
icon : 'fa-tint',
gen : ['<style>',
' .phb{ background : white;}',
' .phb img{ display : none;}',
' .phb hr+blockquote{background : white;}',
'</style>',
''
].join('\n')
},
]
},
];

View File

@@ -1,90 +1,67 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash'); const cx = require('classnames');
const cx = require('classnames');
const CreateRouter = require('pico-router').createRouter;
const CreateRouter = require('pico-router').createRouter; const BrewActions = require('homebrewery/brew.actions.js');
const AccountActions = require('homebrewery/account.actions.js');
const HomePage = require('./pages/homePage/homePage.jsx');
const EditPage = require('./pages/editPage/editPage.jsx'); const HomePage = require('./pages/homePage/homePage.jsx');
const UserPage = require('./pages/userPage/userPage.jsx'); const EditPage = require('./pages/editPage/editPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx'); const UserPage = require('./pages/userPage/userPage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx'); const SharePage = require('./pages/sharePage/sharePage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx'); const NewPage = require('./pages/newPage/newPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx'); //const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx');
let Router;
const Homebrew = createClass({ let Router;
getDefaultProps : function() { const Homebrew = React.createClass({
return { getDefaultProps: function() {
url : '', return {
welcomeText : '', url : '',
changelog : '', version : '0.0.0',
version : '0.0.0', loginPath : '',
account : null,
brew : { user : undefined,
title : '', brew : undefined,
text : '', brews : []
shareId : null, };
editId : null, },
createdAt : null, componentWillMount: function() {
updatedAt : null, BrewActions.init({
} version : this.props.version,
}; brew : this.props.brew
}, });
componentWillMount : function() { AccountActions.init({
global.account = this.props.account; user : this.props.user,
global.version = this.props.version; loginPath : this.props.loginPath
});
Router = CreateRouter({ Router = CreateRouter({
'/edit/:id' : (args)=>{ '/edit/:id' : <EditPage />,
if(!this.props.brew.editId){ '/share/:id' : <SharePage />,
return <ErrorPage errorId={args.id}/>; '/user/:username' : (args) => {
} return <UserPage
username={args.username}
return <EditPage brews={this.props.brews}
id={args.id} />
brew={this.props.brew} />; },
}, '/print/:id' : (args, query) => {
return <PrintPage brew={this.props.brew} query={query}/>;
'/share/:id' : (args)=>{ },
if(!this.props.brew.shareId){ '/print' : (args, query) => {
return <ErrorPage errorId={args.id}/>; return <PrintPage query={query}/>;
} },
'/new' : <NewPage />,
return <SharePage '/changelog' : <SharePage />,
id={args.id} '*' : <HomePage />,
brew={this.props.brew} />; });
}, },
'/user/:username' : (args)=>{ render : function(){
return <UserPage return <div className='homebrew'>
username={args.username} <Router initialUrl={this.props.url}/>
brews={this.props.brews} </div>
/>; }
}, });
'/print/:id' : (args, query)=>{
return <PrintPage brew={this.props.brew} query={query}/>; module.exports = Homebrew;
},
'/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>;
}
});
module.exports = Homebrew;

View File

@@ -1,18 +1,23 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/account.store.js');
module.exports = function(props){ const Actions = require('homebrewery/account.actions.js');
if(global.account){
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fa-user'>
{global.account.username} module.exports = function(props){
</Nav.item>; const user = Store.getUser();
} if(user && user == props.userPage){
let url = ''; return <Nav.item onClick={Actions.logout} color='yellow' icon='fa-user-times'>
if(typeof window !== 'undefined'){ logout
url = window.location.href; </Nav.item>
} }
return <Nav.item href={`http://naturalcrit.com/login?redirect=${url}`} color='teal' icon='fa-sign-in'> if(user){
login return <Nav.item href={`/user/${user}`} color='yellow' icon='fa-user'>
</Nav.item>; {user}
</Nav.item>
}
return <Nav.item onClick={Actions.login} color='teal' icon='fa-sign-in'>
login
</Nav.item>
}; };

View File

@@ -0,0 +1,9 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
module.exports = Store.createSmartComponent((props) => {
return <Nav.item className='brewTitle'>{props.title}</Nav.item>
}, (props) => {
return {title : Store.getMetaData().title};
})

View File

@@ -0,0 +1,76 @@
const flux = require('pico-flux')
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const onStoreChange = () => {
return {
status : Store.getStatus(),
errors : Store.getErrors()
}
};
const ContinousSave = React.createClass({
getDefaultProps: function() {
return {
status : 'ready',
errors : undefined
};
},
componentDidMount: function() {
flux.actionEmitter.on('dispatch', this.actionHandler);
window.onbeforeunload = ()=>{
if(this.props.status !== 'ready') return 'You have unsaved changes!';
};
},
componentWillUnmount: function() {
flux.actionEmitter.removeListener('dispatch', this.actionHandler);
window.onbeforeunload = function(){};
},
actionHandler : function(actionType){
if(actionType == 'UPDATE_BREW_TEXT' || actionType == 'UPDATE_META'){
Actions.pendingSave();
}
},
handleClick : function(){
Actions.save();
},
renderError : function(){
let errMsg = '';
try{
errMsg += this.state.errors.toString() + '\n\n';
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
}catch(e){}
return <Nav.item className='continousSave error' icon="fa-warning">
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)}>
here
</a>.
</div>
</Nav.item>
},
render : function(){
if(this.props.status == 'error') return this.renderError();
if(this.props.status == 'saving'){
return <Nav.item className='continousSave' icon="fa-spinner fa-spin">saving...</Nav.item>
}
if(this.props.status == 'pending'){
return <Nav.item className='continousSave' onClick={this.handleClick} color='blue' icon='fa-save'>Save Now</Nav.item>
}
if(this.props.status == 'ready'){
return <Nav.item className='continousSave saved'>saved.</Nav.item>
}
},
});
module.exports = Store.createSmartComponent(ContinousSave, onStoreChange);

View File

@@ -0,0 +1,76 @@
const flux = require('pico-flux')
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const onStoreChange = () => {
return {
status : Store.getStatus(),
errors : Store.getErrors()
}
};
const ContinousSave = React.createClass({
getDefaultProps: function() {
return {
status : 'ready',
errors : undefined
};
},
componentDidMount: function() {
flux.actionEmitter.on('dispatch', this.actionHandler);
window.onbeforeunload = ()=>{
if(this.props.status !== 'ready') return 'You have unsaved changes!';
};
},
componentWillUnmount: function() {
flux.actionEmitter.removeListener('dispatch', this.actionHandler);
window.onbeforeunload = function(){};
},
actionHandler : function(actionType){
if(actionType == 'UPDATE_BREW_TEXT' || actionType == 'UPDATE_META'){
Actions.pendingSave();
}
},
handleClick : function(){
Actions.save();
},
renderError : function(){
let errMsg = '';
try{
errMsg += this.state.errors.toString() + '\n\n';
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
}catch(e){}
return <Nav.item className='continousSave error' icon="fa-warning">
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)}>
here
</a>.
</div>
</Nav.item>
},
render : function(){
if(this.props.status == 'error') return this.renderError();
if(this.props.status == 'saving'){
return <Nav.item className='continousSave' icon="fa-spinner fa-spin">saving...</Nav.item>
}
if(this.props.status == 'pending'){
return <Nav.item className='continousSave' onClick={this.handleClick} color='blue' icon='fa-save'>Save Now</Nav.item>
}
if(this.props.status == 'ready'){
return <Nav.item className='continousSave saved'>saved.</Nav.item>
}
},
});
module.exports = Store.createSmartComponent(ContinousSave, onStoreChange);

View File

@@ -1,34 +1,33 @@
const React = require('react'); var React = require('react');
const createClass = require('create-react-class'); var _ = require('lodash');
const _ = require('lodash'); var cx = require('classnames');
const cx = require('classnames'); var Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const MAX_TITLE_LENGTH = 50;
const MAX_TITLE_LENGTH = 50;
var EditTitle = React.createClass({
const EditTitle = createClass({ getDefaultProps: function() {
getDefaultProps : function() { return {
return { title : '',
title : '', onChange : function(){}
onChange : function(){} };
}; },
},
handleChange : function(e){
handleChange : function(e){ if(e.target.value.length > MAX_TITLE_LENGTH) return;
if(e.target.value.length > MAX_TITLE_LENGTH) return; this.props.onChange(e.target.value);
this.props.onChange(e.target.value); },
}, render : function(){
render : function(){ return <Nav.item className='editTitle'>
return <Nav.item className='editTitle'> <input placeholder='Brew Title' type='text' value={this.props.title} onChange={this.handleChange} />
<input placeholder='Brew Title' type='text' value={this.props.title} onChange={this.handleChange} />
<div className={cx('charCount', {'max' : this.props.title.length >= MAX_TITLE_LENGTH})}>
<div className={cx('charCount', { 'max': this.props.title.length >= MAX_TITLE_LENGTH })}> {this.props.title.length}/{MAX_TITLE_LENGTH}
{this.props.title.length}/{MAX_TITLE_LENGTH} </div>
</div> </Nav.item>
</Nav.item>; },
},
});
});
module.exports = EditTitle; module.exports = EditTitle;

View File

@@ -1,9 +1,8 @@
const React = require('react'); var React = require('react');
const createClass = require('create-react-class'); var Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
module.exports = function(props){ return <Nav.item newTab={true} href='https://github.com/stolksdorf/homebrewery/issues' color='red' icon='fa-bug'>
return <Nav.item newTab={true} href='https://www.reddit.com/r/homebrewery/submit?selftext=true&title=[Issue]' color='red' icon='fa-bug'> report issue
report issue </Nav.item>
</Nav.item>;
}; };

View File

@@ -1,50 +1,22 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx'); const Store = require('homebrewery/brew.store.js');
const Navbar = createClass({ const Navbar = React.createClass({
getInitialState : function() { render : function(){
return { return <Nav.base>
//showNonChromeWarning : false, <Nav.section>
ver : '0.0.0' <Nav.logo />
}; <Nav.item href='/' className='homebrewLogo'>
}, <div>The Homebrewery</div>
</Nav.item>
componentDidMount : function() { <Nav.item>{`v${Store.getVersion()}`}</Nav.item>
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); </Nav.section>
this.setState({ {this.props.children}
//showNonChromeWarning : !isChrome, </Nav.base>
ver : window.version }
}); });
},
module.exports = Navbar;
/*
renderChromeWarning : function(){
if(!this.state.showNonChromeWarning) return;
return <Nav.item className='warning' icon='fa-exclamation-triangle'>
Optimized for Chrome
<div className='dropdown'>
If you are experiencing rendering issues, use Chrome instead
</div>
</Nav.item>
},
*/
render : function(){
return <Nav.base>
<Nav.section>
<Nav.logo />
<Nav.item href='/' className='homebrewLogo'>
<div>The Homebrewery</div>
</Nav.item>
<Nav.item>{`v${this.state.ver}`}</Nav.item>
{/*this.renderChromeWarning()*/}
</Nav.section>
{this.props.children}
</Nav.base>;
}
});
module.exports = Navbar;

View File

@@ -13,32 +13,6 @@
color : @blue; color : @blue;
} }
} }
.editTitle.navItem{
padding : 2px 12px;
input{
width : 250px;
margin : 0;
padding : 2px;
background-color : #444;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
border : 1px solid @blue;
outline : none;
}
.charCount{
display : inline-block;
vertical-align : bottom;
margin-left : 8px;
color : #666;
text-align : right;
&.max{
color : @red;
}
}
}
.brewTitle.navItem{ .brewTitle.navItem{
font-size : 12px; font-size : 12px;
font-weight : 800; font-weight : 800;
@@ -125,4 +99,34 @@
text-align : center; text-align : center;
} }
} }
.staticSave.navItem{
background-color : @orange;
&:hover{
background-color : @green;
}
}
.continousSave.navItem{
width : 105px;
text-align : center;
&.saved{
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
.errorContainer{
position : absolute;
top : 29px;
left : -20px;
z-index : 1000;
width : 120px;
padding : 8px;
background-color : #333;
a{
color : @teal;
}
}
}
}
} }

View File

@@ -0,0 +1,10 @@
module.exports = {
Account : require('./account.navitem.jsx'),
BrewTitle : require('./brewTitle.navitem.jsx'),
ContinousSave : require('./continousSave.navitem.jsx'),
Issue : require('./issue.navitem.jsx'),
Patreon : require('./patreon.navitem.jsx'),
Print : require('./print.navitem.jsx'),
Recent : require('./recent.navitem.jsx'),
StaticSave : require('./staticSave.navitem.jsx'),
};

View File

@@ -1,14 +1,13 @@
const React = require('react'); var React = require('react');
const createClass = require('create-react-class'); var Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
module.exports = function(props){ return <Nav.item
return <Nav.item className='patreon'
className='patreon' newTab={true}
newTab={true} href='https://www.patreon.com/stolksdorf'
href='https://www.patreon.com/stolksdorf' color='green'
color='green' icon='fa-heart'>
icon='fa-heart'> help out
help out </Nav.item>
</Nav.item>;
}; };

View File

@@ -1,9 +1,8 @@
const React = require('react'); var React = require('react');
const createClass = require('create-react-class'); var Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
module.exports = function(props){ return <Nav.item newTab={true} href={'/print/' + props.shareId +'?dialog=true'} color='purple' icon='fa-file-pdf-o'>
return <Nav.item newTab={true} href={`/print/${props.shareId}?dialog=true`} color='purple' icon='fa-file-pdf-o'> get PDF
get PDF </Nav.item>
</Nav.item>;
}; };

View File

@@ -1,200 +1,199 @@
const React = require('react'); var React = require('react');
const createClass = require('create-react-class'); var _ = require('lodash');
const _ = require('lodash'); var cx = require('classnames');
const cx = require('classnames'); var Moment = require('moment');
const Moment = require('moment');
var Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const VIEW_KEY = 'homebrewery-recently-viewed';
const VIEW_KEY = 'homebrewery-recently-viewed'; const EDIT_KEY = 'homebrewery-recently-edited';
const EDIT_KEY = 'homebrewery-recently-edited';
var BaseItem = React.createClass({
const BaseItem = createClass({ getDefaultProps: function() {
getDefaultProps : function() { return {
return { storageKey : '',
storageKey : '', text : '',
text : '', currentBrew:{
currentBrew : { title : '',
title : '', id : '',
id : '', url : ''
url : '' }
} };
}; },
}, getInitialState: function() {
getInitialState : function() { return {
return { showDropdown: false,
showDropdown : false, brews : []
brews : [] };
}; },
},
componentDidMount: function() {
componentDidMount : function() { var brews = JSON.parse(localStorage.getItem(this.props.storageKey) || '[]');
let brews = JSON.parse(localStorage.getItem(this.props.storageKey) || '[]');
brews = _.filter(brews, (brew)=>{
brews = _.filter(brews, (brew)=>{ return brew.id !== this.props.currentBrew.id;
return brew.id !== this.props.currentBrew.id; });
}); if(this.props.currentBrew.id){
if(this.props.currentBrew.id){ brews.unshift({
brews.unshift({ id : this.props.currentBrew.id,
id : this.props.currentBrew.id, url : this.props.currentBrew.url,
url : this.props.currentBrew.url, title : this.props.currentBrew.title,
title : this.props.currentBrew.title, ts : Date.now()
ts : Date.now() });
}); }
} brews = _.slice(brews, 0, 8);
brews = _.slice(brews, 0, 8); localStorage.setItem(this.props.storageKey, JSON.stringify(brews));
localStorage.setItem(this.props.storageKey, JSON.stringify(brews)); this.setState({
this.setState({ brews : brews
brews : brews });
}); },
},
handleDropdown : function(show){
handleDropdown : function(show){ this.setState({
this.setState({ showDropdown : show
showDropdown : show })
}); },
},
renderDropdown : function(){
renderDropdown : function(){ if(!this.state.showDropdown) return null;
if(!this.state.showDropdown) return null;
var items = _.map(this.state.brews, (brew)=>{
const items = _.map(this.state.brews, (brew)=>{ return <a href={brew.url} className='item' key={brew.id} target='_blank'>
return <a href={brew.url} className='item' key={brew.id} target='_blank'> <span className='title'>{brew.title}</span>
<span className='title'>{brew.title}</span> <span className='time'>{Moment(brew.ts).fromNow()}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</span> </a>
</a>; });
});
return <div className='dropdown'>{items}</div>
return <div className='dropdown'>{items}</div>; },
},
render : function(){
render : function(){ return <Nav.item icon='fa-clock-o' color='grey' className='recent'
return <Nav.item icon='fa-clock-o' color='grey' className='recent' onMouseEnter={this.handleDropdown.bind(null, true)}
onMouseEnter={()=>this.handleDropdown(true)} onMouseLeave={this.handleDropdown.bind(null, false)}>
onMouseLeave={()=>this.handleDropdown(false)}> {this.props.text}
{this.props.text} {this.renderDropdown()}
{this.renderDropdown()} </Nav.item>
</Nav.item>; },
},
});
});
module.exports = {
module.exports = { viewed : React.createClass({
viewed : createClass({ getDefaultProps: function() {
getDefaultProps : function() { return {
return { brew : {
brew : { title : '',
title : '', shareId : ''
shareId : '' }
} };
}; },
}, render : function(){
render : function(){ return <BaseItem text='recently viewed' storageKey={VIEW_KEY}
return <BaseItem text='recently viewed' storageKey={VIEW_KEY} currentBrew={{
currentBrew={{ id : this.props.brew.shareId,
id : this.props.brew.shareId, title : this.props.brew.title,
title : this.props.brew.title, url : `/share/${this.props.brew.shareId}`
url : `/share/${this.props.brew.shareId}` }}
}} />
/>; },
}, }),
}),
edited : React.createClass({
edited : createClass({ getDefaultProps: function() {
getDefaultProps : function() { return {
return { brew : {
brew : { title : '',
title : '', editId : ''
editId : '' }
} };
}; },
}, render : function(){
render : function(){ return <BaseItem text='recently edited' storageKey={EDIT_KEY}
return <BaseItem text='recently edited' storageKey={EDIT_KEY} currentBrew={{
currentBrew={{ id : this.props.brew.editId,
id : this.props.brew.editId, title : this.props.brew.title,
title : this.props.brew.title, url : `/edit/${this.props.brew.editId}`
url : `/edit/${this.props.brew.editId}` }}
}} />
/>; },
}, }),
}),
both : React.createClass({
both : createClass({ getDefaultProps: function() {
getDefaultProps : function() { return {
return { errorId : null
errorId : null };
}; },
},
getInitialState: function() {
getInitialState : function() { return {
return { showDropdown: false,
showDropdown : false, edit : [],
edit : [], view : []
view : [] };
}; },
},
componentDidMount: function() {
componentDidMount : function() {
var edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]'); var viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
if(this.props.errorId){
if(this.props.errorId){ edited = _.filter(edited, (edit) => {
edited = _.filter(edited, (edit)=>{ return edit.id !== this.props.errorId;
return edit.id !== this.props.errorId; });
}); viewed = _.filter(viewed, (view) => {
viewed = _.filter(viewed, (view)=>{ return view.id !== this.props.errorId;
return view.id !== this.props.errorId; });
});
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
localStorage.setItem(EDIT_KEY, JSON.stringify(edited)); localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed)); }
}
this.setState({
this.setState({ edit : edited,
edit : edited, view : viewed
view : viewed });
}); },
},
handleDropdown : function(show){
handleDropdown : function(show){ this.setState({
this.setState({ showDropdown : show
showDropdown : show })
}); },
},
renderDropdown : function(){
renderDropdown : function(){ if(!this.state.showDropdown) return null;
if(!this.state.showDropdown) return null;
var makeItems = (brews) => {
const makeItems = (brews)=>{ return _.map(brews, (brew)=>{
return _.map(brews, (brew)=>{ return <a href={brew.url} className='item' key={brew.id} target='_blank'>
return <a href={brew.url} className='item' key={brew.id} target='_blank'> <span className='title'>{brew.title}</span>
<span className='title'>{brew.title}</span> <span className='time'>{Moment(brew.ts).fromNow()}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</span> </a>
</a>; });
}); };
};
return <div className='dropdown'>
return <div className='dropdown'> <h4>edited</h4>
<h4>edited</h4> {makeItems(this.state.edit)}
{makeItems(this.state.edit)} <h4>viewed</h4>
<h4>viewed</h4> {makeItems(this.state.view)}
{makeItems(this.state.view)} </div>
</div>; },
},
render : function(){
render : function(){ return <Nav.item icon='fa-clock-o' color='grey' className='recent'
return <Nav.item icon='fa-clock-o' color='grey' className='recent' onMouseEnter={this.handleDropdown.bind(null, true)}
onMouseEnter={()=>this.handleDropdown(true)} onMouseLeave={this.handleDropdown.bind(null, false)}>
onMouseLeave={()=>this.handleDropdown(false)}> Recent brews
Recent brews {this.renderDropdown()}
{this.renderDropdown()} </Nav.item>
</Nav.item>; }
}
})
}) }
};

View File

@@ -1,52 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
//var striptags = require('striptags');
const Nav = require('naturalcrit/nav/nav.jsx');
const MAX_URL_SIZE = 2083;
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';
const RedditShare = createClass({
getDefaultProps : function() {
return {
brew : {
title : '',
sharedId : '',
text : ''
}
};
},
getText : function(){
},
handleClick : function(){
const url = [
MAIN_URL,
`title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`,
`text=${encodeURIComponent(this.props.brew.text)}`
].join('&');
window.open(url, '_blank');
},
render : function(){
return <Nav.item icon='fa-reddit-alien' color='red' onClick={this.handleClick}>
share on reddit
</Nav.item>;
},
});
module.exports = RedditShare;

View File

@@ -0,0 +1,37 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const StaticSave = React.createClass({
getDefaultProps: function() {
return {
status : 'ready'
};
},
handleClick : function(){
Actions.create();
},
render : function(){
if(this.props.status === 'saving'){
return <Nav.item icon='fa-spinner fa-spin' className='staticSave'>
save...
</Nav.item>
}
if(this.props.status === 'ready'){
return <Nav.item icon='fa-save' className='staticSave' onClick={this.handleClick}>
save
</Nav.item>
}
}
});
module.exports = Store.createSmartComponent(StaticSave, ()=>{
return {
status : Store.getStatus()
}
});

View File

@@ -1,231 +1,63 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash'); const cx = require('classnames');
const cx = require('classnames');
const request = require('superagent'); const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const Nav = require('naturalcrit/nav/nav.jsx'); const Items = require('../../navbar/navitems.js');
const Navbar = require('../../navbar/navbar.jsx');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx'); const Utils = require('homebrewery/utils.js');
const PrintLink = require('../../navbar/print.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx'); const Store = require('homebrewery/brew.store.js');
//const RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited; const Actions = require('homebrewery/brew.actions.js');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const EditPage = React.createClass({
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); componentDidMount: function(){
document.addEventListener('keydown', this.handleControlKeys);
const Markdown = require('naturalcrit/markdown.js'); },
componentWillUnmount: function() {
const SAVE_TIMEOUT = 3000; document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : Utils.controlKeys({
const EditPage = createClass({ s : Actions.save,
getDefaultProps : function() { p : Actions.print
return { }),
brew : { render : function(){
text : '', return <div className='editPage page'>
shareId : null, <SmartNav />
editId : null, <div className='content'>
createdAt : null, <BrewInterface />
updatedAt : null, </div>
</div>
title : '', }
description : '', });
tags : '',
published : false, const SmartNav = Store.createSmartComponent(React.createClass({
authors : [], getDefaultProps: function() {
systems : [] return {
} brew : {}
}; };
}, },
render : function(){
getInitialState : function() { return <Navbar>
return { <Nav.section>
brew : this.props.brew, <Items.BrewTitle />
</Nav.section>
isSaving : false, <Nav.section>
isPending : false, <Items.ContinousSave />
errors : null, <Items.Issue />
htmlErrors : Markdown.validate(this.props.brew.text), <Nav.item newTab={true} href={'/share/' + Store.getBrew().shareId} color='teal' icon='fa-share-alt'>
lastUpdated : this.props.brew.updatedAt Share
}; </Nav.item>
}, <Items.Print />
savedBrew : null, <Items.Account />
</Nav.section>
componentDidMount : function(){ </Navbar>
this.trySave(); }
window.onbeforeunload = ()=>{ }), ()=>{
if(this.state.isSaving || this.state.isPending){ return {brew : Store.getBrew()}
return 'You have unsaved changes!'; });
}
}; module.exports = EditPage;
this.setState({
htmlErrors : Markdown.validate(this.state.brew.text)
});
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount : function() {
window.onbeforeunload = function(){};
document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save();
if(e.keyCode == P_KEY) window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleMetadataChange : function(metadata){
this.setState({
brew : _.merge({}, this.state.brew, metadata),
isPending : true,
}, ()=>{
this.trySave();
});
},
handleTextChange : function(text){
//If there are errors, run the validator on everychange to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState({
brew : _.merge({}, this.state.brew, { text: text }),
isPending : true,
htmlErrors : htmlErrors
});
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;
},
trySave : function(){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){
this.debounceSave();
} else {
this.debounceSave.cancel();
}
},
save : function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
this.setState({
isSaving : true,
errors : null,
htmlErrors : Markdown.validate(this.state.brew.text)
});
request
.put(`/api/update/${this.props.brew.editId}`)
.send(this.state.brew)
.end((err, res)=>{
if(err){
this.setState({
errors : err,
});
} else {
this.savedBrew = res.body;
this.setState({
isPending : false,
isSaving : false,
lastUpdated : res.body.updatedAt
});
}
});
},
renderSaveButton : function(){
if(this.state.errors){
let errMsg = '';
try {
errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
} catch (e){}
return <Nav.item className='save error' icon='fa-warning'>
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)}`}>
here
</a>.
</div>
</Nav.item>;
}
if(this.state.isSaving){
return <Nav.item className='save' icon='fa-spinner fa-spin'>saving...</Nav.item>;
}
if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>;
}
if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item>;
}
},
renderNavbar : function(){
return <Navbar>
<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} />
<Account />
</Nav.section>
</Navbar>;
},
render : function(){
return <div className='editPage page'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
value={this.state.brew.text}
onChange={this.handleTextChange}
metadata={this.state.brew}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} />
</SplitPane>
</div>
</div>;
}
});
module.exports = EditPage;

View File

@@ -1,27 +1,4 @@
.editPage{ .editPage{
.navItem.save{
width : 105px;
text-align : center;
&.saved{
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
.errorContainer{
position : absolute;
top : 29px;
left : -20px;
z-index : 1000;
width : 120px;
padding : 8px;
background-color : #333;
a{
color : @teal;
}
}
}
}
} }

View File

@@ -1,21 +1,20 @@
const React = require('react'); var React = require('react');
const createClass = require('create-react-class'); var _ = require('lodash');
const _ = require('lodash'); var cx = require('classnames');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx'); var Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); var Navbar = require('../../navbar/navbar.jsx');
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx'); var PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx'); var IssueNavItem = require('../../navbar/issue.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx'); var RecentNavItem = require('../../navbar/recent.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const ErrorPage = createClass({ var ErrorPage = React.createClass({
getDefaultProps : function() { getDefaultProps: function() {
return { return {
ver : '0.0.0', ver : '0.0.0',
errorId : '' errorId: ''
}; };
}, },
@@ -40,7 +39,7 @@ const ErrorPage = createClass({
<div className='content'> <div className='content'>
<BrewRenderer text={this.text} /> <BrewRenderer text={this.text} />
</div> </div>
</div>; </div>
} }
}); });

View File

@@ -4,7 +4,7 @@ module.exports = function(shareId){
return function(event){ return function(event){
event = event || window.event; event = event || window.event;
if((event.ctrlKey || event.metaKey) && event.keyCode == 80){ if((event.ctrlKey || event.metaKey) && event.keyCode == 80){
const win = window.open(`/homebrew/print/${shareId}?dialog=true`, '_blank'); var win = window.open(`/homebrew/print/${shareId}?dialog=true`, '_blank');
win.focus(); win.focus();
event.preventDefault(); event.preventDefault();
} }

View File

@@ -1,95 +1,63 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash'); const cx = require('classnames');
const cx = require('classnames');
const request = require('superagent'); const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const Nav = require('naturalcrit/nav/nav.jsx'); const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx'); const RecentNavItem = require('../../navbar/recent.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx'); const AccountNavItem = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx'); const Actions = require('homebrewery/brew.actions.js');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); //
const HomePage = React.createClass({
handleSave : function(){
const HomePage = createClass({ Actions.saveNew();
getDefaultProps : function() { },
return {
welcomeText : '', renderNavbar : function(){
ver : '0.0.0' return <Navbar>
}; <Nav.section>
<PatreonNavItem />
<IssueNavItem />
}, <Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'>
getInitialState : function() { Changelog
return { </Nav.item>
text : this.props.welcomeText <RecentNavItem.both />
}; <AccountNavItem />
}, {/*}
handleSave : function(){ <Nav.item href='/new' color='green' icon='fa-external-link'>
request.post('/api') New Brew
.send({ </Nav.item>
text : this.state.text */}
}) </Nav.section>
.end((err, res)=>{ </Navbar>
if(err) return; },
const brew = res.body;
window.location = `/edit/${brew.editId}`; render : function(){
}); return <div className='homePage page'>
}, {this.renderNavbar()}
handleSplitMove : function(){ <div className='content'>
this.refs.editor.update(); <BrewInterface />
}, </div>
handleTextChange : function(text){
this.setState({ <div className={cx('floatingSaveButton', {
text : text //show : Store.getBrewText() !== this.props.welcomeText
}); })} onClick={this.handleSave}>
}, Save current <i className='fa fa-save' />
renderNavbar : function(){ </div>
return <Navbar ver={this.props.ver}>
<Nav.section> <a href='/new' className='floatingNewButton'>
<PatreonNavItem /> Create your own <i className='fa fa-magic' />
<IssueNavItem /> </a>
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'> </div>
Changelog }
</Nav.item> });
<RecentNavItem.both />
<AccountNavItem /> module.exports = HomePage;
{/*}
<Nav.item href='/new' color='green' icon='fa-external-link'>
New Brew
</Nav.item>
*/}
</Nav.section>
</Navbar>;
},
render : function(){
return <div className='homePage page'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<BrewRenderer text={this.state.text} />
</SplitPane>
</div>
<div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
Save current <i className='fa fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fa fa-magic' />
</a>
</div>;
}
});
module.exports = HomePage;

View File

@@ -1,162 +1,62 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash');
const cx = require('classnames'); const Nav = require('naturalcrit/nav/nav.jsx');
const request = require('superagent'); const Navbar = require('../../navbar/navbar.jsx');
const Items = require('../../navbar/navitems.js');
const Markdown = require('naturalcrit/markdown.js');
const Store = require('homebrewery/brew.store.js');
const Nav = require('naturalcrit/nav/nav.jsx'); const Actions = require('homebrewery/brew.actions.js');
const Navbar = require('../../navbar/navbar.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx'); const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx'); const Utils = require('homebrewery/utils.js');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const KEY = 'homebrewery-new';
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const NewPage = React.createClass({
componentDidMount: function() {
try{
const KEY = 'homebrewery-new'; const storedBrew = JSON.parse(localStorage.getItem(KEY));
if(storedBrew && storedBrew.text) Actions.setBrew(storedBrew);
const NewPage = createClass({ }catch(e){}
getInitialState : function() { Store.updateEmitter.on('change', this.saveToLocal);
return { document.addEventListener('keydown', this.handleControlKeys);
metadata : { },
title : '', componentWillUnmount: function() {
description : '', Store.updateEmitter.removeListener('change', this.saveToLocal);
tags : '', document.removeEventListener('keydown', this.handleControlKeys);
published : false, },
authors : [],
systems : [] saveToLocal : function(){
}, localStorage.setItem(KEY, JSON.stringify(Store.getBrew()));
},
text : '', handleControlKeys : Utils.controlKeys({
isSaving : false, s : Actions.saveNew,
errors : [] p : Actions.localPrint
}; }),
},
componentDidMount : function() { render : function(){
const storage = localStorage.getItem(KEY); return <div className='newPage page'>
if(storage){ <Navbar>
this.setState({ <Nav.section>
text : storage <Items.BrewTitle />
}); </Nav.section>
}
document.addEventListener('keydown', this.handleControlKeys); <Nav.section>
}, <Items.StaticSave />
componentWillUnmount : function() { <Nav.item color='purple' icon='fa-file-pdf-o' onClick={Actions.localPrint}>
document.removeEventListener('keydown', this.handleControlKeys); get PDF
}, </Nav.item>
<Items.Issue />
handleControlKeys : function(e){ <Items.Account />
if(!(e.ctrlKey || e.metaKey)) return; </Nav.section>
const S_KEY = 83; </Navbar>
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save(); <div className='content'>
if(e.keyCode == P_KEY) this.print(); <BrewInterface />
if(e.keyCode == P_KEY || e.keyCode == S_KEY){ </div>
e.stopPropagation(); </div>
e.preventDefault(); }
} });
},
module.exports = NewPage;
handleSplitMove : function(){
this.refs.editor.update();
},
handleMetadataChange : function(metadata){
this.setState({
metadata : _.merge({}, this.state.metadata, metadata)
});
},
handleTextChange : function(text){
this.setState({
text : text,
errors : Markdown.validate(text)
});
localStorage.setItem(KEY, text);
},
save : function(){
this.setState({
isSaving : true
});
request.post('/api')
.send(_.merge({}, this.state.metadata, {
text : this.state.text
}))
.end((err, res)=>{
if(err){
this.setState({
isSaving : false
});
return;
}
window.onbeforeunload = function(){};
const brew = res.body;
localStorage.removeItem(KEY);
window.location = `/edit/${brew.editId}`;
});
},
renderSaveButton : function(){
if(this.state.isSaving){
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fa-save' className='saveButton' onClick={this.save}>
save
</Nav.item>;
}
},
print : function(){
localStorage.setItem('print', this.state.text);
window.open('/print?dialog=true&local=print', '_blank');
},
renderLocalPrintButton : function(){
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}>
get PDF
</Nav.item>;
},
renderNavbar : function(){
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.metadata.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
{this.renderLocalPrintButton()}
<IssueNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
},
render : function(){
return <div className='newPage page'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
value={this.state.text}
onChange={this.handleTextChange}
metadata={this.state.metadata}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.text} errors={this.state.errors} />
</SplitPane>
</div>
</div>;
}
});
module.exports = NewPage;

View File

@@ -1,10 +1,4 @@
.newPage{ .newPage{
.saveButton{
background-color: @orange;
&:hover{
background-color: @green;
}
}
} }

View File

@@ -1,39 +1,34 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('homebrewery/markdown.js');
const PrintPage = createClass({ const PrintPage = React.createClass({
getDefaultProps : function() { getDefaultProps: function() {
return { return {
query : {}, query : {},
brew : { brew : {
text : '', text : '',
} }
}; };
}, },
getInitialState: function() {
getInitialState : function() {
return { return {
brewText : this.props.brew.text brewText: this.props.brew.text
}; };
}, },
componentDidMount: function() {
componentDidMount : function() {
if(this.props.query.local){ if(this.props.query.local){
this.setState({ brewText: localStorage.getItem(this.props.query.local) }); this.setState({ brewText : localStorage.getItem(this.props.query.local)});
} }
if(this.props.query.dialog) window.print(); if(this.props.query.dialog) window.print();
}, },
renderPages : function(){ renderPages : function(){
return _.map(this.state.brewText.split('\\page'), (page, index)=>{ return _.map(this.state.brewText.split('\\page'), (page, index) => {
return <div return <div
className='phb' className='phb'
id={`p${index + 1}`} id={`p${index + 1}`}
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }} dangerouslySetInnerHTML={{__html:Markdown.render(page)}}
key={index} />; key={index} />;
}); });
}, },
@@ -41,7 +36,7 @@ const PrintPage = createClass({
render : function(){ render : function(){
return <div> return <div>
{this.renderPages()} {this.renderPages()}
</div>; </div>
} }
}); });

View File

@@ -1,72 +1,87 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash'); const cx = require('classnames');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Nav = require('naturalcrit/nav/nav.jsx'); const Navbar = require('../../navbar/navbar.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const PrintLink = require('../../navbar/print.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx'); const ReportIssue = require('../../navbar/issue.navitem.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx'); //const RecentlyViewed = require('../../navbar/recent.navitem.jsx').viewed;
//const RecentlyViewed = require('../../navbar/recent.navitem.jsx').viewed; const Account = require('../../navbar/account.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('homebrewery/brewRenderer/brewRenderer.jsx');
const Utils = require('homebrewery/utils.js');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Actions = require('homebrewery/brew.actions.js');
const Store = require('homebrewery/brew.store.js');
const SharePage = createClass({
getDefaultProps : function() { const Headtags = require('vitreum/headtags');
return {
brew : { const SharePage = React.createClass({
title : '', getDefaultProps: function() {
text : '', return {
shareId : null, brew : {
createdAt : null, title : '',
updatedAt : null, text : '',
views : 0 shareId : null,
} createdAt : null,
}; updatedAt : null,
}, views : 0
}
componentDidMount : function() { };
document.addEventListener('keydown', this.handleControlKeys); },
},
componentWillUnmount : function() { componentDidMount: function() {
document.removeEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
handleControlKeys : function(e){ componentWillUnmount: function() {
if(!(e.ctrlKey || e.metaKey)) return; document.removeEventListener('keydown', this.handleControlKeys);
const P_KEY = 80; },
if(e.keyCode == P_KEY){ handleControlKeys : Utils.controlKeys({
window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus(); p : Actions.print
e.stopPropagation(); }),
e.preventDefault();
} renderMetatags : function(brew){
}, let metatags = [
<Headtags.meta key='site_name' property='og:site_name' content='Homebrewery'/>,
render : function(){ <Headtags.meta key='type' property='og:type' content='article' />
return <div className='sharePage page'> ];
<Navbar> if(brew.title){
<Nav.section> metatags.push(<Headtags.meta key='title' property='og:title' content={brew.title} />);
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item> }
</Nav.section> if(brew.description){
metatags.push(<Headtags.meta key='description' name='description' content={brew.description} />);
<Nav.section> }
<ReportIssue /> if(brew.thumbnail){
{/*<RecentlyViewed brew={this.props.brew} />*/} metatags.push(<Headtags.meta key='image' property='og:image' content={brew.thumbnail} />);
<PrintLink shareId={this.props.brew.shareId} /> }
<Nav.item href={`/source/${this.props.brew.shareId}`} color='teal' icon='fa-code'> return metatags;
source },
</Nav.item>
<Account /> render : function(){
</Nav.section> const brew = Store.getBrew();
</Navbar> return <div className='sharePage page'>
{this.renderMetatags(brew)}
<div className='content'>
<BrewRenderer text={this.props.brew.text} /> <Navbar>
</div> <Nav.section>
</div>; <Nav.item className='brewTitle'>{brew.title}</Nav.item>
} </Nav.section>
});
<Nav.section>
module.exports = SharePage; <ReportIssue />
<PrintLink shareId={brew.shareId} />
<Nav.item href={'/source/' + brew.shareId} color='teal' icon='fa-code'>
source
</Nav.item>
<Account />
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer brewText={brew.text} />
</div>
</div>
}
});
module.exports = SharePage;

View File

@@ -1,15 +1,13 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const moment = require('moment'); const moment = require('moment');
const request = require('superagent');
const BrewItem = createClass({ const BrewItem = React.createClass({
getDefaultProps : function() { getDefaultProps: function() {
return { return {
brew : { brew : {
title : '', title : '',
description : '', description : '',
authors : [] authors : []
@@ -17,30 +15,12 @@ const BrewItem = createClass({
}; };
}, },
deleteBrew : function(){ renderEditLink: 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;
request.get(`/api/remove/${this.props.brew.editId}`)
.send()
.end(function(err, res){
location.reload();
});
},
renderDeleteBrewLink : function(){
if(!this.props.brew.editId) return;
return <a onClick={this.deleteBrew}>
<i className='fa fa-trash' />
</a>;
},
renderEditLink : function(){
if(!this.props.brew.editId) return; 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'>
<i className='fa fa-pencil' /> <i className='fa fa-pencil' />
</a>; </a>
}, },
render : function(){ render : function(){
@@ -67,9 +47,8 @@ const BrewItem = createClass({
<i className='fa fa-share-alt' /> <i className='fa fa-share-alt' />
</a> </a>
{this.renderEditLink()} {this.renderEditLink()}
{this.renderDeleteBrewLink()}
</div> </div>
</div>; </div>
} }
}); });

View File

@@ -1,5 +1,4 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -10,19 +9,19 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx'); const Account = require('../../navbar/account.navitem.jsx');
const BrewItem = require('./brewItem/brewItem.jsx'); const BrewItem = require('./brewItem/brewItem.jsx');
// const brew = { const brew = {
// title : 'SUPER Long title woah now', title : 'SUPER Long title woah now',
// authors : [] authors : []
// }; }
//const BREWS = _.times(25, ()=>{ return brew;}); const BREWS = _.times(25, ()=>{ return brew});
const UserPage = createClass({ const UserPage = React.createClass({
getDefaultProps : function() { getDefaultProps: function() {
return { return {
username : '', username : '',
brews : [] brews : []
}; };
}, },
@@ -31,14 +30,14 @@ const UserPage = createClass({
const sortedBrews = _.sortBy(brews, (brew)=>{ return brew.title; }); const sortedBrews = _.sortBy(brews, (brew)=>{ return brew.title; });
return _.map(sortedBrews, (brew, idx)=>{ return _.map(sortedBrews, (brew, idx) => {
return <BrewItem brew={brew} key={idx}/>; return <BrewItem brew={brew} key={idx}/>
}); });
}, },
getSortedBrews : function(){ getSortedBrews : function(){
return _.groupBy(this.props.brews, (brew)=>{ return _.groupBy(this.props.brews, (brew)=>{
return (brew.published ? 'published' : 'private'); return (brew.published ? 'published' : 'private')
}); });
}, },
@@ -53,12 +52,13 @@ const UserPage = createClass({
render : function(){ render : function(){
const brews = this.getSortedBrews(); const brews = this.getSortedBrews();
console.log('user brews', brews);
return <div className='userPage page'> return <div className='userPage page'>
<Navbar> <Navbar>
<Nav.section> <Nav.section>
<RecentNavItem.both /> <RecentNavItem.both />
<Account /> <Account userPage={this.props.username} />
</Nav.section> </Nav.section>
</Navbar> </Navbar>
@@ -69,7 +69,7 @@ const UserPage = createClass({
{this.renderPrivateBrews(brews.private)} {this.renderPrivateBrews(brews.private)}
</div> </div>
</div> </div>
</div>; </div>
} }
}); });

View File

@@ -384,6 +384,19 @@ body {
} }
} }
//***************************** //*****************************
// * PRINT
// *****************************/
.phb.print{
blockquote{
box-shadow : none;
}
}
@media print {
.phb .descriptive, .phb blockquote{
box-shadow : none;
}
}
//*****************************
// * WIDE // * WIDE
// *****************************/ // *****************************/
.phb .wide{ .phb .wide{
@@ -402,7 +415,7 @@ body {
border : initial; border : initial;
border-style : solid; border-style : solid;
border-image-outset : 25px 17px; border-image-outset : 25px 17px;
border-image-repeat : stretch; border-image-repeat : round;
border-image-slice : 150 200 150 200; border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage; border-image-source : @frameBorderImage;
border-image-width : 47px; border-image-width : 47px;
@@ -410,9 +423,9 @@ body {
margin-bottom : 10px; margin-bottom : 10px;
} }
} }
//************************************ //*****************************
// * DESCRIPTIVE TEXT BOX // * CLASS TABLE
// ************************************/ // *****************************/
.phb .descriptive{ .phb .descriptive{
display : block-inline; display : block-inline;
margin-bottom : 1em; margin-bottom : 1em;
@@ -420,7 +433,7 @@ body {
font-family : ScalySans; font-family : ScalySans;
border-style : solid; border-style : solid;
border-width : 7px; border-width : 7px;
border-image : @descriptiveBoxImage 12 stretch; border-image : @descriptiveBoxImage 12 round;
border-image-outset : 4px; border-image-outset : 4px;
box-shadow : 0px 0px 6px #faf7ea; box-shadow : 0px 0px 6px #faf7ea;
p{ p{
@@ -464,4 +477,4 @@ body {
&>ul>li{ &>ul>li{
margin-bottom : 10px; margin-bottom : 10px;
} }
} }

View File

@@ -1,21 +1,21 @@
module.exports = function(vitreum){ module.exports = function(vitreum){
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" /> <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="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
<title>The Homebrewery - NaturalCrit</title> <title>The Homebrewery - NaturalCrit</title>
${vitreum.head} ${vitreum.head}
</head> </head>
<body> <body>
<main id="reactRoot">${vitreum.body}</main> <main id="reactRoot">${vitreum.body}</main>
</body> </body>
${vitreum.js} ${vitreum.js}
</html> </html>
`; `;
}; }

View File

@@ -1,5 +1,10 @@
{ {
"host" : "homebrewery.local.naturalcrit.com:8000", "log_level" : "info",
"naturalcrit_url" : "local.naturalcrit.com:8010", "login_path" : "/dev/login",
"secret" : "secret" "jwt_secret" : "secretsecret",
"admin" : {
"user" : "admin",
"pass" : "password",
"key" : "adminadminadmin"
}
} }

4
config/production.json Normal file
View File

@@ -0,0 +1,4 @@
{
"login_path" : "http://naturalcrit.com/login",
"log_level" : "warn"
}

4
config/staging.json Normal file
View File

@@ -0,0 +1,4 @@
{
"login_path" : "http://staging.naturalcrit.com/login",
"log_level" : "trace"
}

View File

@@ -1,45 +0,0 @@
# Contributing to Homebrewery
## How can I contribute?
### Improve documentation
As a user of Homebrewery you're the perfect candidate to help us improve our documentation. Typo corrections, error fixes, better explanations, more examples, etc. Open issues for things that could be improved. Anything. Even improvements to this document.
### Improve issues
Some issues are created with missing information, not reproducible, or plain invalid. Help make them easier to resolve. Handling issues takes a lot of time that we could rather spend on fixing bugs and adding features.
### Write code
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.
If you're updating dependencies, please make sure you use npm@5.6.0 and commit the updated `package-lock.json` file.
You can also refer to the [Development Roadmap on Trello](https://trello.com/b/q6kE29F8/development-roadmap)
## Submitting an issue
- The issue tracker is for issues. Use the [subreddit](https://www.reddit.com/r/homebrewery/) for support.
- Search the issue tracker before opening an issue.
- Use a clear and descriptive title.
- Include as much information as possible: Steps to reproduce the issue, error message, browser type and version, etc.
## Submitting a pull request
- Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work.
- For ambitious tasks, you should try to get your work in front of the community for feedback as soon as possible. Open a pull request as soon as you have done the minimum needed to demonstrate your idea. At this early stage, don't worry about making things perfect, or 100% complete. Add a [WIP] prefix to the title, and describe what you still need to do. This lets reviewers know not to nit-pick small details or point out improvements you already know you need to make.
- New features should be accompanied with tests and documentation if applicable.
- Lint and test before submitting the pull request by running `$ npm run verify`.
- If your code is not passing Linting checks due to a non-fixable warning, and you feel it's valid (eg. we lint on a file being too long, but sometimes a file just _has_ to be long), add `/* eslint-disable [rule-name] */` to the top of the file. Be sure to justfiy your lint override in your PR description.
- Use a clear and descriptive title for the pull request and commits.
- You might be asked to do changes to your pull request. There's never a need to open another pull request. [Just update the existing one.](https://github.com/RichardLitt/knowledge/blob/master/github/amending-a-commit-guide.md)

8158
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,68 +1,53 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "2.8.0", "version": "3.0.0",
"repository": {
"type": "git",
"url": "git://github.com/stolksdorf/homebrewery.git"
},
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"quick": "node scripts/quick.js", "quick": "node scripts/quick.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"lint": "eslint --fix **/*.{js,jsx}",
"lint:dry": "eslint **/*.{js,jsx}",
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
"verify": "npm run lint && npm test",
"test": "pico-check",
"test:dev": "pico-check -v -w",
"phb": "node scripts/phb.js", "phb": "node scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build", "populate": "node scripts/populate.js",
"prod": "set NODE_ENV=production&& npm run build",
"postinstall": "npm run build", "postinstall": "npm run build",
"start": "node server.js" "start": "node server.js",
"test": "mocha test",
"test:dev": "nodemon -x mocha test || exit 0",
"test:markdown": "nodemon -x mocha test/markdown.test.js || exit 0"
}, },
"author": "stolksdorf", "author": "stolksdorf",
"license": "MIT", "license": "MIT",
"eslintIgnore": [
"build/*"
],
"pico-check": {
"require": "./tests/test.init.js"
},
"babel": {
"presets": [
"env",
"react"
]
},
"dependencies": { "dependencies": {
"babel-preset-env": "^1.1.8",
"babel-preset-react": "^6.24.1",
"basic-auth": "^1.0.3", "basic-auth": "^1.0.3",
"body-parser": "^1.14.2", "body-parser": "^1.14.2",
"classnames": "^2.2.0", "classnames": "^2.2.0",
"codemirror": "^5.22.0", "codemirror": "^5.22.0",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"create-react-class": "^15.6.3", "egads": "^1.0.1",
"express": "^4.13.3", "express": "^4.13.3",
"jwt-simple": "^0.5.1", "jwt-simple": "^0.5.1",
"lodash": "^4.11.2", "lodash": "^4.17.3",
"loglevel": "^1.4.1",
"marked": "^0.3.5", "marked": "^0.3.5",
"moment": "^2.11.0", "moment": "^2.11.0",
"mongoose": "^4.3.3", "mongoose": "^4.3.3",
"nconf": "^0.8.4", "nconf": "^0.8.4",
"pico-flux": "^1.1.0", "pico-flux": "^2.1.2",
"pico-router": "^1.1.0", "pico-router": "^1.1.0",
"react": "^16.3.1", "react": "^15.4.1",
"react-dom": "^16.3.1", "react-dom": "^15.4.1",
"shortid": "^2.2.4", "shortid": "^2.2.4",
"striptags": "^2.1.1", "striptags": "^2.1.1",
"superagent": "^3.8.2", "superagent": "^1.6.1",
"vitreum": "^4.10.1" "vitreum": "^4.0.12"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^4.19.1", "app-module-path": "^2.1.0",
"eslint-plugin-react": "^7.7.0", "chai": "^3.5.0",
"pico-check": "^1.0.3" "chai-as-promised": "^6.0.0",
"chai-subset": "^1.4.0",
"mocha": "^3.2.0",
"supertest": "^2.0.1",
"supertest-as-promised": "^4.0.2"
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -2,19 +2,19 @@ const label = 'build';
console.time(label); console.time(label);
const clean = require('vitreum/steps/clean.js'); const clean = require('vitreum/steps/clean.js');
const jsx = require('vitreum/steps/jsx.js'); const jsx = require('vitreum/steps/jsx.js').partial;
const lib = require('vitreum/steps/libs.js'); const lib = require('vitreum/steps/libs.js').partial;
const less = require('vitreum/steps/less.js'); const less = require('vitreum/steps/less.js').partial;
const asset = require('vitreum/steps/assets.js'); const asset = require('vitreum/steps/assets.js').partial;
const Proj = require('./project.json'); const Proj = require('./project.json');
clean() clean()
.then(lib(Proj.libs)) .then(lib(Proj.libs))
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] })) .then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, ['./shared']))
.then((deps)=>less('homebrew', { shared: ['./shared'] }, deps)) .then(less('homebrew', ['./shared']))
.then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] })) .then(jsx('admin', './client/admin/admin.jsx', Proj.libs, ['./shared']))
.then((deps)=>less('admin', { shared: ['./shared'] }, deps)) .then(less('admin', ['./shared']))
.then(()=>asset(Proj.assets, ['./shared', './client'])) .then(asset(Proj.assets, ['./shared', './client']))
.then(console.timeEnd.bind(console, label)) .then(console.timeEnd.bind(console, label))
.catch(console.error); .catch(console.error);

View File

@@ -1,22 +1,21 @@
const label = 'dev'; const label = 'dev';
console.time(label); console.time(label);
const jsx = require('vitreum/steps/jsx.watch.js'); const jsx = require('vitreum/steps/jsx.watch.js').partial;
const less = require('vitreum/steps/less.watch.js'); const less = require('vitreum/steps/less.watch.js').partial;
const assets = require('vitreum/steps/assets.watch.js'); const assets = require('vitreum/steps/assets.watch.js').partial;
const server = require('vitreum/steps/server.watch.js'); const server = require('vitreum/steps/server.watch.js').partial;
const livereload = require('vitreum/steps/livereload.js'); const livereload = require('vitreum/steps/livereload.js').partial;
const Proj = require('./project.json'); const Proj = require('./project.json');
Promise.resolve() Promise.resolve()
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] })) .then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared'))
.then((deps)=>less('homebrew', { shared: ['./shared'] }, deps)) .then(less('homebrew', './shared'))
.then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] })) .then(jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared'))
.then((deps)=>less('admin', { shared: ['./shared'] }, deps)) .then(less('admin', './shared'))
.then(assets(Proj.assets, ['./shared', './client']))
.then(()=>assets(Proj.assets, ['./shared', './client'])) .then(livereload())
.then(()=>livereload()) .then(server('./server.js', ['server']))
.then(()=>server('./server.js', ['server']))
.then(console.timeEnd.bind(console, label)) .then(console.timeEnd.bind(console, label))
.catch(console.error); .catch(console.error)

View File

@@ -1,10 +1,10 @@
const less = require('less'); const less = require('less');
const fs = require('fs'); const fs = require('fs');
less.render(fs.readFileSync('./client/homebrew/phbStyle/phb.style.less', 'utf8'), { compress: true }) less.render(fs.readFileSync('./client/homebrew/phbStyle/phb.style.less', 'utf8'), {compress : true})
.then((output)=>{ .then((output) => {
fs.writeFileSync('./phb.standalone.css', output.css); fs.writeFileSync('./phb.standalone.css', output.css);
console.log('phb.standalone.css created!'); console.log('phb.standalone.css created!');
}, (err)=>{ }, (err) => {
console.error(err); console.error(err);
}); });

22
scripts/populate.js Normal file
View File

@@ -0,0 +1,22 @@
//Populates the DB with a bunch of brews for UI testing
const _ = require('lodash');
const DB = require('../server/db.js');
const BrewData = require('../server/brew.data.js');
const BrewGen = require('../test/brew.gen.js');
return Promise.resolve()
.then(DB.connect)
.then(BrewData.removeAll)
.then(() => {
console.log('Adding random brews...');
return BrewGen.populateDB(BrewGen.random(50));
})
.then(() => {
console.log('Adding specific brews...');
return BrewGen.populateDB(BrewGen.static());
})
.then(() => {
return DB.close();
})
.catch(console.error);

View File

@@ -5,7 +5,6 @@
"libs" : [ "libs" : [
"react", "react",
"react-dom", "react-dom",
"create-react-class",
"lodash", "lodash",
"classnames", "classnames",
"codemirror", "codemirror",

175
server.js
View File

@@ -1,140 +1,35 @@
const _ = require('lodash'); const config = require('nconf')
const jwt = require('jwt-simple'); .argv()
const express = require('express'); .env({ lowerCase: true })
const app = express(); .file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });
app.use(express.static(`${__dirname}/build`));'';
app.use(require('body-parser').json({ limit: '25mb' })); const log = require('loglevel');
app.use(require('cookie-parser')()); log.setLevel(config.get('log_level'));
const config = require('nconf') //DB
.argv() require('./server/db.js').connect();
.env({ lowerCase: true })
.file('environment', { file: `config/${process.env.NODE_ENV}.json` }) //Server
.file('defaults', { file: 'config/default.json' }); const app = require('./server/app.js');
//DB /*
require('mongoose') app.use((req, res, next) => {
.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/naturalcrit') log.debug('---------------------------');
.connection.on('error', ()=>{ log.debug(req.method, req.path);
console.log('Error : Could not connect to a Mongo Database.');
console.log(' If you are running locally, make sure mongodb.exe is running.'); if (req.params) {
}); log.debug('req params', req.params);
}
if (req.query) {
//Account MIddleware log.debug('req query', req.query);
app.use((req, res, next)=>{ }
if(req.cookies && req.cookies.nc_session){
try { next();
req.account = jwt.decode(req.cookies.nc_session, config.get('secret')); });
} catch (e){} */
}
return next(); const PORT = process.env.PORT || 8000;
}); const httpServer = app.listen(PORT, () => {
log.info(`server on port:${PORT}`);
});
app.use(require('./server/homebrew.api.js'));
app.use(require('./server/admin.api.js'));
const HomebrewModel = require('./server/homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
//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('<', '&lt;').replaceAll('>', '&gt;');
return res.send(`<code><pre>${text}</pre></code>`);
})
.catch((err)=>{
console.log(err);
return res.status(404).send('Could not find Homebrew with that id');
});
});
app.get('/user/:username', (req, res, next)=>{
const fullAccess = req.account && (req.account.username == req.params.username);
HomebrewModel.getByUser(req.params.username, fullAccess)
.then((brews)=>{
req.brews = brews;
return next();
})
.catch((err)=>{
console.log(err);
});
});
app.get('/edit/:id', (req, res, next)=>{
HomebrewModel.get({ editId: req.params.id })
.then((brew)=>{
req.brew = brew.sanatize();
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
});
//Share Page
app.get('/share/:id', (req, res, next)=>{
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
return brew.increaseView();
})
.then((brew)=>{
req.brew = brew.sanatize(true);
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
});
//Print Page
app.get('/print/:id', (req, res, next)=>{
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
req.brew = brew.sanatize(true);
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
});
//Render Page
const render = require('vitreum/steps/render');
const templateFn = require('./client/template.js');
app.use((req, res)=>{
render('homebrew', templateFn, {
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);
})
.catch((err)=>{
console.log(err);
return res.sendStatus(500);
});
});
const PORT = process.env.PORT || 8000;
app.listen(PORT);
console.log(`server on port:${PORT}`);

View File

@@ -1,85 +0,0 @@
const _ = require('lodash');
const auth = require('basic-auth');
const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
const mw = {
adminOnly : (req, res, next)=>{
if(req.query && req.query.admin_key == process.env.ADMIN_KEY) 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
});
});
}
});
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' } },
] }).exec((err, brew)=>{
return res.json(brew);
});
});
//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);
})
.catch((err)=>{
console.log(err);
return res.sendStatus(500);
});
});
module.exports = router;

60
server/admin.routes.js Normal file
View File

@@ -0,0 +1,60 @@
const _ = require('lodash');
const router = require('express').Router();
const vitreumRender = require('vitreum/steps/render');
const templateFn = require('../client/template.js');
const config = require('nconf');
const Moment = require('moment');
const mw = require('./middleware.js');
const BrewData = require('./brew.data.js');
const getInvalidBrewQuery = ()=>{
return BrewData.model.find({
'$where' : "this.text.length < 140",
createdAt: {
$lt: Moment().subtract(3, 'days').toDate()
}
}).select({ text : false });
}
router.get('/admin', mw.adminLogin, (req, res, next) => {
return vitreumRender('admin', templateFn, {
url : req.originalUrl,
admin_key : config.get('admin:key')
})
.then((page) => {
return res.send(page)
})
.catch(next);
});
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
router.get('/admin/invalid', mw.adminOnly, (req, res, next)=>{
getInvalidBrewQuery().exec()
.then((brews) => {
return res.json(brews);
})
.catch(next);
});
router.delete('/admin/invalid', mw.adminOnly, (req, res, next)=>{
getInvalidBrewQuery().remove()
.then(()=>{
return res.status(200).send();
})
.catch(next);
});
router.get('/admin/lookup/:search', mw.adminOnly, (req, res, next) => {
BrewData.get({ $or:[
//Searches for partial matches on either edit or share
{editId : { "$regex": req.params.search, "$options": "i" }},
{shareId : { "$regex": req.params.search, "$options": "i" }},
]})
.then((brews) => {
return res.json(brews);
})
.catch(next);
});
module.exports = router;

28
server/app.js Normal file
View File

@@ -0,0 +1,28 @@
const config = require('nconf');
const express = require("express");
const app = express();
app.use(express.static(__dirname + '/../build'));
app.use(require('body-parser').json({limit: '25mb'}));
app.use(require('cookie-parser')());
//Middleware
const mw = require('./middleware.js');
app.use(mw.account);
app.use(mw.admin);
//Routes
app.use(require('./brew.api.js'));
app.use(require('./interface.routes.js'));
app.use(require('./admin.routes.js'));
if(config.get('NODE_ENV') !== 'staging' && config.get('NODE_ENV') !== 'production'){
app.use(require('./dev.routes.js'));
}
//Error Handler
app.use(require('./error.js').expressHandler);
module.exports = app;

68
server/brew.api.js Normal file
View File

@@ -0,0 +1,68 @@
const _ = require('lodash');
const router = require('express').Router();
const BrewData = require('./brew.data.js');
const mw = require('./middleware.js');
//Search
router.get('/api/brew', (req, res, next) => {
const opts = _.pick(req.query, ['limit', 'sort', 'page']);
BrewData.termSearch(req.query.terms, opts, req.admin)
.then((result) => {
return res.status(200).json(result);
})
.catch(next);
});
//User
router.get('/api/user/:username', (req, res, next) => {
const fullAccess = req.admin ||
!!(req.account && req.params.username == req.account.username);
BrewData.userSearch(req.params.username, fullAccess)
.then((result) => {
return res.status(200).json(result);
})
.catch(next);
});
//Get
router.get('/api/brew/:shareId', mw.viewBrew, (req, res, next) => {
return res.json(req.brew.toJSON());
});
//Create
router.post('/api/brew', (req, res, next)=>{
const brew = req.body;
if(req.account) brew.authors = [req.account.username];
BrewData.create(brew)
.then((brew) => {
return res.json(brew.toJSON());
})
.catch(next)
});
//Update
router.put('/api/brew/:editId', mw.loadBrew, (req, res, next)=>{
const brew = req.body || {};
if(req.account){
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
}
BrewData.update(req.params.editId, brew)
.then((brew) => {
return res.json(brew.toJSON());
})
.catch(next);
});
//Delete
router.delete('/api/brew/:editId', mw.loadBrew, (req, res, next) => {
BrewData.remove(req.params.editId)
.then(()=>{
return res.sendStatus(200);
})
.catch(next);
});
module.exports = router;

101
server/brew.data.js Normal file
View File

@@ -0,0 +1,101 @@
const _ = require('lodash');
const shortid = require('shortid');
const mongoose = require('./db.js').instance;
const Error = require('./error.js');
const utils = require('./utils.js');
const BrewSchema = mongoose.Schema({
shareId : {type : String, default: shortid.generate, index: { unique: true }},
editId : {type : String, default: shortid.generate, index: { unique: true }},
text : {type : String, default : ""},
title : {type : String, default : ""},
description : {type : String, default : ""},
tags : {type : String, default : ""},
thumbnail : {type : String, default : ""},
systems : [String],
authors : [String],
published : {type : Boolean, default : false},
createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now},
lastViewed : { type: Date, default: Date.now},
views : {type:Number, default:0},
version : {type: Number, default:1}
}, {
versionKey: false,
toJSON : {
transform: (doc, ret, options) => {
delete ret._id;
return ret;
}
}
});
//Index these fields for fast text searching
BrewSchema.index({ title: "text", description: "text" });
BrewSchema.methods.increaseView = function(){
this.views = this.views + 1;
return this.save();
};
const Brew = mongoose.model('Brew', BrewSchema);
const BrewData = {
schema : BrewSchema,
model : Brew,
get : (query) => {
return Brew.findOne(query).exec()
.then((brew) => {
if(!brew) throw Error.noBrew();
return brew;
});
},
create : (brew) => {
delete brew.shareId;
delete brew.editId;
if(!brew.title) brew.title = utils.getGoodBrewTitle(brew.text);
return (new Brew(brew)).save();
},
update : (editId, newBrew) => {
return BrewData.get({ editId })
.then((brew) => {
delete newBrew.shareId;
delete newBrew.editId;
brew = _.merge(brew, newBrew, { updatedAt : Date.now() });
brew.markModified('authors');
brew.markModified('systems');
return brew.save();
});
},
remove : (editId) => {
return Brew.find({ editId }).remove().exec();
},
removeAll : ()=>{
return Brew.find({}).remove().exec();
},
//////// Special
getByShare : (shareId) => {
return BrewData.get({ shareId : shareId})
.then((brew) => {
brew.increaseView();
const brewJSON = brew.toJSON();
delete brewJSON.editId;
return brewJSON;
});
},
getByEdit : (editId) => {
return BrewData.get({ editId });
},
};
const BrewSearch = require('./brew.search.js')(Brew);
module.exports = _.merge(BrewData, BrewSearch);

66
server/brew.search.js Normal file
View File

@@ -0,0 +1,66 @@
const _ = require('lodash');
module.exports = (Brew) => {
const cmds = {
termSearch : (terms='', opts, fullAccess) => {
let query = {};
if(terms){
query = {$text: {
//Wrap terms in quotes to perform an AND operation
$search: _.map(terms.split(' '), (term)=>{
return `\"${term}\"`;
}).join(' '),
$caseSensitive : false
}};
}
return cmds.search(query, opts, fullAccess);
},
userSearch : (username, fullAccess) => {
const query = {
authors : username
};
return cmds.search(query, {}, fullAccess);
},
search : (queryObj={}, options={}, fullAccess = true) => {
const opts = _.merge({
limit : 25,
page : 0,
sort : {}
}, options);
opts.limit = _.toNumber(opts.limit);
opts.page = _.toNumber(opts.page);
let filter = {
text : 0
};
if(!fullAccess){
filter.editId = 0;
queryObj.published = true;
}
const searchQuery = Brew
.find(queryObj)
.sort(opts.sort)
.select(filter)
.limit(opts.limit)
.skip(opts.page * opts.limit)
.lean()
.exec();
const countQuery = Brew.count(queryObj).exec();
return Promise.all([searchQuery, countQuery])
.then((result) => {
return {
brews : result[0],
total : result[1]
}
});
}
};
return cmds;
};

36
server/db.js Normal file
View File

@@ -0,0 +1,36 @@
const log = require('loglevel');
const mongoose = require('mongoose');
mongoose.Promise = Promise;
const dbPath = process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/homebrewery';
module.exports = {
connect : ()=>{
return new Promise((resolve, reject)=>{
if(mongoose.connection.readyState == 1){
log.warn('DB already connected');
return resolve();
}
mongoose.connect(dbPath,
(err) => {
if(err){
log.error('Error : Could not connect to a Mongo Database.');
log.error(' If you are running locally, make sure mongodb.exe is running.');
return reject(err);
}
log.info('DB connected.');
return resolve();
}
);
});
},
close : ()=>{
return new Promise((resolve, reject) => {
mongoose.connection.close(()=>{
log.info('DB connection closed.');
return resolve();
});
});
},
instance : mongoose
}

29
server/dev.routes.js Normal file
View File

@@ -0,0 +1,29 @@
const router = require('express').Router();
const jwt = require('jwt-simple');
const auth = require('basic-auth');
const config = require('nconf');
if(process.env.NODE_ENV == 'production') throw 'Loading dev routes in prod. ABORT ABORT';
router.get('/dev/login', (req, res, next) => {
const user = req.query.user;
if(!user){
return res.send(`
<html>
<body>dev login</body>
<script>
var user = prompt('enter username');
if(user) window.location = '/dev/login?user=' + encodeURIComponent(user);
</script></html>
`);
}
res.cookie('nc_session', jwt.encode({username : req.query.user}, config.get('jwt_secret')));
return res.redirect('/');
});
module.exports = router;

25
server/error.js Normal file
View File

@@ -0,0 +1,25 @@
const Error = require('egads').extend('Server Error', 500, 'Generic Server Error');
Error.noBrew = Error.extend('Can not find a brew with that id', 404, 'No Brew Found');
Error.noAuth = Error.extend('You can not access this route', 401, 'Unauthorized');
Error.noAdmin = Error.extend('You need admin credentials to access this route', 401, 'Unauthorized');
Error.expressHandler = (err, req, res, next) => {
if(err instanceof Error){
return res.status(err.status).send({
type : err.name,
message : err.message
});
}
//If server error, print the whole stack for debugging
return res.status(500).send({
message : err.message,
stack : err.stack
});
};
module.exports = Error;

View File

@@ -1,130 +1,146 @@
const _ = require('lodash'); const _ = require('lodash');
const HomebrewModel = require('./homebrew.model.js').model; const Moment = require('moment');
const router = require('express').Router(); const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
// const getTopBrews = (cb)=>{
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
// cb(brews);
// }); //TODO: Possiblity remove
// }; let homebrewTotal = 0;
const refreshCount = ()=>{
const getGoodBrewTitle = (text)=>{ HomebrewModel.count({}, (err, total)=>{
const titlePos = text.indexOf('# '); homebrewTotal = total;
if(titlePos !== -1){ });
const ending = text.indexOf('\n', titlePos); };
return text.substring(titlePos + 2, ending); refreshCount();
} else {
return _.find(text.split('\n'), (line)=>{
return line;
}); const getTopBrews = (cb)=>{
} HomebrewModel.find().sort({views: -1}).limit(5).exec(function(err, brews) {
}; cb(brews);
});
}
router.post('/api', (req, res)=>{ const getGoodBrewTitle = (text) => {
const titlePos = text.indexOf('# ');
let authors = []; if(titlePos !== -1){
if(req.account) authors = [req.account.username]; const ending = text.indexOf('\n', titlePos);
return text.substring(titlePos + 2, ending);
const newHomebrew = new HomebrewModel(_.merge({}, }else{
req.body, return _.find(text.split('\n'), (line)=>{
{ authors: authors } return line;
)); });
if(!newHomebrew.title){ }
newHomebrew.title = getGoodBrewTitle(newHomebrew.text); };
}
newHomebrew.save((err, obj)=>{
if(err){
console.error(err, err.toString(), err.stack);
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
} router.post('/api', (req, res)=>{
return res.json(obj);
}); let authors = [];
}); if(req.account) authors = [req.account.username];
router.put('/api/update/:id', (req, res)=>{ const newHomebrew = new HomebrewModel(_.merge({},
HomebrewModel.get({ editId: req.params.id }) req.body,
.then((brew)=>{ {authors : authors}
brew = _.merge(brew, req.body); ));
brew.updatedAt = new Date(); if(!newHomebrew.title){
if(req.account) brew.authors = _.uniq(_.concat(brew.authors, req.account.username)); newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
}
brew.markModified('authors'); newHomebrew.save((err, obj)=>{
brew.markModified('systems'); if(err){
console.error(err, err.toString(), err.stack);
brew.save((err, obj)=>{ return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
if(err) throw err; }
return res.status(200).send(obj); return res.json(obj);
}); })
}) });
.catch((err)=>{
console.log(err); router.put('/api/update/:id', (req, res)=>{
return res.status(500).send('Error while saving'); HomebrewModel.get({editId : req.params.id})
}); .then((brew)=>{
}); brew = _.merge(brew, req.body);
brew.updatedAt = new Date();
router.get('/api/remove/:id', (req, res)=>{ if(req.account) brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{
if(!objs.length || err) return res.status(404).send('Can not find homebrew with that id'); brew.markModified('authors');
const resEntry = objs[0]; brew.markModified('systems');
resEntry.remove((err)=>{
if(err) return res.status(500).send('Error while removing'); brew.save((err, obj)=>{
return res.status(200).send(); if(err) throw err;
}); return res.status(200).send(obj);
}); })
}); })
.catch((err)=>{
console.log(err);
module.exports = router; return res.status(500).send("Error while saving");
});
/* });
router.get('/api/remove/:id', (req, res)=>{
HomebrewModel.find({editId : req.params.id}, (err, objs)=>{
module.exports = function(app){ if(!objs.length || err) return res.status(404).send("Can not find homebrew with that id");
var resEntry = objs[0];
app; resEntry.remove((err)=>{
if(err) return res.status(500).send("Error while removing");
return res.status(200).send();
})
});
app.get('/api/search', mw.adminOnly, function(req, res){ });
var page = req.query.page || 0;
var count = req.query.count || 20; module.exports = router;
var query = {}; /*
if(req.query && req.query.id){
query = {
"$or" : [{
editId : req.query.id module.exports = function(app){
},{
shareId : req.query.id app;
}]
};
}
HomebrewModel.find(query, { app.get('/api/search', mw.adminOnly, function(req, res){
text : 0 //omit the text
}, { var page = req.query.page || 0;
skip: page*count, var count = req.query.count || 20;
limit: count*1
}, function(err, objs){ var query = {};
if(err) console.log(err); if(req.query && req.query.id){
return res.json({ query = {
page : page, "$or" : [{
count : count, editId : req.query.id
total : homebrewTotal, },{
brews : objs shareId : req.query.id
}); }]
};
}); }
})
HomebrewModel.find(query, {
text : 0 //omit the text
}, {
skip: page*count,
return app; limit: count*1
} }, function(err, objs){
if(err) console.log(err);
return res.json({
page : page,
count : count,
total : homebrewTotal,
brews : objs
});
});
})
return app;
}
*/ */

View File

@@ -1,81 +1,81 @@
const mongoose = require('mongoose'); var mongoose = require('mongoose');
const shortid = require('shortid'); var shortid = require('shortid');
const _ = require('lodash'); var _ = require('lodash');
const HomebrewSchema = mongoose.Schema({ var HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: shortid.generate, index: { unique: true } }, shareId : {type : String, default: shortid.generate, index: { unique: true }},
editId : { type: String, default: shortid.generate, index: { unique: true } }, editId : {type : String, default: shortid.generate, index: { unique: true }},
title : { type: String, default: '' }, title : {type : String, default : ""},
text : { type: String, default: '' }, text : {type : String, default : ""},
description : { type: String, default: '' }, description : {type : String, default : ""},
tags : { type: String, default: '' }, tags : {type : String, default : ""},
systems : [String], systems : [String],
authors : [String], authors : [String],
published : { type: Boolean, default: false }, published : {type : Boolean, default : false},
createdAt : { type: Date, default: Date.now }, createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now }, updatedAt : { type: Date, default: Date.now},
lastViewed : { type: Date, default: Date.now }, lastViewed : { type: Date, default: Date.now},
views : { type: Number, default: 0 }, views : {type:Number, default:0},
version : { type: Number, default: 1 } version : {type: Number, default:1}
}, { versionKey: false }); }, { versionKey: false });
HomebrewSchema.methods.sanatize = function(full=false){ HomebrewSchema.methods.sanatize = function(full=false){
const brew = this.toJSON(); const brew = this.toJSON();
delete brew._id; delete brew._id;
delete brew.__v; delete brew.__v;
if(full){ if(full){
delete brew.editId; delete brew.editId;
} }
return brew; return brew;
}; };
HomebrewSchema.methods.increaseView = function(){ HomebrewSchema.methods.increaseView = function(){
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject) => {
this.lastViewed = new Date(); this.lastViewed = new Date();
this.views = this.views + 1; this.views = this.views + 1;
this.save((err)=>{ this.save((err) => {
if(err) return reject(err); if(err) return reject(err);
return resolve(this); return resolve(this);
}); });
}); });
}; };
HomebrewSchema.statics.get = function(query){ HomebrewSchema.statics.get = function(query){
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject) => {
Homebrew.find(query, (err, brews)=>{ Homebrew.find(query, (err, brews)=>{
if(err || !brews.length) return reject('Can not find brew'); if(err || !brews.length) return reject('Can not find brew');
return resolve(brews[0]); return resolve(brews[0]);
}); });
}); });
}; };
HomebrewSchema.statics.getByUser = function(username, allowAccess=false){ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
return new Promise((resolve, reject)=>{ return new Promise((resolve, reject) => {
const query = { authors: username, published: true }; let query = {authors : username, published : true};
if(allowAccess){ if(allowAccess){
delete query.published; delete query.published;
} }
Homebrew.find(query, (err, brews)=>{ Homebrew.find(query, (err, brews)=>{
if(err) return reject('Can not find brew'); if(err) return reject('Can not find brew');
return resolve(_.map(brews, (brew)=>{ return resolve(_.map(brews, (brew)=>{
return brew.sanatize(!allowAccess); return brew.sanatize(!allowAccess);
})); }));
}); });
}); });
}; };
const Homebrew = mongoose.model('Homebrew', HomebrewSchema); var Homebrew = mongoose.model('Homebrew', HomebrewSchema);
module.exports = { module.exports = {
schema : HomebrewSchema, schema : HomebrewSchema,
model : Homebrew, model : Homebrew,
}; }

View File

@@ -0,0 +1,88 @@
const _ = require('lodash');
const config = require('nconf');
const utils = require('./utils.js');
const BrewData = require('./brew.data.js');
const router = require('express').Router();
const mw = require('./middleware.js');
const docs = {
welcomeBrew : require('fs').readFileSync('./welcome.brew.md', 'utf8'),
changelog : require('fs').readFileSync('./changelog.md', 'utf8'),
};
const vitreumRender = require('vitreum/steps/render');
const templateFn = require('../client/template.js');
//TODO: Catch errors here?
const renderPage = (req, res, next) => {
return vitreumRender('homebrew', templateFn, {
url : req.originalUrl,
version : require('../package.json').version,
loginPath : config.get('login_path'),
user : req.account && req.account.username,
brews : req.brews,
brew : req.brew
})
.then((page) => {
return res.send(page)
})
.catch(next);
};
//Share Page
router.get('/share/:shareId', mw.viewBrew, renderPage);
//Edit Page
router.get('/edit/:editId', mw.loadBrew, renderPage);
//Print Page
router.get('/print/:shareId', mw.viewBrew, renderPage);
//Source page
router.get('/source/:sharedId', mw.viewBrew, (req, res, next)=>{
const text = utils.replaceByMap(req.brew.text, { '<' : '&lt;', '>' : '&gt;' });
return res.send(`<code><pre>${text}</pre></code>`);
});
//User Page
router.get('/user/:username', (req, res, next) => {
BrewData.search({ user : req.params.username })
.then((brews) => {
req.brews = brews;
return next();
})
.catch(next);
}, renderPage);
//Search Page
router.get('/search', (req, res, next) => {
BrewData.search()
.then((brews) => {
req.brews = brews;
return next();
})
.catch(next);
}, renderPage);
//Changelog Page
router.get('/changelog', (req, res, next) => {
req.brew = {
text : docs.changelog,
title : 'Changelog'
};
return next();
}, renderPage);
//New Page
router.get('/new', renderPage);
//Home Page
router.get('/', (req, res, next) => {
req.brew = { text : docs.welcomeBrew };
return next();
}, renderPage);
module.exports = router;

69
server/middleware.js Normal file
View File

@@ -0,0 +1,69 @@
const _ = require('lodash');
const jwt = require('jwt-simple');
const auth = require('basic-auth');
const config = require('nconf');
const Error = require('./error.js');
const BrewData = require('./brew.data.js');
const Middleware = {
account : (req, res, next) => {
if(req.cookies && req.cookies.nc_session){
try{
req.account = jwt.decode(req.cookies.nc_session, config.get('jwt_secret'));
}catch(e){}
}
return next();
},
admin : (req, res, next) => {
req.admin = false;
if(req.headers['x-homebrew-admin'] === config.get('admin:key')){
req.admin = true;
}
return next();
},
//Filters
devOnly : (req, res, next) => {
const env = process.env.NODE_ENV;
if(env !== 'staging' && env !== 'production') return next();
return res.sendStatus(404);
},
adminOnly : (req, res, next) => {
if(req.admin) return next();
return next(Error.noAuth());
},
adminLogin : (req, res, next) => {
const creds = auth(req);
if(!creds
|| creds.name !== config.get('admin:user')
|| creds.pass !== config.get('admin:pass')){
res.setHeader('WWW-Authenticate', 'Basic realm="example"');
return next(Error.noAdmin());
}
return next();
},
//TODO: REMOVE
//Loaders
loadBrew : (req, res, next) => {
BrewData.getByEdit(req.params.editId)
.then((brew) => {
req.brew = brew;
return next()
})
.catch(next);
},
viewBrew : (req, res, next) => {
BrewData.getByShare(req.params.shareId)
.then((brew) => {
req.brew = brew;
return next()
})
.catch(next);
},
};
module.exports = Middleware;

23
server/utils.js Normal file
View File

@@ -0,0 +1,23 @@
const _ = require('lodash');
module.exports = {
getGoodBrewTitle : (text = '') => {
const titlePos = text.indexOf('# ');
if(titlePos !== -1){
let ending = text.indexOf('\n', titlePos);
ending = (ending == -1 ? undefined : ending);
return text.substring(titlePos + 2, ending).trim();
}else{
return (_.find(text.split('\n'), (line)=>{
return line;
}) || '').trim();
}
},
replaceByMap : (text, mapping) => {
return _.reduce(mapping, (r, search, replace) => {
return r.split(search).join(replace)
}, text)
}
}

View File

@@ -0,0 +1,18 @@
const Store = require('./account.store.js');
const Actions = {
init : (initState) => {
Store.init(initState);
},
login : ()=>{
window.location = Store.getLoginPath();
},
logout : ()=>{
document.cookie = 'nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;domain=.naturalcrit.com';
//Remove local dev cookies too
document.cookie = 'nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;';
window.location ='/';
}
};
module.exports = Actions;

View File

@@ -0,0 +1,27 @@
const _ = require('lodash');
const flux = require('pico-flux');
let State = {
loginPath : '',
user : undefined,
};
const Store = {}; //Maybe Flux it later?
Store.init = (state)=>{
State = _.merge({}, State, state);
};
Store.getLoginPath = ()=>{
let path = State.loginPath;
if(typeof window !== 'undefined'){
path = `${path}?redirect=${encodeURIComponent(window.location.href)}`;
}
return path;
};
Store.getUser = ()=>{
return State.user;
};
module.exports = Store;

View File

@@ -0,0 +1,78 @@
const _ = require('lodash');
const dispatch = require('pico-flux').dispatch;
const request = require('superagent');
const Store = require('./brew.store.js');
let pendingTimer;
const PENDING_TIMEOUT = 3000;
const APIActions = {
save : () => {
clearTimeout(pendingTimer);
const brew = Store.getBrew();
dispatch('SET_STATUS', 'saving');
request
.put('/api/brew/' + brew.editId)
.send(brew)
.end((err, res) => {
if(err) return dispatch('SET_STATUS', 'error', err);
dispatch('SET_BREW', res.body);
dispatch('SET_STATUS', 'ready');
});
},
create : () => {
dispatch('SET_STATUS', 'saving');
request.post('/api/brew')
.send(Store.getBrew())
.end((err, res)=>{
if(err) return dispatch('SET_STATUS', 'error', err);
localStorage.setItem('homebrewery-new', null);
const brew = res.body;
window.location = '/edit/' + brew.editId;
});
},
delete : (editId) => {
dispatch('SET_STATUS', 'deleting');
request.delete('/api/brew/' + editId)
.send()
.end((err, res)=>{
if(err) return dispatch('SET_STATUS', 'error', err);
window.location = '/';
});
}
}
const Actions = {
init : (initState) => {
const filteredState = _.reduce(initState, (r, val, key) => {
if(typeof val !== 'undefined') r[key] = val;
return r;
}, {});
Store.init(filteredState);
},
setBrew : (brew) => {
dispatch('SET_BREW', brew);
},
updateBrewText : (brewText) => {
dispatch('UPDATE_BREW_TEXT', brewText)
},
updateMetaData : (meta) => {
dispatch('UPDATE_META', meta);
},
pendingSave : () => {
clearTimeout(pendingTimer);
pendingTimer = setTimeout(APIActions.save, PENDING_TIMEOUT);
dispatch('SET_STATUS', 'pending');
},
localPrint : ()=>{
localStorage.setItem('print', Store.getBrewText());
window.open('/print?dialog=true&local=print','_blank');
},
print : ()=>{
window.open(`/print/${Store.getBrew().shareId}?dialog=true`, '_blank').focus();
}
};
module.exports = _.merge(Actions, APIActions);

View File

@@ -0,0 +1,69 @@
const _ = require('lodash');
const flux = require('pico-flux');
const Markdown = require('homebrewery/markdown.new.js');
let State = {
version : '0.0.0',
brew : {
text : '',
shareId : undefined,
editId : undefined,
createdAt : undefined,
updatedAt : undefined,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
},
errors : [],
status : 'ready', //ready, pending, saving, error
};
const Store = flux.createStore({
SET_BREW : (brew) => {
State.brew = brew;
},
UPDATE_BREW_TEXT : (brewText) => {
State.brew.text = brewText;
State.errors = Markdown.validate(brewText);
},
UPDATE_META : (meta) => {
State.brew = _.merge({}, State.brew, meta);
},
SET_STATUS : (status, error) => {
if(status == State.status) return false;
if(error) State.errors = error;
State.status = status;
}
});
Store.init = (state)=>{
State = _.merge({}, State, state);
};
Store.getBrew = ()=>{
return State.brew;
};
Store.getBrewText = ()=>{
return State.brew.text;
};
Store.getMetaData = ()=>{
return _.omit(State.brew, ['text']);
};
Store.getErrors = ()=>{
return State.errors;
};
Store.getVersion = ()=>{
return State.version;
};
Store.getStatus = ()=>{
return State.status;
};
module.exports = Store;

View File

@@ -1,146 +1,131 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class'); const _ = require('lodash');
const _ = require('lodash'); const cx = require('classnames');
const cx = require('classnames');
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx'); const SnippetBar = require('./snippetbar/snippetbar.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx'); const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index);
const splice = function(str, index, inject){ };
return str.slice(0, index) + inject + str.slice(index);
}; const SNIPPETBAR_HEIGHT = 25;
const SNIPPETBAR_HEIGHT = 25; const BrewEditor = React.createClass({
getDefaultProps: function() {
const Editor = createClass({ return {
getDefaultProps : function() { value : '',
return { onChange : ()=>{},
value : '',
onChange : ()=>{}, metadata : {},
onMetadataChange : ()=>{},
metadata : {}, };
onMetadataChange : ()=>{}, },
}; getInitialState: function() {
}, return {
getInitialState : function() { showMetadataEditor: false
return { };
showMetadataEditor : false },
}; cursorPosition : {
}, line : 0,
cursorPosition : { ch : 0
line : 0, },
ch : 0
}, componentDidMount: function() {
this.updateEditorSize();
componentDidMount : function() { this.highlightPageLines();
this.updateEditorSize(); window.addEventListener("resize", this.updateEditorSize);
this.highlightPageLines(); },
window.addEventListener('resize', this.updateEditorSize); componentWillUnmount: function() {
}, window.removeEventListener("resize", this.updateEditorSize);
componentWillUnmount : function() { },
window.removeEventListener('resize', this.updateEditorSize);
}, updateEditorSize : function() {
let paneHeight = this.refs.main.parentNode.clientHeight;
updateEditorSize : function() { paneHeight -= SNIPPETBAR_HEIGHT + 1;
let paneHeight = this.refs.main.parentNode.clientHeight; this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
paneHeight -= SNIPPETBAR_HEIGHT + 1; },
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
}, handleTextChange : function(text){
this.props.onChange(text);
handleTextChange : function(text){ },
this.props.onChange(text); handleCursorActivty : function(curpos){
}, this.cursorPosition = curpos;
handleCursorActivty : function(curpos){ },
this.cursorPosition = curpos; handleInject : function(injectText){
}, const lines = this.props.value.split('\n');
handleInject : function(injectText){ lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
const lines = this.props.value.split('\n');
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText); this.handleTextChange(lines.join('\n'));
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
this.handleTextChange(lines.join('\n')); },
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length); handgleToggle : function(){
}, this.setState({
handgleToggle : function(){ showMetadataEditor : !this.state.showMetadataEditor
this.setState({ })
showMetadataEditor : !this.state.showMetadataEditor },
});
}, brewJump : function(){
const currentPage = this.getCurrentPage();
getCurrentPage : function(){ window.location.hash = 'p' + currentPage;
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1); },
return _.reduce(lines, (r, line)=>{
if(line.indexOf('\\page') !== -1) r++; //Called when there are changes to the editor's dimensions
return r; update : function(){
}, 1); this.refs.codeEditor.updateSize();
}, },
highlightPageLines : function(){ //TODO: convert this into a generic function for columns and blocks
if(!this.refs.codeEditor) return; highlightPageLines : function(){
const codeMirror = this.refs.codeEditor.codeMirror; if(!this.refs.codeEditor) return;
const codeMirror = this.refs.codeEditor.codeMirror;
const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
if(line.indexOf('\\page') !== -1){ const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
codeMirror.addLineClass(lineNumber, 'background', 'pageLine'); if(line.indexOf('\\page') !== -1){
r.push(lineNumber); codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
} r.push(lineNumber);
return r; }
}, []); return r;
return lineNumbers; }, []);
}, return lineNumbers
},
brewJump : function(){ renderMetadataEditor : function(){
const currentPage = this.getCurrentPage(); if(!this.state.showMetadataEditor) return;
window.location.hash = `p${currentPage}`; return <MetadataEditor
}, metadata={this.props.metadata}
onChange={this.props.onMetadataChange}
//Called when there are changes to the editor's dimensions />
update : function(){ },
this.refs.codeEditor.updateSize();
}, render : function(){
renderMetadataEditor : function(){ this.highlightPageLines();
if(!this.state.showMetadataEditor) return;
return <MetadataEditor return<div className='brewEditor' ref='main'>
metadata={this.props.metadata} <SnippetBar
onChange={this.props.onMetadataChange} brew={this.props.value}
/>; onInject={this.handleInject}
}, onToggle={this.handgleToggle}
showmeta={this.state.showMetadataEditor} />
render : function(){ {this.renderMetadataEditor()}
this.highlightPageLines(); <CodeEditor
return ( ref='codeEditor'
<div className='editor' ref='main'> wrap={true}
<SnippetBar language='gfm'
brew={this.props.value} value={this.props.value}
onInject={this.handleInject} onChange={this.handleTextChange}
onToggle={this.handgleToggle} onCursorActivity={this.handleCursorActivty} />
showmeta={this.state.showMetadataEditor} /> </div>
{this.renderMetadataEditor()}
<CodeEditor /*
ref='codeEditor' <div className='brewJump' onClick={this.brewJump}>
wrap={true} <i className='fa fa-arrow-right' />
language='gfm' </div>
value={this.props.value} */
onChange={this.handleTextChange} }
onCursorActivity={this.handleCursorActivty} /> });
{/* module.exports = BrewEditor;
<div className='brewJump' onClick={this.brewJump}>
<i className='fa fa-arrow-right' />
</div>
*/}
</div>
);
}
});
module.exports = Editor;

View File

@@ -1,8 +1,7 @@
.editor{ .brewEditor{
position : relative; position : relative;
width : 100%; width : 100%;
.codeEditor{ .codeEditor{
height : 100%; height : 100%;
.pageLine{ .pageLine{
@@ -25,5 +24,4 @@
justify-content:center; justify-content:center;
.tooltipLeft("Jump to brew page"); .tooltipLeft("Jump to brew page");
} }
} }

View File

@@ -0,0 +1,13 @@
const Actions = require('homebrewery/brew.actions.js');
const Store = require('homebrewery/brew.store.js');
const BrewEditor = require('./brewEditor.jsx')
module.exports = Store.createSmartComponent(BrewEditor, ()=>{
return {
value : Store.getBrewText(),
onChange : Actions.updateBrewText,
metadata : Store.getMetaData(),
onMetadataChange : Actions.updateMetaData,
};
});

View File

@@ -1,22 +1,21 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require('superagent'); const request = require("superagent");
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']; const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']
const MetadataEditor = createClass({ const MetadataEditor = React.createClass({
getDefaultProps : function() { getDefaultProps: function() {
return { return {
metadata : { metadata: {
editId : null, editId : null,
title : '', title : '',
description : '', description : '',
tags : '', tags : '',
published : false, published : false,
authors : [], authors : [],
systems : [] systems : []
}, },
onChange : ()=>{} onChange : ()=>{}
}; };
@@ -25,12 +24,12 @@ const MetadataEditor = createClass({
handleFieldChange : function(name, e){ handleFieldChange : function(name, e){
this.props.onChange(_.merge({}, this.props.metadata, { this.props.onChange(_.merge({}, this.props.metadata, {
[name] : e.target.value [name] : e.target.value
})); }))
}, },
handleSystem : function(system, e){ handleSystem : function(system, e){
if(e.target.checked){ if(e.target.checked){
this.props.metadata.systems.push(system); this.props.metadata.systems.push(system);
} else { }else{
this.props.metadata.systems = _.without(this.props.metadata.systems, system); this.props.metadata.systems = _.without(this.props.metadata.systems, system);
} }
this.props.onChange(this.props.metadata); this.props.onChange(this.props.metadata);
@@ -42,10 +41,10 @@ const MetadataEditor = createClass({
}, },
handleDelete : function(){ handleDelete : function(){
if(!confirm('are you sure you want to delete this brew?')) return; 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(!confirm("are you REALLY sure? You will not be able to recover it")) return;
request.get(`/api/remove/${this.props.metadata.editId}`) request.get('/api/remove/' + this.props.metadata.editId)
.send() .send()
.end(function(err, res){ .end(function(err, res){
window.location.href = '/'; window.location.href = '/';
@@ -68,21 +67,22 @@ const MetadataEditor = createClass({
<input <input
type='checkbox' type='checkbox'
checked={_.includes(this.props.metadata.systems, val)} checked={_.includes(this.props.metadata.systems, val)}
onChange={()=>this.handleSystem(val)} /> onChange={this.handleSystem.bind(null, val)} />
{val} {val}
</label>; </label>
}); });
}, },
renderPublish : function(){ renderPublish : function(){
//TODO: Move the publish element into here
if(this.props.metadata.published){ if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}> return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}>
<i className='fa fa-ban' /> unpublish <i className='fa fa-ban' /> unpublish
</button>; </button>
} else { }else{
return <button className='publish' onClick={()=>this.handlePublish(true)}> return <button className='publish' onClick={this.handlePublish.bind(null, true)}>
<i className='fa fa-globe' /> publish <i className='fa fa-globe' /> publish
</button>; </button>
} }
}, },
@@ -96,7 +96,7 @@ const MetadataEditor = createClass({
<i className='fa fa-trash' /> delete brew <i className='fa fa-trash' /> delete brew
</button> </button>
</div> </div>
</div>; </div>
}, },
renderAuthors : function(){ renderAuthors : function(){
@@ -109,7 +109,7 @@ const MetadataEditor = createClass({
<div className='value'> <div className='value'>
{text} {text}
</div> </div>
</div>; </div>
}, },
renderShareToReddit : function(){ renderShareToReddit : function(){
@@ -124,7 +124,7 @@ const MetadataEditor = createClass({
</button> </button>
</a> </a>
</div> </div>
</div>; </div>
}, },
render : function(){ render : function(){
@@ -133,20 +133,19 @@ const MetadataEditor = createClass({
<label>title</label> <label>title</label>
<input type='text' className='value' <input type='text' className='value'
value={this.props.metadata.title} value={this.props.metadata.title}
onChange={()=>this.handleFieldChange('title')} /> onChange={this.handleFieldChange.bind(null, 'title')} />
</div> </div>
<div className='field description'> <div className='field description'>
<label>description</label> <label>description</label>
<textarea value={this.props.metadata.description} className='value' <textarea value={this.props.metadata.description} className='value'
onChange={()=>this.handleFieldChange('description')} /> onChange={this.handleFieldChange.bind(null, 'description')} />
</div> </div>
{/*} <div className='field thumbnail'>
<div className='field tags'> <label>thumbnail</label>
<label>tags</label> <input type='text' className='value'
<textarea value={this.props.metadata.tags} value={this.props.metadata.thumbnail}
onChange={()=>this.handleFieldChange('tags')} /> onChange={this.handleFieldChange.bind(null, 'thumbnail')} />
</div> </div>
*/}
<div className='field systems'> <div className='field systems'>
<label>systems</label> <label>systems</label>
@@ -169,7 +168,7 @@ const MetadataEditor = createClass({
{this.renderDelete()} {this.renderDelete()}
</div>; </div>
} }
}); });

View File

@@ -76,4 +76,7 @@
font-size: 0.8em; font-size: 0.8em;
line-height : 1.5em; line-height : 1.5em;
} }
.thumbnail.field{
}
} }

View File

@@ -1,5 +1,4 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -9,14 +8,14 @@ const Snippets = require('./snippets/snippets.js');
const execute = function(val, brew){ const execute = function(val, brew){
if(_.isFunction(val)) return val(brew); if(_.isFunction(val)) return val(brew);
return val; return val;
}; }
const Snippetbar = createClass({ const Snippetbar = React.createClass({
getDefaultProps : function() { getDefaultProps: function() {
return { return {
brew : '', brew : '',
onInject : ()=>{}, onInject : ()=>{},
onToggle : ()=>{}, onToggle : ()=>{},
showmeta : false showmeta : false
@@ -24,7 +23,7 @@ const Snippetbar = createClass({
}, },
handleSnippetClick : function(injectedText){ handleSnippetClick : function(injectedText){
this.props.onInject(injectedText); this.props.onInject(injectedText)
}, },
renderSnippetGroups : function(){ renderSnippetGroups : function(){
@@ -36,18 +35,18 @@ const Snippetbar = createClass({
snippets={snippetGroup.snippets} snippets={snippetGroup.snippets}
key={snippetGroup.groupName} key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick} onSnippetClick={this.handleSnippetClick}
/>; />
}); })
}, },
render : function(){ render : function(){
return <div className='snippetBar'> return <div className='snippetBar'>
{this.renderSnippetGroups()} {this.renderSnippetGroups()}
<div className={cx('toggleMeta', { selected: this.props.showmeta })} <div className={cx('toggleMeta', {selected: this.props.showmeta})}
onClick={this.props.onToggle}> onClick={this.props.onToggle}>
<i className='fa fa-bars' /> <i className='fa fa-bars' />
</div> </div>
</div>; </div>
} }
}); });
@@ -58,13 +57,13 @@ module.exports = Snippetbar;
const SnippetGroup = createClass({ const SnippetGroup = React.createClass({
getDefaultProps : function() { getDefaultProps: function() {
return { return {
brew : '', brew : '',
groupName : '', groupName : '',
icon : 'fa-rocket', icon : 'fa-rocket',
snippets : [], snippets : [],
onSnippetClick : function(){}, onSnippetClick : function(){},
}; };
}, },
@@ -73,23 +72,23 @@ const SnippetGroup = createClass({
}, },
renderSnippets : function(){ renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{ return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}> return <div className='snippet' key={snippet.name} onClick={this.handleSnippetClick.bind(this, snippet)}>
<i className={`fa fa-fw ${snippet.icon}`} /> <i className={'fa fa-fw ' + snippet.icon} />
{snippet.name} {snippet.name}
</div>; </div>
}); })
}, },
render : function(){ render : function(){
return <div className='snippetGroup'> return <div className='snippetGroup'>
<div className='text'> <div className='text'>
<i className={`fa fa-fw ${this.props.icon}`} /> <i className={'fa fa-fw ' + this.props.icon} />
<span className='groupName'>{this.props.groupName}</span> <span className='groupName'>{this.props.groupName}</span>
</div> </div>
<div className='dropdown'> <div className='dropdown'>
{this.renderSnippets()} {this.renderSnippets()}
</div> </div>
</div>; </div>
}, },
}); });

View File

@@ -13,7 +13,6 @@
cursor : pointer; cursor : pointer;
line-height : @height; line-height : @height;
text-align : center; text-align : center;
.tooltipLeft("Edit Brew Metadata");
&:hover, &.selected{ &:hover, &.selected{
background-color : #999; background-color : #999;
} }

View File

@@ -0,0 +1,42 @@
var _ = require('lodash');
module.exports = function(classname){
classname = classname || _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge'])
classname = classname.toLowerCase();
var hitDie = _.sample([4, 6, 8, 10, 12]);
var abilityList = ["Strength", "Dexerity", "Constitution", "Wisdom", "Charisma", "Intelligence"];
var skillList = ["Acrobatics ", "Animal Handling", "Arcana", "Athletics", "Deception", "History", "Insight", "Intimidation", "Investigation", "Medicine", "Nature", "Perception", "Performance", "Persuasion", "Religion", "Sleight of Hand", "Stealth", "Survival"];
return [
"## Class Features",
"As a " + classname + ", you gain the following class features",
"#### Hit Points",
"___",
"- **Hit Dice:** 1d" + hitDie + " per " + classname + " level",
"- **Hit Points at 1st Level:** " + hitDie + " + your Constitution modifier",
"- **Hit Points at Higher Levels:** 1d" + hitDie + " (or " + (hitDie/2 + 1) + ") + your Constitution modifier per " + classname + " level after 1st",
"",
"#### Proficiencies",
"___",
"- **Armor:** " + (_.sampleSize(["Light armor", "Medium armor", "Heavy armor", "Shields"], _.random(0,3)).join(', ') || "None"),
"- **Weapons:** " + (_.sampleSize(["Squeegee", "Rubber Chicken", "Simple weapons", "Martial weapons"], _.random(0,2)).join(', ') || "None"),
"- **Tools:** " + (_.sampleSize(["Artian's tools", "one musical instrument", "Thieve's tools"], _.random(0,2)).join(', ') || "None"),
"",
"___",
"- **Saving Throws:** " + (_.sampleSize(abilityList, 2).join(', ')),
"- **Skills:** Choose two from " + (_.sampleSize(skillList, _.random(4, 6)).join(', ')),
"",
"#### Equipment",
"You start with the following equipment, in addition to the equipment granted by your background:",
"- *(a)* a martial weapon and a shield or *(b)* two martial weapons",
"- *(a)* five javelins or *(b)* any simple melee weapon",
"- " + (_.sample(["10 lint fluffs", "1 button", "a cherished lost sock"])),
"\n\n\n"
].join('\n');
}

View File

@@ -0,0 +1,114 @@
var _ = require('lodash');
var features = [
"Astrological Botany",
"Astrological Chemistry",
"Biochemical Sorcery",
"Civil Alchemy",
"Consecrated Biochemistry",
"Demonic Anthropology",
"Divinatory Mineralogy",
"Genetic Banishing",
"Hermetic Geography",
"Immunological Incantations",
"Nuclear Illusionism",
"Ritual Astronomy",
"Seismological Divination",
"Spiritual Biochemistry",
"Statistical Occultism",
"Police Necromancer",
"Sixgun Poisoner",
"Pharmaceutical Gunslinger",
"Infernal Banker",
"Spell Analyst",
"Gunslinger Corruptor",
"Torque Interfacer",
"Exo Interfacer",
"Gunpowder Torturer",
"Orbital Gravedigger",
"Phased Linguist",
"Mathematical Pharmacist",
"Plasma Outlaw",
"Malefic Chemist",
"Police Cultist"
];
var classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
var levels = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", "20th"]
var profBonus = [2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6];
var getFeature = (level)=>{
var res = []
if(_.includes([4,6,8,12,14,16,19], level+1)){
res = ["Ability Score Improvement"]
}
res = _.union(res, _.sampleSize(features, _.sample([0,1,1,1,1,1])));
if(!res.length) return "─";
return res.join(', ');
}
module.exports = {
full : function(){
var classname = _.sample(classnames)
var maxes = [4,3,3,3,3,2,2,1,1]
var drawSlots = function(Slots){
var slots = Number(Slots);
return _.times(9, function(i){
var max = maxes[i];
if(slots < 1) return "—";
var res = _.min([max, slots]);
slots -= res;
return res;
}).join(' | ')
}
var cantrips = 3;
var spells = 1;
var 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" +
_.map(levels, function(levelName, level){
var res = [
levelName,
"+" + profBonus[level],
getFeature(level),
cantrips,
spells,
drawSlots(slots)
].join(' | ');
cantrips += _.random(0,1);
spells += _.random(0,1);
slots += _.random(0,2);
return "| " + res + " |";
}).join('\n') +'\n</div>\n\n';
},
half : function(){
var classname = _.sample(classnames)
var featureScore = 1
return "<div class='classTable'>\n##### The " + classname + "\n" +
"| Level | Proficiency Bonus | Features | " + _.sample(features) + "|\n" +
"|:---:|:---:|:---|:---:|\n" +
_.map(levels, function(levelName, level){
var res = [
levelName,
"+" + profBonus[level],
getFeature(level),
"+" + featureScore
].join(' | ');
featureScore += _.random(0,1);
return "| " + res + " |";
}).join('\n') +'\n</div>\n\n';
}
};

View File

@@ -0,0 +1,117 @@
var _ = require('lodash');
var titles = [
"The Burning Gallows",
"The Ring of Nenlast",
"Below the Blind Tavern",
"Below the Hungering River",
"Before Bahamut's Land",
"The Cruel Grave from Within",
"The Strength of Trade Road",
"Through The Raven Queen's Worlds",
"Within the Settlement",
"The Crown from Within",
"The Merchant Within the Battlefield",
"Ioun's Fading Traveler",
"The Legion Ingredient",
"The Explorer Lure",
"Before the Charming Badlands",
"The Living Dead Above the Fearful Cage",
"Vecna's Hidden Sage",
"Bahamut's Demonspawn",
"Across Gruumsh's Elemental Chaos",
"The Blade of Orcus",
"Beyond Revenge",
"Brain of Insanity",
"Breed Battle!, A New Beginning",
"Evil Lake, A New Beginning",
"Invasion of the Gigantic Cat, Part II",
"Kraken War 2020",
"The Body Whisperers",
"The Diabolical Tales of the Ape-Women",
"The Doctor Immortal",
"The Doctor from Heaven",
"The Graveyard",
"Azure Core",
"Core Battle",
"Core of Heaven: The Guardian of Amazement",
"Deadly Amazement III",
"Dry Chaos IX",
"Gate Thunder",
"Guardian: Skies of the Dark Wizard",
"Lute of Eternity",
"Mercury's Planet: Brave Evolution",
"Ruby of Atlantis: The Quake of Peace",
"Sky of Zelda: The Thunder of Force",
"Vyse's Skies",
"White Greatness III",
"Yellow Divinity",
"Zidane's Ghost"
];
var subtitles = [
"In an ominous universe, a botanist opposes terrorism.",
"In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.",
"In a land of corruption, two cyberneticists and a dungeon delver search for freedom.",
"In an evil empire of horror, two rangers battle the forces of hell.",
"In a lost city, in an age of sorcery, a librarian quests for revenge.",
"In a universe of illusions and danger, three time travellers and an adventurer search for justice.",
"In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.",
"In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.",
"In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.",
"In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.",
"In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.",
"In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.",
"In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.",
"In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.",
"In a kingdom of deception, a reporter searches for fame.",
"In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.",
"In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.",
"In a dark city of confusion, three swordswomen and a singer battle lawlessness.",
"In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.",
"In a kingdom of panic, six adventurers oppose lawlessness.",
"In a land of dreams and hopelessness, three hackers and a cyborg search for justice.",
"On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.",
"In a wicked universe, five seers fight lawlessness.",
"In a kingdom of death, in an era of illusion and blood, four colonists search for fame.",
"In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.",
"In a cursed empire, five inventors oppose terrorism.",
"On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.",
"In a forgotten land, a reporter and a spy try to stop the apocalypse.",
"In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.",
"On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.",
"In a galaxy of dark magic, four fighters seek freedom.",
"In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.",
"In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.",
"In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.",
"In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.",
"On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.",
"In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.",
"In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.",
"In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.",
"In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.",
"In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.",
"In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.",
"In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.",
"In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.",
"In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime."
];
module.exports = () => {
return `<style>
.phb#p1{ text-align:center; }
.phb#p1:after{ display:none; }
</style>
<div style='margin-top:450px;'></div>
# ${_.sample(titles)}
<div style='margin-top:25px'></div>
<div class='wide'>
##### ${_.sample(subtitles)}
</div>
\\page`
}

View File

@@ -0,0 +1,43 @@
var _ = require('lodash');
var ClassFeatureGen = require('./classfeature.gen.js');
var ClassTableGen = require('./classtable.gen.js');
module.exports = function(){
var classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'])
var image = _.sample(_.map([
"http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png",
"http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png",
"http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png",
"http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png",
"http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png",
"http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png",
"http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png",
"http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png",
], function(url){
return "<img src = '" + url + "' style='max-width:8cm;max-height:25cm' />"
}))
return [
image,
"",
"```",
"```",
"<div style='margin-top:240px'></div>\n\n",
"## " + classname,
"Cool intro stuff will go here",
"\\page",
ClassTableGen(classname),
ClassFeatureGen(classname),
].join('\n') + '\n\n\n';
};

View File

@@ -0,0 +1,91 @@
var _ = require('lodash');
var spellNames = [
"Astral Rite of Acne",
"Create Acne",
"Cursed Ramen Erruption",
"Dark Chant of the Dentists",
"Erruption of Immaturity",
"Flaming Disc of Inconvenience",
"Heal Bad Hygene",
"Heavenly Transfiguration of the Cream Devil",
"Hellish Cage of Mucus",
"Irritate Peanut Butter Fairy",
"Luminous Erruption of Tea",
"Mystic Spell of the Poser",
"Sorcerous Enchantment of the Chimneysweep",
"Steak Sauce Ray",
"Talk to Groupie",
"Astonishing Chant of Chocolate",
"Astounding Pasta Puddle",
"Ball of Annoyance",
"Cage of Yarn",
"Control Noodles Elemental",
"Create Nervousness",
"Cure Baldness",
"Cursed Ritual of Bad Hair",
"Dispell Piles in Dentist",
"Eliminate Florists",
"Illusionary Transfiguration of the Babysitter",
"Necromantic Armor of Salad Dressing",
"Occult Transfiguration of Foot Fetish",
"Protection from Mucus Giant",
"Tinsel Blast",
"Alchemical Evocation of the Goths",
"Call Fangirl",
"Divine Spell of Crossdressing",
"Dominate Ramen Giant",
"Eliminate Vindictiveness in Gym Teacher",
"Extra-Planar Spell of Irritation",
"Induce Whining in Babysitter",
"Invoke Complaining",
"Magical Enchantment of Arrogance",
"Occult Globe of Salad Dressing",
"Overwhelming Enchantment of the Chocolate Fairy",
"Sorcerous Dandruff Globe",
"Spiritual Invocation of the Costumers",
"Ultimate Rite of the Confetti Angel",
"Ultimate Ritual of Mouthwash",
];
module.exports = {
spellList : function(){
var levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
var content = _.map(levels, (level)=>{
var spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
return `- ${spell}`;
}).join('\n');
return `##### ${level} \n${spells} \n`;
}).join('\n');
return `<div class='spellList'>\n${content}\n</div>`;
},
spell : function(){
var level = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th"];
var spellSchools = ["abjuration", "conjuration", "divination", "enchantment", "evocation", "illusion", "necromancy", "transmutation"];
var components = _.sampleSize(["V", "S", "M"], _.random(1,3)).join(', ');
if(components.indexOf("M") !== -1){
components += " (" + _.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1,3)).join(', ') + ")"
}
return [
"#### " + _.sample(spellNames),
"*" + _.sample(level) + "-level " + _.sample(spellSchools) + "*",
"___",
"- **Casting Time:** 1 action",
"- **Range:** " + _.sample(["Self", "Touch", "30 feet", "60 feet"]),
"- **Components:** " + components,
"- **Duration:** " + _.sample(["Until dispelled", "1 round", "Instantaneous", "Concentration, up to 10 minutes", "1 hour"]),
"",
"A flame, equivalent in brightness to a torch, springs from from an object that you touch. ",
"The effect look like a regular flame, but it creates no heat and doesn't use oxygen. ",
"A *continual flame* can be covered or hidden but not smothered or quenched.",
"\n\n\n"
].join('\n');
}
}

View File

@@ -0,0 +1,196 @@
var _ = require('lodash');
var genList = function(list, max){
return _.sampleSize(list, _.random(0,max)).join(', ') || "None";
}
var getMonsterName = function(){
return _.sample([
"All-devouring Baseball Imp",
"All-devouring Gumdrop Wraith",
"Chocolate Hydra",
"Devouring Peacock",
"Economy-sized Colossus of the Lemonade Stand",
"Ghost Pigeon",
"Gibbering Duck",
"Sparklemuffin Peacock Spider",
"Gum Elemental",
"Illiterate Construct of the Candy Store",
"Ineffable Chihuahua",
"Irritating Death Hamster",
"Irritating Gold Mouse",
"Juggernaut Snail",
"Juggernaut of the Sock Drawer",
"Koala of the Cosmos",
"Mad Koala of the West",
"Milk Djinni of the Lemonade Stand",
"Mind Ferret",
"Mystic Salt Spider",
"Necrotic Halitosis Angel",
"Pinstriped Famine Sheep",
"Ritalin Leech",
"Shocker Kangaroo",
"Stellar Tennis Juggernaut",
"Wailing Quail of the Sun",
"Angel Pigeon",
"Anime Sphinx",
"Bored Avalanche Sheep of the Wasteland",
"Devouring Nougat Sphinx of the Sock Drawer",
"Djinni of the Footlocker",
"Ectoplasmic Jazz Devil",
"Flatuent Angel",
"Gelatinous Duck of the Dream-Lands",
"Gelatinous Mouse",
"Golem of the Footlocker",
"Lich Wombat",
"Mechanical Sloth of the Past",
"Milkshake Succubus",
"Puffy Bone Peacock of the East",
"Rainbow Manatee",
"Rune Parrot",
"Sand Cow",
"Sinister Vanilla Dragon",
"Snail of the North",
"Spider of the Sewer",
"Stellar Sawdust Leech",
"Storm Anteater of Hell",
"Stupid Spirit of the Brewery",
"Time Kangaroo",
"Tomb Poodle",
]);
}
var getType = function(){
return _.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast']) + " " + _.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])
}
var getAlignment = function(){
return _.sample([
"annoying evil",
"chaotic gossipy",
"chaotic sloppy",
"depressed neutral",
"lawful bogus",
"lawful coy",
"manic-depressive evil",
"narrow-minded neutral",
"neutral annoying",
"neutral ignorant",
"oedpipal neutral",
"silly neutral",
"unoriginal neutral",
"weird neutral",
"wordy evil",
"unaligned"
]);
};
var getStats = function(){
return '>|' + _.times(6, function(){
var num = _.random(1,20);
var mod = Math.ceil(num/2 - 5)
return num + " (" + (mod >= 0 ? '+'+mod : mod ) + ")"
}).join('|') + '|';
}
var 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.",
]);
}
var genAction = function(){
var name = _.sample([
"Abdominal Drop",
"Airplane Hammer",
"Atomic Death Throw",
"Bulldog Rake",
"Corkscrew Strike",
"Crossed Splash",
"Crossface Suplex",
"DDT Powerbomb",
"Dual Cobra Wristlock",
"Dual Throw",
"Elbow Hold",
"Gory Body Sweep",
"Heel Jawbreaker",
"Jumping Driver",
"Open Chin Choke",
"Scorpion Flurry",
"Somersault Stump Fists",
"Suffering Wringer",
"Super Hip Submission",
"Super Spin",
"Team Elbow",
"Team Foot",
"Tilt-a-whirl Chin Sleeper",
"Tilt-a-whirl Eye Takedown",
"Turnbuckle Roll"
])
return "> ***" + name + ".*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) ";
}
module.exports = {
full : function(){
return [
"___",
"___",
"> ## " + getMonsterName(),
">*" + getType() + ", " + getAlignment() + "*",
"> ___",
"> - **Armor Class** " + _.random(10,20),
"> - **Hit Points** " + _.random(1, 150) + "(1d4 + 5)",
"> - **Speed** " + _.random(0,50) + "ft.",
">___",
">|STR|DEX|CON|INT|WIS|CHA|",
">|:---:|:---:|:---:|:---:|:---:|:---:|",
getStats(),
">___",
"> - **Condition Immunities** " + genList(["groggy", "swagged", "weak-kneed", "buzzed", "groovy", "melancholy", "drunk"], 3),
"> - **Senses** passive Perception " + _.random(3, 20),
"> - **Languages** " + genList(["Common", "Pottymouth", "Gibberish", "Latin", "Jive"], 2),
"> - **Challenge** " + _.random(0, 15) + " (" + _.random(10,10000)+ " XP)",
"> ___",
_.times(_.random(3,6), function(){
return genAbilities()
}).join('\n>\n'),
"> ### Actions",
_.times(_.random(4,6), function(){
return genAction()
}).join('\n>\n'),
].join('\n') + '\n\n\n';
},
half : function(){
return [
"___",
"> ## " + getMonsterName(),
">*" + getType() + ", " + getAlignment() + "*",
"> ___",
"> - **Armor Class** " + _.random(10,20),
"> - **Hit Points** " + _.random(1, 150) + "(1d4 + 5)",
"> - **Speed** " + _.random(0,50) + "ft.",
">___",
">|STR|DEX|CON|INT|WIS|CHA|",
">|:---:|:---:|:---:|:---:|:---:|:---:|",
getStats(),
">___",
"> - **Condition Immunities** " + genList(["groggy", "swagged", "weak-kneed", "buzzed", "groovy", "melancholy", "drunk"], 3),
"> - **Senses** passive Perception " + _.random(3, 20),
"> - **Languages** " + genList(["Common", "Pottymouth", "Gibberish", "Latin", "Jive"], 2),
"> - **Challenge** " + _.random(0, 15) + " (" + _.random(10,10000)+ " XP)",
"> ___",
_.times(_.random(0,2), function(){
return genAbilities()
}).join('\n>\n'),
"> ### Actions",
_.times(_.random(1,2), function(){
return genAction()
}).join('\n>\n'),
].join('\n') + '\n\n\n';
}
}

View File

@@ -0,0 +1,267 @@
var MagicGen = require('./magic.gen.js');
var ClassTableGen = require('./classtable.gen.js');
var MonsterBlockGen = require('./monsterblock.gen.js');
var ClassFeatureGen = require('./classfeature.gen.js');
var FullClassGen = require('./fullclass.gen.js');
var CoverPageGen = require('./coverpage.gen.js');
var TableOfContentsGen = require('./tableOfContents.gen.js');
module.exports = [
{
groupName : 'Editor',
icon : 'fa-pencil',
snippets : [
{
name : "Column Break",
icon : 'fa-columns',
gen : "```\n```\n\n"
},
{
name : "New Page",
icon : 'fa-file-text',
gen : "\\page\n\n"
},
{
name : "Vertical Spacing",
icon : 'fa-arrows-v',
gen : "<div style='margin-top:140px'></div>\n\n"
},
{
name : "Wide Block",
icon : 'fa-arrows-h',
gen : "<div class='wide'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n"
},
{
name : "Image",
icon : 'fa-image',
gen : [
"<img ",
" src='https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg' ",
" style='width:325px' />",
"Credit: Kyounghwan Kim"
].join('\n')
},
{
name : "Background Image",
icon : 'fa-tree',
gen : [
"<img ",
" src='http://i.imgur.com/hMna6G0.png' ",
" style='position:absolute; top:50px; right:30px; width:280px' />"
].join('\n')
},
{
name : "Page Number",
icon : 'fa-bookmark',
gen : "<div class='pageNumber'>1</div>\n<div class='footnote'>PART 1 | FANCINESS</div>\n\n"
},
{
name : "Auto-incrementing Page Number",
icon : 'fa-sort-numeric-asc',
gen : "<div class='pageNumber auto'></div>\n"
},
{
name : "Link to page",
icon : 'fa-link',
gen : "[Click here](#p3) to go to page 3\n"
},
{
name : "Table of Contents",
icon : 'fa-book',
gen : TableOfContentsGen
},
]
},
/************************* PHB ********************/
{
groupName : 'PHB',
icon : 'fa-book',
snippets : [
{
name : 'Spell',
icon : 'fa-magic',
gen : MagicGen.spell,
},
{
name : 'Spell List',
icon : 'fa-list',
gen : MagicGen.spellList,
},
{
name : 'Class Feature',
icon : 'fa-trophy',
gen : ClassFeatureGen,
},
{
name : 'Note',
icon : 'fa-sticky-note',
gen : function(){
return [
"> ##### Time to Drop Knowledge",
"> Use notes to point out some interesting information. ",
"> ",
"> **Tables and lists** both work within a note."
].join('\n');
},
},
{
name : 'Descriptive Text Box',
icon : 'fa-sticky-note-o',
gen : function(){
return [
"<div class='descriptive'>",
"##### Time to Drop Knowledge",
"Use notes to point out some interesting information. ",
"",
"**Tables and lists** both work within a note.",
"</div>"
].join('\n');
},
},
{
name : 'Monster Stat Block',
icon : 'fa-bug',
gen : MonsterBlockGen.half,
},
{
name : 'Wide Monster Stat Block',
icon : 'fa-paw',
gen : MonsterBlockGen.full,
},
{
name : 'Cover Page',
icon : 'fa-file-word-o',
gen : CoverPageGen,
},
]
},
/********************* TABLES *********************/
{
groupName : 'Tables',
icon : 'fa-table',
snippets : [
{
name : "Class Table",
icon : 'fa-table',
gen : ClassTableGen.full,
},
{
name : "Half Class Table",
icon : 'fa-list-alt',
gen : ClassTableGen.half,
},
{
name : 'Table',
icon : 'fa-th-list',
gen : function(){
return [
"##### Cookie Tastiness",
"| Tastiness | Cookie Type |",
"|:----:|:-------------|",
"| -5 | Raisin |",
"| 8th | Chocolate Chip |",
"| 11th | 2 or lower |",
"| 14th | 3 or lower |",
"| 17th | 4 or lower |\n\n",
].join('\n');
},
},
{
name : 'Wide Table',
icon : 'fa-list',
gen : function(){
return [
"<div class='wide'>",
"##### Cookie Tastiness",
"| Tastiness | Cookie Type |",
"|:----:|:-------------|",
"| -5 | Raisin |",
"| 8th | Chocolate Chip |",
"| 11th | 2 or lower |",
"| 14th | 3 or lower |",
"| 17th | 4 or lower |",
"</div>\n\n"
].join('\n');
},
},
{
name : 'Split Table',
icon : 'fa-th-large',
gen : function(){
return [
"<div style='column-count:2'>",
"| d10 | Damage Type |",
"|:---:|:------------|",
"| 1 | Acid |",
"| 2 | Cold |",
"| 3 | Fire |",
"| 4 | Force |",
"| 5 | Lightning |",
"",
"```",
"```",
"",
"| d10 | Damage Type |",
"|:---:|:------------|",
"| 6 | Necrotic |",
"| 7 | Poison |",
"| 8 | Psychic |",
"| 9 | Radiant |",
"| 10 | Thunder |",
"</div>\n\n",
].join('\n');
},
}
]
},
/**************** PRINT *************/
{
groupName : 'Print',
icon : 'fa-print',
snippets : [
{
name : "A4 PageSize",
icon : 'fa-file-o',
gen : ['<style>',
' .phb{',
' width : 210mm;',
' height : 296.8mm;',
' }',
'</style>'
].join('\n')
},
{
name : "Ink Friendly",
icon : 'fa-tint',
gen : ['<style>',
' .phb{ background : white;}',
' .phb img{ display : none;}',
' .phb hr+blockquote{background : white;}',
'</style>',
''
].join('\n')
},
]
},
]

Some files were not shown because too many files have changed in this diff Show More