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

Compare commits

..

23 Commits
v3 ... v2.8.0

Author SHA1 Message Date
Scott Tolksdorf
6cd6b3c1c6 Upping version 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
907dbe4416 Updating to user create-react-class 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
47429d804a Upping the node ver on circleci 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
1d54ab49e6 Updating superagent 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
f634c451ff Adding in the missing repository field 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
778c0ca0b1 Updating build scripts and libraries 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
f4985b68ca Updating build scripts and libraries 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
c8875cff94 lint 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
40f1d4c790 Updating navbar link to go to the new subreddit 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
7818b6db46 Adding mongo image to the circleci config 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
4163e95938 Updating contributing and issue guides, added new verify npm task 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
fed0536673 Moving babelrc into package 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
a4b92af351 Adding in test case runner 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
4584b83c60 adding a circleCI task 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
c57ca0b27f Adding circleCI and eslint 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
c04cd23213 Adding circleCI 2018-11-07 16:00:24 -05:00
Trevor Buckner
140f064786 Re-enable Box Shadows in PDF
Box shadows have been fixed in Chrome. No reason to hide them anymore.
2018-11-07 16:00:24 -05:00
Scott Tolksdorf
643f6f933d Fixing issue#413 class feature duping entire brew 2018-11-07 16:00:24 -05:00
Trevor Buckner
a1140f75d8 Fix border-image bug on PDFs
I think this might be caused by a new bug in Chrome or something since it seems to have popped up in GMBinder as well in the last couple weeks. See #683

Anyway, the error only occurs on the Class Table and Descriptive Text Box, which are also the only ones to use `border-image-repeat: round`. I changed it to `border-image-repeat: stretch` which solves the graphical issue and doesn't affect the appearance otherwise in this particular case.
2018-11-07 14:57:52 -05:00
Scott Tolksdorf
b68c6a4ad2 Added ability to hide render wanring notification 2017-04-22 12:09:18 -04:00
Scott Tolksdorf
6e0f042b42 Adding quick fix for PPR getting out of sync 2017-03-03 19:25:56 -05:00
Scott Tolksdorf
c47d492ed3 fix 2017-02-18 14:48:45 -05:00
Scott Tolksdorf
18852932e8 Added new delete brew option on user page 2017-02-18 14:46:14 -05:00
196 changed files with 12252 additions and 7891 deletions

33
.circleci/config.yml Normal file
View File

@@ -0,0 +1,33 @@
# 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

78
.eslintrc.js Normal file
View File

@@ -0,0 +1,78 @@
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 Normal file
View File

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

View File

@@ -1,5 +1,28 @@
Share link to issue brew: http://homebrewery.naturalcrit.com/share/XXXXXXX <!-- 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.
For *bug reports*, please provide as much *relevant* info as possible.
**Share Link** :
or
**Brew code to reproduce** : <details><summary>Click to expand</summary><code><pre>
PASTE BREW CODE HERE
</pre></code></details>

14
.gitignore vendored
View File

@@ -1,16 +1,10 @@
# 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,8 +1,14 @@
# changelog # changelog
### Saturday, 22/04/217 - v2.7.4
- Give ability to hide the render warning notification
The self-discovery aspect of the snippets isn't working out.
### 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.
@@ -30,8 +36,7 @@ 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,41 +1,38 @@
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 Admin = createClass({
const AdminSearch = require('./adminSearch/adminSearch.jsx'); getDefaultProps : function() {
const InvalidBrew = require('./invalidBrew/invalidBrew.jsx');
const Admin = React.createClass({
getDefaultProps: function() {
return { return {
url : '',
admin_key : '', admin_key : '',
homebrews : [],
}; };
}, },
renderNavbar : function(){
return <Nav.base>
<Nav.section>
<Nav.item icon='fa-magic' className='homebreweryLogo'>
Homebrewery Admin
</Nav.item>
</Nav.section>
</Nav.base>
},
render : function(){ render : function(){
return <div className='admin'> return (
{this.renderNavbar()} <div className='admin'>
<main className='content'>
<BrewLookup adminKey={this.props.admin_key} />
<AdminSearch adminKey={this.props.admin_key} />
<div className='dangerZone'>Danger Zone</div> <header>
<div className='container'>
<i className='fa fa-rocket' />
naturalcrit admin
</div>
</header>
<InvalidBrew adminKey={this.props.admin_key} /> <div className='container'>
</main>
</div> <HomebrewAdmin homebrews={this.props.homebrews} admin_key={this.props.admin_key} />
</div>
</div>
);
} }
}); });

View File

@@ -1,53 +1,39 @@
@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 'naturalcrit/styles/core.less'; @import 'font-awesome/css/font-awesome.css';
html,body, #reactRoot{
html,body, #reactContainer, .naturalCrit{
min-height : 100%; min-height : 100%;
} }
@sidebarWidth : 250px;
body{ body{
height : 100%; background-color : #eee;
font-family : 'Open Sans', sans-serif;
color : #4b5055;
font-weight : 100;
text-rendering : optimizeLegibility;
margin : 0; margin : 0;
padding : 0; padding : 0;
background-color : #ddd; height : 100%;
font-family : 'Open Sans', sans-serif;
font-weight : 100;
color : #4b5055;
text-rendering : optimizeLegibility;
} }
.admin {
nav { .admin{
header{
background-color : @red; background-color : @red;
.navItem{ font-size: 2em;
background-color : @red; padding : 20px 0px;
} color : white;
.homebreweryLogo{ margin-bottom: 30px;
font-family : CodeBold; i{
font-size : 12px; margin-right: 30px;
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

@@ -1,86 +0,0 @@
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

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

View File

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

View File

@@ -1,54 +0,0 @@
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

@@ -1,44 +0,0 @@
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,85 +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 request = require('superagent'); const request = require('superagent');
const BrewTable = require('../brewTable/brewTable.jsx'); const Moment = require('moment');
const BrewLookup = React.createClass({
getDefaultProps: function() { const BrewLookup = createClass({
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, error : null }); this.setState({ searching: true });
request.get(`/admin/lookup/${this.state.query}`) request.get(`/admin/lookup/${this.state.query}`)
.set('x-homebrew-admin', this.props.adminKey) .query({ admin_key: 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'>{brew.editId}</a></div> <div><a href={`/edit/${brew.editId}`} target='_blank'>/edit/{brew.editId}</a></div>
<div><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></div> <div><a href={`/share/${brew.shareId}`} target='_blank'>/share/{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>
*/
}, },
renderError : function(){ render : 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()}
{this.renderError()} </div>;
</div>
} }
}); });

View File

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

View File

@@ -0,0 +1,73 @@
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

@@ -0,0 +1,171 @@
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

@@ -0,0 +1,53 @@
.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

@@ -1,54 +0,0 @@
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

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

View File

@@ -1,63 +1,60 @@
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 OldBrewRenderer = require('depricated/brewRendererOld/brewRendererOld.jsx'); const Markdown = require('naturalcrit/markdown.js');
const Markdown = require('homebrewery/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx'); const ErrorBar = require('./errorBar/errorBar.jsx');
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx') //TODO: move to the brew renderer
const Store = require('homebrewery/brew.store.js'); const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50; const PPR_THRESHOLD = 50;
const BrewRenderer = React.createClass({ const BrewRenderer = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
brew : { text : '',
text : '',
style : ''
},
//TODO: maybe remove?
errors : [] errors : []
}; };
}, },
getInitialState: function() { getInitialState : function() {
const pages = this.props.brew.text.split('\\page'); const pages = this.props.text.split('\\page');
return { return {
viewablePageNumber: 0, viewablePageNumber : 0,
height : 0, height : 0,
isMounted : false, isMounted : false,
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD usePPR : true,
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD,
errors : []
}; };
}, },
height : 0, height : 0,
pageHeight : PAGE_HEIGHT, pageHeight : PAGE_HEIGHT,
lastRender : <div></div>, lastRender : <div></div>,
componentDidMount: function() { componentDidMount : function() {
this.updateSize(); this.updateSize();
window.addEventListener('resize', this.updateSize); window.addEventListener('resize', this.updateSize);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
window.removeEventListener('resize', this.updateSize); window.removeEventListener('resize', this.updateSize);
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps : function(nextProps) {
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight; if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
const pages = nextProps.brew.text.split('\\page'); const pages = nextProps.text.split('\\page');
this.setState({ this.setState({
pages : pages, pages : pages,
usePPR : pages.length >= PPR_THRESHOLD usePPR : pages.length >= PPR_THRESHOLD
}) });
}, },
updateSize : function() { updateSize : function() {
@@ -65,9 +62,8 @@ const BrewRenderer = React.createClass({
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight; if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
}, 1); }, 1);
this.setState({ this.setState({
height : this.refs.main.parentNode.clientHeight, height : this.refs.main.parentNode.clientHeight,
isMounted : true isMounted : true
}); });
}, },
@@ -81,10 +77,14 @@ const BrewRenderer = React.createClass({
shouldRender : function(pageText, index){ shouldRender : function(pageText, index){
if(!this.state.isMounted) return false; if(!this.state.isMounted) return false;
var viewIndex = this.state.viewablePageNumber; const viewIndex = this.state.viewablePageNumber;
if(index == viewIndex - 3) return true;
if(index == viewIndex - 2) return true;
if(index == viewIndex - 1) return true; if(index == viewIndex - 1) return true;
if(index == viewIndex) return true; if(index == viewIndex) return true;
if(index == viewIndex + 1) return true; if(index == viewIndex + 1) return true;
if(index == viewIndex + 2) return true;
if(index == viewIndex + 3) return true;
//Check for style tages //Check for style tages
if(pageText.indexOf('<style>') !== -1) return true; if(pageText.indexOf('<style>') !== -1) return true;
@@ -95,7 +95,7 @@ const BrewRenderer = React.createClass({
renderPageInfo : function(){ renderPageInfo : function(){
return <div className='pageInfo'> return <div className='pageInfo'>
{this.state.viewablePageNumber + 1} / {this.state.pages.length} {this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div> </div>;
}, },
renderPPRmsg : function(){ renderPPRmsg : function(){
@@ -103,17 +103,17 @@ const BrewRenderer = React.createClass({
return <div className='ppr_msg'> return <div className='ppr_msg'>
Partial Page Renderer enabled, because your brew is so large. May effect rendering. Partial Page Renderer enabled, because your brew is so large. May effect rendering.
</div> </div>;
}, },
renderDummyPage : function(index){ renderDummyPage : function(index){
return <div className='phb v2' id={`p${index + 1}`} key={index}> return <div className='phb' id={`p${index + 1}`} key={index}>
<i className='fa fa-spinner fa-spin' /> <i className='fa fa-spinner fa-spin' />
</div> </div>;
}, },
renderPage : function(pageText, index){ renderPage : function(pageText, index){
return <div className='phb v2' id={`p${index + 1}`} dangerouslySetInnerHTML={{__html:Markdown.render(pageText)}} key={index} /> return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
}, },
renderPages : function(){ renderPages : function(){
@@ -121,7 +121,7 @@ const BrewRenderer = React.createClass({
return _.map(this.state.pages, (page, index)=>{ return _.map(this.state.pages, (page, index)=>{
if(this.shouldRender(page, index)){ if(this.shouldRender(page, index)){
return this.renderPage(page, index); return this.renderPage(page, index);
}else{ } else {
return this.renderDummyPage(index); return this.renderDummyPage(index);
} }
}); });
@@ -133,32 +133,21 @@ const BrewRenderer = React.createClass({
return this.lastRender; return this.lastRender;
}, },
//TODO: This is pretty bad
renderStyle : function(){
return <style>{this.props.brew.style.replace(/;/g, ' !important;')}</style>
},
render : function(){ render : function(){
if(this.props.brew.version == 1) return <OldBrewRenderer value={this.props.brew.text} />;
return <div className='brewRenderer' return <div className='brewRenderer'
onScroll={this.handleScroll} onScroll={this.handleScroll}
ref='main' ref='main'
style={{height : this.state.height}}> style={{ height: this.state.height }}>
<ErrorBar errors={this.props.errors} /> <ErrorBar errors={this.props.errors} />
<RenderWarnings /> <RenderWarnings />
{this.renderStyle()}
<div className='pages' ref='pages'> <div className='pages' ref='pages'>
{this.renderPages()} {this.renderPages()}
</div> </div>
{this.renderPageInfo()} {this.renderPageInfo()}
{this.renderPPRmsg()} {this.renderPPRmsg()}
</div> </div>;
} }
}); });

View File

@@ -1,5 +1,5 @@
@import (less) './shared/homebrewery/phb_style/phb.less'; @import (less) './client/homebrew/phbStyle/phb.style.less';
.pane{ .pane{
position : relative; position : relative;
} }

View File

@@ -1,15 +1,16 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
const cx = require('classnames');
var ErrorBar = React.createClass({ const ErrorBar = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
errors : [] errors : []
}; };
}, },
hasOpenError : false, hasOpenError : false,
hasCloseError : false, hasCloseError : false,
hasMatchError : false, hasMatchError : false,
@@ -19,20 +20,20 @@ var ErrorBar = React.createClass({
this.hasMatchError = false; this.hasMatchError = false;
var errors = _.map(this.props.errors, (err, idx) => { const errors = _.map(this.props.errors, (err, idx)=>{
if(err.id == 'OPEN') this.hasOpenError = true; if(err.id == 'OPEN') this.hasOpenError = true;
if(err.id == 'CLOSE') this.hasCloseError = true; if(err.id == 'CLOSE') this.hasCloseError = true;
if(err.id == 'MISMATCH') this.hasMatchError = true; if(err.id == 'MISMATCH') this.hasMatchError = true;
return <li key={idx}> return <li key={idx}>
Line {err.line} : {err.text}, '{err.type}' tag Line {err.line} : {err.text}, '{err.type}' tag
</li> </li>;
}); });
return <ul>{errors}</ul> return <ul>{errors}</ul>;
}, },
renderProtip : function(){ renderProtip : function(){
var msg = []; const msg = [];
if(this.hasOpenError){ if(this.hasOpenError){
msg.push(<div> msg.push(<div>
An unmatched opening tag means there's an opened tag that isn't closed, you need to close a tag, like this {'</div>'}. Make sure to match types! An unmatched opening tag means there's an opened tag that isn't closed, you need to close a tag, like this {'</div>'}. Make sure to match types!
@@ -53,7 +54,7 @@ var ErrorBar = React.createClass({
return <div className='protips'> return <div className='protips'>
<h4>Protips!</h4> <h4>Protips!</h4>
{msg} {msg}
</div> </div>;
}, },
render : function(){ render : function(){
@@ -66,7 +67,7 @@ var ErrorBar = React.createClass({
{this.renderErrors()} {this.renderErrors()}
<hr /> <hr />
{this.renderProtip()} {this.renderProtip()}
</div> </div>;
} }
}); });

View File

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

View File

@@ -1,21 +1,14 @@
.brewEditor{ .editor{
position : relative; position : relative;
width : 100%; width : 100%;
.codeEditor{ .codeEditor{
height : 100%; height : 100%;
.pageLine{ .pageLine{
background-color : fade(#333, 15%); background-color : fade(#333, 15%);
border-bottom : #333 solid 1px; border-bottom : #333 solid 1px;
} }
.block{
color : purple;
//font-style: italic;
}
.columnSplit{
font-style : italic;
color : grey;
}
} }
.brewJump{ .brewJump{
@@ -32,4 +25,5 @@
justify-content:center; justify-content:center;
.tooltipLeft("Jump to brew page"); .tooltipLeft("Jump to brew page");
} }
} }

View File

@@ -1,21 +1,22 @@
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 = React.createClass({ const MetadataEditor = 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 : ()=>{}
}; };
@@ -24,12 +25,12 @@ const MetadataEditor = React.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);
@@ -41,10 +42,10 @@ const MetadataEditor = React.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 = '/';
@@ -67,22 +68,21 @@ const MetadataEditor = React.createClass({
<input <input
type='checkbox' type='checkbox'
checked={_.includes(this.props.metadata.systems, val)} checked={_.includes(this.props.metadata.systems, val)}
onChange={this.handleSystem.bind(null, val)} /> onChange={()=>this.handleSystem(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.bind(null, false)}> return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
<i className='fa fa-ban' /> unpublish <i className='fa fa-ban' /> unpublish
</button> </button>;
}else{ } else {
return <button className='publish' onClick={this.handlePublish.bind(null, true)}> return <button className='publish' onClick={()=>this.handlePublish(true)}>
<i className='fa fa-globe' /> publish <i className='fa fa-globe' /> publish
</button> </button>;
} }
}, },
@@ -96,7 +96,7 @@ const MetadataEditor = React.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 = React.createClass({
<div className='value'> <div className='value'>
{text} {text}
</div> </div>
</div> </div>;
}, },
renderShareToReddit : function(){ renderShareToReddit : function(){
@@ -124,7 +124,7 @@ const MetadataEditor = React.createClass({
</button> </button>
</a> </a>
</div> </div>
</div> </div>;
}, },
render : function(){ render : function(){
@@ -133,19 +133,20 @@ const MetadataEditor = React.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.bind(null, 'title')} /> onChange={()=>this.handleFieldChange('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.bind(null, 'description')} /> onChange={()=>this.handleFieldChange('description')} />
</div> </div>
<div className='field thumbnail'> {/*}
<label>thumbnail</label> <div className='field tags'>
<input type='text' className='value' <label>tags</label>
value={this.props.metadata.thumbnail} <textarea value={this.props.metadata.tags}
onChange={this.handleFieldChange.bind(null, 'thumbnail')} /> onChange={()=>this.handleFieldChange('tags')} />
</div> </div>
*/}
<div className='field systems'> <div className='field systems'>
<label>systems</label> <label>systems</label>
@@ -168,7 +169,7 @@ const MetadataEditor = React.createClass({
{this.renderDelete()} {this.renderDelete()}
</div> </div>;
} }
}); });

View File

@@ -1,11 +1,11 @@
.metadataEditor{ .metadataEditor{
position : absolute; position : absolute;
z-index : 10000;
box-sizing : border-box; box-sizing : border-box;
width : 100%; width : 100%;
padding : 25px; padding : 25px;
// background-color : #999; background-color : #999;
background-color: white;
.field{ .field{
display : flex; display : flex;
width : 100%; width : 100%;
@@ -76,7 +76,4 @@
font-size: 0.8em; font-size: 0.8em;
line-height : 1.5em; line-height : 1.5em;
} }
.thumbnail.field{
}
} }

View File

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

View File

@@ -0,0 +1,73 @@
.snippetBar{
@height : 25px;
position : relative;
height : @height;
background-color : #ddd;
.toggleMeta{
position : absolute;
top : 0px;
right : 0px;
height : @height;
width : @height;
cursor : pointer;
line-height : @height;
text-align : center;
.tooltipLeft("Edit Brew Metadata");
&:hover, &.selected{
background-color : #999;
}
}
.snippetGroup{
display : inline-block;
height : @height;
padding : 0px 5px;
cursor : pointer;
font-size : 0.6em;
font-weight : 800;
line-height : @height;
text-transform : uppercase;
border-right : 1px solid black;
i{
vertical-align : middle;
margin-right : 3px;
font-size : 1.2em;
}
&:hover, &.selected{
background-color : #999;
}
.text{
line-height : @height;
.groupName{
font-size : 10px;
}
}
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown{
position : absolute;
top : 100%;
visibility : hidden;
z-index : 1000;
margin-left : -5px;
padding : 0px;
background-color : #ddd;
.snippet{
.animate(background-color);
padding : 5px;
cursor : pointer;
font-size : 10px;
i{
margin-right : 8px;
font-size : 13px;
}
&:hover{
background-color : #999;
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,114 @@
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

@@ -0,0 +1,117 @@
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

@@ -0,0 +1,43 @@
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

@@ -0,0 +1,91 @@
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

@@ -0,0 +1,196 @@
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

@@ -0,0 +1,268 @@
/* 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,38 +1,38 @@
const _ = require('lodash'); const _ = require('lodash');
const getTOC = (pages) => { const getTOC = (pages)=>{
const add1 = (title, page)=>{ const add1 = (title, page)=>{
res.push({ res.push({
title : title, title : title,
page : page + 1, page : page + 1,
children : [] children : []
}); });
} };
const add2 = (title, page)=>{ const add2 = (title, page)=>{
if(!_.last(res)) add1('', page); if(!_.last(res)) add1('', page);
_.last(res).children.push({ _.last(res).children.push({
title : title, title : title,
page : page + 1, page : page + 1,
children : [] children : []
}); });
} };
const add3 = (title, page)=>{ const add3 = (title, page)=>{
if(!_.last(res)) add1('', page); if(!_.last(res)) add1('', page);
if(!_.last(_.last(res).children)) add2('', page); if(!_.last(_.last(res).children)) add2('', page);
_.last(_.last(res).children).children.push({ _.last(_.last(res).children).children.push({
title : title, title : title,
page : page + 1, page : page + 1,
children : [] children : []
}); });
} };
let res = []; const res = [];
_.each(pages, (page, pageNum)=>{ _.each(pages, (page, pageNum)=>{
const lines = page.split('\n'); const lines = page.split('\n');
_.each(lines, (line) => { _.each(lines, (line)=>{
if(_.startsWith(line, '# ')){ if(_.startsWith(line, '# ')){
const title = line.replace('# ', ''); const title = line.replace('# ', '');
add1(title, pageNum) add1(title, pageNum);
} }
if(_.startsWith(line, '## ')){ if(_.startsWith(line, '## ')){
const title = line.replace('## ', ''); const title = line.replace('## ', '');
@@ -42,21 +42,21 @@ const getTOC = (pages) => {
const title = line.replace('### ', ''); const title = line.replace('### ', '');
add3(title, pageNum); add3(title, pageNum);
} }
}) });
}); });
return res; return res;
} };
module.exports = function(brew){ module.exports = function(brew){
const pages = brew.split('\\page');
const TOC = getTOC(pages); const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{ const markdown = _.reduce(TOC, (r, g1, idx1)=>{
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`) r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
if(g1.children.length){ if(g1.children.length){
_.each(g1.children, (g2, idx2) => { _.each(g1.children, (g2, idx2)=>{
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`); r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
if(g2.children.length){ if(g2.children.length){
_.each(g2.children, (g3, idx3) => { _.each(g2.children, (g3, idx3)=>{
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`); r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
}); });
} }
@@ -69,4 +69,4 @@ module.exports = function(brew){
##### Table Of Contents ##### Table Of Contents
${markdown} ${markdown}
</div>\n`; </div>\n`;
} };

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
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

@@ -1,76 +0,0 @@
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,79 +0,0 @@
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_CODE' || actionType == 'UPDATE_META' || actionType == 'UPDATE_BREW_STYLE'){
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 />
Back up your brew in a text file, just in case.
<br /><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,15 +1,16 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
var Nav = require('naturalcrit/nav/nav.jsx'); const cx = require('classnames');
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(){}
}; };
}, },
@@ -22,10 +23,10 @@ var EditTitle = React.createClass({
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>;
}, },
}); });

View File

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

View File

@@ -1,10 +1,36 @@
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 = React.createClass({ const Navbar = createClass({
getInitialState : function() {
return {
//showNonChromeWarning : false,
ver : '0.0.0'
};
},
componentDidMount : function() {
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
this.setState({
//showNonChromeWarning : !isChrome,
ver : window.version
});
},
/*
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(){ render : function(){
return <Nav.base> return <Nav.base>
<Nav.section> <Nav.section>
@@ -12,10 +38,12 @@ const Navbar = React.createClass({
<Nav.item href='/' className='homebrewLogo'> <Nav.item href='/' className='homebrewLogo'>
<div>The Homebrewery</div> <div>The Homebrewery</div>
</Nav.item> </Nav.item>
<Nav.item>{`v${Store.getVersion()}`}</Nav.item> <Nav.item>{`v${this.state.ver}`}</Nav.item>
{/*this.renderChromeWarning()*/}
</Nav.section> </Nav.section>
{this.props.children} {this.props.children}
</Nav.base> </Nav.base>;
} }
}); });

View File

@@ -13,6 +13,32 @@
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;
@@ -99,34 +125,4 @@
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 : 170px;
padding : 8px;
background-color : #333;
a{
color : @teal;
}
}
}
}
} }

View File

@@ -1,10 +0,0 @@
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,14 @@
var React = require('react'); const React = require('react');
var Nav = require('naturalcrit/nav/nav.jsx'); const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){ module.exports = function(props){
return <Nav.item return <Nav.item
{...props}
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,8 +1,9 @@
var React = require('react'); const React = require('react');
var Nav = require('naturalcrit/nav/nav.jsx'); const createClass = require('create-react-class');
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,49 +1,45 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
var Moment = require('moment'); const cx = require('classnames');
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';
//DEPRICATED const BaseItem = createClass({
getDefaultProps : function() {
var BaseItem = React.createClass({
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() {
console.log('Recent nav item is depricated'); let brews = JSON.parse(localStorage.getItem(this.props.storageKey) || '[]');
var 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);
@@ -56,29 +52,29 @@ var BaseItem = React.createClass({
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>;
}, },
}); });
@@ -86,11 +82,11 @@ var BaseItem = React.createClass({
module.exports = { module.exports = {
viewed : React.createClass({ viewed : createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
brew : { brew : {
title : '', title : '',
shareId : '' shareId : ''
} }
}; };
@@ -98,19 +94,19 @@ module.exports = {
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 : ''
} }
}; };
@@ -118,39 +114,39 @@ module.exports = {
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;
}); });
@@ -168,18 +164,18 @@ module.exports = {
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>;
}); });
}; };
@@ -188,17 +184,17 @@ module.exports = {
{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

@@ -0,0 +1,52 @@
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

@@ -1,37 +0,0 @@
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,63 +1,231 @@
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 Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
const Items = require('../../navbar/navitems.js');
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 RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
const Store = require('homebrewery/brew.store.js'); const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Actions = require('homebrewery/brew.actions.js'); const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Markdown = require('naturalcrit/markdown.js');
const SAVE_TIMEOUT = 3000;
const EditPage = React.createClass({ const EditPage = createClass({
componentDidMount: function(){ getDefaultProps : function() {
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount: function() {
document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : Utils.controlKeys({
s : Actions.save,
p : Actions.print
}),
render : function(){
return <div className='editPage page'>
<SmartNav />
<div className='content'>
<BrewInterface />
</div>
</div>
}
});
const SmartNav = Store.createSmartComponent(React.createClass({
getDefaultProps: function() {
return { return {
brew : {} brew : {
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
}
}; };
}, },
render : function(){
getInitialState : function() {
return {
brew : this.props.brew,
isSaving : false,
isPending : false,
errors : null,
htmlErrors : Markdown.validate(this.props.brew.text),
lastUpdated : this.props.brew.updatedAt
};
},
savedBrew : null,
componentDidMount : function(){
this.trySave();
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){
return 'You have unsaved changes!';
}
};
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> return <Navbar>
<Nav.section> <Nav.section>
<Items.BrewTitle /> <Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section> </Nav.section>
<Nav.section> <Nav.section>
<Items.ContinousSave /> {this.renderSaveButton()}
<Items.Issue /> {/*<RecentlyEdited brew={this.props.brew} />*/}
<Nav.item newTab={true} href={'/share/' + Store.getBrew().shareId} color='teal' icon='fa-share-alt'> <ReportIssue />
<Nav.item newTab={true} href={`/share/${this.props.brew.shareId}`} color='teal' icon='fa-share-alt'>
Share Share
</Nav.item> </Nav.item>
<Items.Print /> <PrintLink shareId={this.props.brew.shareId} />
<Items.Account /> <Account />
</Nav.section> </Nav.section>
</Navbar> </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>;
} }
}), ()=>{
return {brew : Store.getBrew()}
}); });
module.exports = EditPage; module.exports = EditPage;

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
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 Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
@@ -10,24 +12,52 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx');
const AccountNavItem = require('../../navbar/account.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 BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Actions = require('homebrewery/brew.actions.js');
//
const HomePage = React.createClass({ const HomePage = createClass({
handleSave : function(){ getDefaultProps : function() {
Actions.saveNew(); return {
welcomeText : '',
ver : '0.0.0'
};
},
getInitialState : function() {
return {
text : this.props.welcomeText
};
},
handleSave : function(){
request.post('/api')
.send({
text : this.state.text
})
.end((err, res)=>{
if(err) return;
const brew = res.body;
window.location = `/edit/${brew.editId}`;
});
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleTextChange : function(text){
this.setState({
text : text
});
}, },
renderNavbar : function(){ renderNavbar : function(){
return <Navbar> return <Navbar ver={this.props.ver}>
<Nav.section> <Nav.section>
<PatreonNavItem collaspe={true} /> <PatreonNavItem />
<IssueNavItem collaspe={true} /> <IssueNavItem />
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-star' collaspe={true}> <Nav.item newTab={true} href='/changelog' color='purple' icon='fa-file-text-o'>
What's new Changelog
</Nav.item> </Nav.item>
<RecentNavItem.both /> <RecentNavItem.both />
<AccountNavItem /> <AccountNavItem />
@@ -37,26 +67,28 @@ const HomePage = React.createClass({
</Nav.item> </Nav.item>
*/} */}
</Nav.section> </Nav.section>
</Navbar> </Navbar>;
}, },
render : function(){ render : function(){
return <div className='homePage page'> return <div className='homePage page'>
{this.renderNavbar()} {this.renderNavbar()}
<div className='content'> <div className='content'>
<BrewInterface /> <SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<BrewRenderer text={this.state.text} />
</SplitPane>
</div> </div>
<div className={cx('floatingSaveButton', { <div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
//show : Store.getBrewText() !== this.props.welcomeText
})} onClick={this.handleSave}>
Save current <i className='fa fa-save' /> Save current <i className='fa fa-save' />
</div> </div>
<a href='/new' className='floatingNewButton'> <a href='/new' className='floatingNewButton'>
Create your own <i className='fa fa-magic' /> Create your own <i className='fa fa-magic' />
</a> </a>
</div> </div>;
} }
}); });

View File

@@ -1,61 +1,161 @@
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 request = require('superagent');
const Markdown = require('naturalcrit/markdown.js');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
const Items = require('../../navbar/navitems.js'); const AccountNavItem = require('../../navbar/account.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const Store = require('homebrewery/brew.store.js'); const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Actions = require('homebrewery/brew.actions.js'); const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Utils = require('homebrewery/utils.js');
const KEY = 'homebrewery-new'; const KEY = 'homebrewery-new';
const NewPage = React.createClass({ const NewPage = createClass({
componentDidMount: function() { getInitialState : function() {
try{ return {
const storedBrew = JSON.parse(localStorage.getItem(KEY)); metadata : {
if(storedBrew && storedBrew.text) Actions.setBrew(storedBrew); title : '',
}catch(e){} description : '',
Store.updateEmitter.on('change', this.saveToLocal); tags : '',
published : false,
authors : [],
systems : []
},
text : '',
isSaving : false,
errors : []
};
},
componentDidMount : function() {
const storage = localStorage.getItem(KEY);
if(storage){
this.setState({
text : storage
});
}
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
Store.updateEmitter.removeListener('change', this.saveToLocal);
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
saveToLocal : function(){ handleControlKeys : function(e){
localStorage.setItem(KEY, JSON.stringify(Store.getBrew())); 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) this.print();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
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>;
}, },
handleControlKeys : Utils.controlKeys({
s : Actions.saveNew,
p : Actions.localPrint
}),
render : function(){ render : function(){
return <div className='newPage page'> return <div className='newPage page'>
<Navbar> {this.renderNavbar()}
<Nav.section>
<Items.BrewTitle />
</Nav.section>
<Nav.section>
<Items.StaticSave />
<Nav.item color='purple' icon='fa-file-pdf-o' onClick={Actions.localPrint}>
get PDF
</Nav.item>
<Items.Issue collaspe={true} />
<Items.Account />
</Nav.section>
</Navbar>
<div className='content'> <div className='content'>
<BrewInterface /> <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>
</div> </div>;
} }
}); });

View File

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

View File

@@ -1,66 +1,47 @@
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('homebrewery/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
const Headtags = require('vitreum/headtags'); const PrintPage = createClass({
getDefaultProps : function() {
const PrintPage = React.createClass({
getDefaultProps: function() {
return { return {
query : {}, query : {},
brew : { brew : {
text : '', text : '',
style : ''
} }
}; };
}, },
getInitialState: function() {
getInitialState : function() {
return { return {
brew: this.props.brew brewText : this.props.brew.text
}; };
}, },
componentDidMount: function() {
componentDidMount : function() {
if(this.props.query.local){ if(this.props.query.local){
try{ this.setState({ brewText: localStorage.getItem(this.props.query.local) });
this.setState({
brew : JSON.parse(
localStorage.getItem(this.props.query.local)
)
});
}catch(e){}
} }
if(this.props.query.dialog) window.print(); if(this.props.query.dialog) window.print();
}, },
//TODO: Print page shouldn't replicate functionality in brew renderer
renderStyle : function(){
if(!this.state.brew.style) return;
return <style>{this.state.brew.style.replace(/;/g, ' !important;')}</style>
},
renderPages : function(){ renderPages : function(){
return _.map(this.state.brew.text.split('\\page'), (page, index) => { return _.map(this.state.brewText.split('\\page'), (page, index)=>{
return <div return <div
className='phb v2' className='phb'
id={`p${index + 1}`} id={`p${index + 1}`}
dangerouslySetInnerHTML={{__html:Markdown.render(page)}} dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
key={index} />; key={index} />;
}); });
}, },
renderPrintInstructions : function(){
return <div className='printInstructions'>
Hey, I'm really cool instructions!!!!!
</div>
},
render : function(){ render : function(){
return <div className='printPage'> return <div>
<Headtags.title>{this.state.brew.title}</Headtags.title>
{this.renderPrintInstructions()}
{this.renderStyle()}
{this.renderPages()} {this.renderPages()}
</div> </div>;
} }
}); });

View File

@@ -1,17 +1,3 @@
.printPage{ .printPage{
position : relative;
@media print{
.printInstructions{
display : none;
}
}
.printInstructions{
position : absolute;
top : 0px;
right : 0px;
z-index : 100000;
padding : 30px;
background-color : @blue;
}
} }

View File

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

View File

@@ -1,13 +1,15 @@
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 = React.createClass({ const BrewItem = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
brew : { brew : {
title : '', title : '',
description : '', description : '',
authors : [] authors : []
@@ -15,12 +17,30 @@ const BrewItem = React.createClass({
}; };
}, },
renderEditLink: function(){ deleteBrew : function(){
if(!confirm('are you sure you want to delete this brew?')) return;
if(!confirm('are you REALLY sure? You will not be able to recover it')) return;
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(){
@@ -47,8 +67,9 @@ const BrewItem = React.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,4 +1,5 @@
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,19 +10,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 = React.createClass({ const UserPage = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
username : '', username : '',
brews : [] brews : []
}; };
}, },
@@ -30,14 +31,14 @@ const UserPage = React.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');
}); });
}, },
@@ -52,13 +53,12 @@ const UserPage = React.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 userPage={this.props.username} /> <Account />
</Nav.section> </Nav.section>
</Navbar> </Navbar>
@@ -69,7 +69,7 @@ const UserPage = React.createClass({
{this.renderPrivateBrews(brews.private)} {this.renderPrivateBrews(brews.private)}
</div> </div>
</div> </div>
</div> </div>;
} }
}); });

View File

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

View File

Before

Width:  |  Height:  |  Size: 530 B

After

Width:  |  Height:  |  Size: 530 B

View File

@@ -0,0 +1,31 @@
.phb{
//Double hr for full width elements
hr+hr+blockquote{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
hr+table{
margin-top : -5px;
margin-bottom : 50px;
padding-top : 10px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 37px 17px;
border-image-repeat : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
}
h5+hr+table{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
}

View File

@@ -1,54 +1,47 @@
@media print {
.phb.v1{ @import (less) 'shared/naturalcrit/styles/reset.less';
.descriptive, blockquote{ @import (less) './client/homebrew/phbStyle/phb.fonts.css';
box-shadow : none; @import (less) './client/homebrew/phbStyle/phb.assets.less';
} @import (less) './client/homebrew/phbStyle/phb.depricated.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
}
*{
-webkit-print-color-adjust : exact;
}
.useSansSerif(){
font-family : ScalySans;
em{
font-family : ScalySans;
font-style : italic;
}
strong{
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
} }
} }
.useColumns(@multiplier : 1){
.phb.v1{ column-count : 2;
@import (less) './phb.fonts.v1.css'; column-fill : auto;
@import (less) './phb.assets.v1.less'; column-gap : 1cm;
column-width : 8cm * @multiplier;
-webkit-column-count : 2;
//Colors -moz-column-count : 2;
@background : #EEE5CE; -webkit-column-width : 8cm * @multiplier;
@noteGreen : #e0e5c1; -moz-column-width : 8cm * @multiplier;
@headerUnderline : #c9ad6a; -webkit-column-gap : 1cm;
@horizontalRule : #9c2b1b; -moz-column-gap : 1cm;
@headerText : #58180D; }
@monsterStatBackground : #FDF1DC; .phb{
@page { margin: 0; }
.useSansSerif(){
font-family : ScalySans;
em{
font-family : ScalySans;
font-style : italic;
}
strong{
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
}
}
.useColumns(@multiplier : 1){
column-count : 2;
column-fill : auto;
column-gap : 1cm;
column-width : 8cm * @multiplier;
-webkit-column-count : 2;
-moz-column-count : 2;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
-webkit-column-gap : 1cm;
-moz-column-gap : 1cm;
}
& *{
-webkit-print-color-adjust : exact;
}
.useColumns(); .useColumns();
counter-increment : phb-page-numbers; counter-increment : phb-page-numbers;
position : relative; position : relative;
@@ -66,7 +59,6 @@
text-rendering : optimizeLegibility; text-rendering : optimizeLegibility;
page-break-before : always; page-break-before : always;
page-break-after : always; page-break-after : always;
//***************************** //*****************************
// * BASE // * BASE
// *****************************/ // *****************************/
@@ -365,157 +357,111 @@
-webkit-column-break-inside : avoid; -webkit-column-break-inside : avoid;
column-break-inside : avoid; column-break-inside : avoid;
} }
}
//***************************** //*****************************
// * SPELL LIST // * SPELL LIST
// *****************************/ // *****************************/
.spellList{ .phb .spellList{
.useSansSerif(); .useSansSerif();
column-count : 4; column-count : 4;
column-span : all; column-span : all;
-webkit-column-span : all; -webkit-column-span : all;
-moz-column-span : all; -moz-column-span : all;
ul+h5{ ul+h5{
margin-top : 15px; margin-top : 15px;
}
p, ul{
font-size : 0.352cm;
line-height : 1.3em;
}
ul{
margin-bottom : 0.5em;
padding-left : 1em;
text-indent : -1em;
list-style-type : none;
-webkit-column-break-inside : auto;
column-break-inside : auto;
}
} }
//***************************** p, ul{
// * PRINT font-size : 0.352cm;
// *****************************/ line-height : 1.3em;
&.print{
blockquote{
box-shadow : none;
}
} }
ul{
//***************************** margin-bottom : 0.5em;
// * WIDE padding-left : 1em;
// *****************************/ text-indent : -1em;
.wide{ list-style-type : none;
column-span : all; -webkit-column-break-inside : auto;
-webkit-column-span : all; column-break-inside : auto;
-moz-column-span : all; }
} }
//***************************** //*****************************
// * CLASS TABLE // * WIDE
// *****************************/ // *****************************/
.classTable{ .phb .wide{
margin-top : 25px; column-span : all;
margin-bottom : 40px; -webkit-column-span : all;
border-collapse : separate; -moz-column-span : all;
background-color : white; }
border : initial; //*****************************
border-style : solid; // * CLASS TABLE
border-image-outset : 25px 17px; // *****************************/
border-image-repeat : round; .phb .classTable{
border-image-slice : 150 200 150 200; margin-top : 25px;
border-image-source : @frameBorderImage; margin-bottom : 40px;
border-image-width : 47px; border-collapse : separate;
h5{ background-color : white;
margin-bottom : 10px; border : initial;
} border-style : solid;
} border-image-outset : 25px 17px;
//***************************** border-image-repeat : stretch;
// * CLASS TABLE border-image-slice : 150 200 150 200;
// *****************************/ border-image-source : @frameBorderImage;
.descriptive{ border-image-width : 47px;
display : block-inline; h5{
margin-bottom : 1em; margin-bottom : 10px;
background-color : #faf7ea; }
font-family : ScalySans; }
border-style : solid; //************************************
border-width : 7px; // * DESCRIPTIVE TEXT BOX
border-image : @descriptiveBoxImage 12 round; // ************************************/
border-image-outset : 4px; .phb .descriptive{
box-shadow : 0px 0px 6px #faf7ea; display : block-inline;
p{ margin-bottom : 1em;
display : block; background-color : #faf7ea;
padding-bottom : 0px; font-family : ScalySans;
line-height : 1.5em; border-style : solid;
} border-width : 7px;
p + p { border-image : @descriptiveBoxImage 12 stretch;
padding-top : .8em; border-image-outset : 4px;
} box-shadow : 0px 0px 6px #faf7ea;
em { p{
font-family : ScalySans; display : block;
font-style : italic; padding-bottom : 0px;
} line-height : 1.5em;
strong { }
font-family : ScalySans; p + p {
font-weight : 800; padding-top : .8em;
letter-spacing : -0.02em; }
} em {
} font-family : ScalySans;
pre+.descriptive{ font-style : italic;
margin-top : 8px; }
} strong {
//***************************** font-family : ScalySans;
// * TABLE OF CONTENTS font-weight : 800;
// *****************************/ letter-spacing : -0.02em;
.toc{ }
-webkit-column-break-inside : avoid; }
column-break-inside : avoid; .phb pre+.descriptive{
a{ margin-top : 8px;
color : black; }
text-decoration : none; //*****************************
&:hover{ // * TABLE OF CONTENTS
text-decoration : underline; // *****************************/
} .phb .toc{
} -webkit-column-break-inside : avoid;
ul{ column-break-inside : avoid;
padding-left : 0; a{
list-style-type : none; color : black;
} text-decoration : none;
&>ul>li{ &:hover{
margin-bottom : 10px; text-decoration : underline;
} }
} }
ul{
padding-left : 0;
list-style-type : none;
//***************************** }
// * Old Stuff &>ul>li{
// *****************************/ margin-bottom : 10px;
//Double hr for full width elements
hr+hr+blockquote{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
hr+table{
margin-top : -5px;
margin-bottom : 50px;
padding-top : 10px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 37px 17px;
border-image-repeat : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
}
h5+hr+table{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

View File

@@ -6,7 +6,6 @@ module.exports = function(vitreum){
<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>
@@ -16,7 +15,7 @@ module.exports = function(vitreum){
${vitreum.js} ${vitreum.js}
</html> </html>
`; `;
} };

View File

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

View File

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

View File

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

45
contributing.md Normal file
View File

@@ -0,0 +1,45 @@
# 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 Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,55 +1,68 @@
{ {
"name": "homebrewery", "name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown", "description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.0.0", "version": "2.8.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",
"populate": "node scripts/populate.js", "lint": "eslint --fix **/*.{js,jsx}",
"prod": "set NODE_ENV=production&& npm run build", "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",
"prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run build", "postinstall": "npm run build",
"start": "node server.js", "start": "node server.js"
"snippet": "nodemon scripts/snippet.test.js",
"todo": "./node_modules/.bin/fixme -i node_modules/** -i build/**",
"test": "mocha tests",
"test:dev": "nodemon -x mocha tests || exit 0",
"test:markdown": "nodemon -x mocha tests/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",
"egads": "^1.0.1", "create-react-class": "^15.6.3",
"express": "^4.13.3", "express": "^4.13.3",
"jwt-simple": "^0.5.1", "jwt-simple": "^0.5.1",
"lodash": "^4.17.3", "lodash": "^4.11.2",
"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": "^2.1.2", "pico-flux": "^1.1.0",
"pico-router": "^1.1.0", "pico-router": "^1.1.0",
"react": "^15.4.1", "react": "^16.3.1",
"react-dom": "^15.4.1", "react-dom": "^16.3.1",
"shortid": "^2.2.4", "shortid": "^2.2.4",
"striptags": "^2.1.1", "striptags": "^2.1.1",
"superagent": "^1.6.1", "superagent": "^3.8.2",
"vitreum": "^4.0.12" "vitreum": "^4.10.1"
}, },
"devDependencies": { "devDependencies": {
"app-module-path": "^2.1.0", "eslint": "^4.19.1",
"chai": "^3.5.0", "eslint-plugin-react": "^7.7.0",
"chai-as-promised": "^6.0.0", "pico-check": "^1.0.3"
"chai-subset": "^1.4.0",
"fixme": "^0.4.3",
"mocha": "^3.2.0",
"supertest": "^2.0.1",
"supertest-as-promised": "^4.0.2"
} }
} }

1
phb.standalone.css Normal file

File diff suppressed because one or more lines are too long

View File

@@ -9,13 +9,12 @@ const asset = require('vitreum/steps/assets.js');
const Proj = require('./project.json'); const Proj = require('./project.json');
Promise.resolve() clean()
.then(()=>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((deps)=>less('homebrew', ['./shared'], deps)) .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((deps)=>less('admin', ['./shared'], deps))
.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

@@ -10,12 +10,13 @@ const livereload = require('vitreum/steps/livereload.js');
const Proj = require('./project.json'); const Proj = require('./project.json');
Promise.resolve() Promise.resolve()
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared')) .then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] }))
.then((deps)=>less('homebrew', './shared', deps)) .then((deps)=>less('homebrew', { shared: ['./shared'] }, deps))
.then(()=>jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared')) .then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] }))
.then((deps)=>less('admin', './shared', deps)) .then((deps)=>less('admin', { shared: ['./shared'] }, deps))
.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,8 +0,0 @@
require('fixme')({
path: process.cwd(),
ignored_directories: ['node_modules/**', '.git/**', 'build/**'],
file_patterns: ['**/*.js', '**/*.jsx', '**/*.less'],
file_encoding: 'utf8',
line_length_limit: 200
});

10
scripts/phb.js Normal file
View File

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

View File

@@ -1,22 +0,0 @@
//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

@@ -1,15 +1,16 @@
{ {
"assets": ["*.png","*.otf", "*.ico", "*.jpg"], "assets": ["*.png","*.otf", "*.ico"],
"shared":["./shared"], "shared":[
],
"libs" : [ "libs" : [
"react", "react",
"react-dom", "react-dom",
"create-react-class",
"lodash", "lodash",
"classnames", "classnames",
"codemirror", "codemirror",
"codemirror/mode/gfm/gfm.js", "codemirror/mode/gfm/gfm.js",
"codemirror/mode/javascript/javascript.js", "codemirror/mode/javascript/javascript.js",
"codemirror/mode/css/css.js",
"moment", "moment",
"superagent", "superagent",
"marked", "marked",

View File

@@ -1,8 +0,0 @@
const snippets = require('../shared/homebrewery/snippets');
console.log(snippets);
//console.log(snippets.brew.spell());
//console.log(snippets.brew.table());
console.log(snippets.brew.noncasterTable());

150
server.js
View File

@@ -1,36 +1,140 @@
const _ = require('lodash');
const jwt = require('jwt-simple');
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')());
const config = require('nconf') const config = require('nconf')
.argv() .argv()
.env({ lowerCase: true }) .env({ lowerCase: true })
.file('environment', { file: `config/${process.env.NODE_ENV}.json` }) .file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' }); .file('defaults', { file: 'config/default.json' });
const log = require('loglevel'); //DB
log.setLevel(config.get('log_level')); require('mongoose')
.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/naturalcrit')
.connection.on('error', ()=>{
console.log('Error : Could not connect to a Mongo Database.');
console.log(' If you are running locally, make sure mongodb.exe is running.');
});
//Server
const app = require('./server/app.js');
/* //Account MIddleware
app.use((req, res, next) => { app.use((req, res, next)=>{
log.debug('---------------------------'); if(req.cookies && req.cookies.nc_session){
log.debug(req.method, req.path); try {
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
if (req.params) { } catch (e){}
log.debug('req params', req.params);
} }
if (req.query) { return next();
log.debug('req query', req.query);
}
next();
}); });
*/
require('./server/db.js').connect()
.then(()=>{ app.use(require('./server/homebrew.api.js'));
const PORT = process.env.PORT || 8000; app.use(require('./server/admin.api.js'));
const httpServer = app.listen(PORT, () => {
log.info(`server on port:${PORT}`);
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
}) })
.catch((err)=>console.error(err)) .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}`);

85
server/admin.api.js Normal file
View File

@@ -0,0 +1,85 @@
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;

View File

@@ -1,60 +0,0 @@
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;

View File

@@ -1,28 +0,0 @@
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;

View File

@@ -1,68 +0,0 @@
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;

View File

@@ -1,102 +0,0 @@
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 : ""},
style : {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:2}
}, {
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);

View File

@@ -1,66 +0,0 @@
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;
};

View File

@@ -1,36 +0,0 @@
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 !== 0){
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
}

View File

@@ -1,29 +0,0 @@
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;

View File

@@ -1,25 +0,0 @@
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;

130
server/homebrew.api.js Normal file
View File

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

81
server/homebrew.model.js Normal file
View File

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

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