0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-23 23:03:01 +00:00

Compare commits

..

46 Commits

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

View File

@@ -1,7 +0,0 @@
{
"dependencies": {
"unused-ignores": [
"react-dom"
]
}
}

View File

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

View File

@@ -1,6 +0,0 @@
node_modules
npm-debug.log
.git
Dockerfile
docker-compose.yml
tests

View File

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

1
.gitattributes vendored
View File

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

View File

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

14
.gitignore vendored
View File

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

View File

@@ -1,14 +0,0 @@
FROM node:8
# Create app directory
WORKDIR /usr/src/app
# Bundle app source
COPY . .
ENV NODE_ENV=docker
RUN yarn
EXPOSE 8000
CMD [ "yarn", "start" ]

View File

@@ -1,13 +1,13 @@
# The Homebrewery
The Homebrewery is a tool for making authentic looking [D&D content](http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook) using only [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). Check it out [here](http://homebrewery.naturalcrit.com).
The Homebrewery is a tool for making authnetic looking [D&D content](http://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook) using only [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). Check it out [here](http://homebrewery.naturalcrit.com).
### issues, suggestions, bugs
If you run into any issues using The Homebrewery, please submit an issue [here](/issues).
If you run into any issues using The Homebrewery, please submit an issues [here](/issues)
### local dev
The Homebrewery is open source, so feel free to clone it, tinker with it, or run your own local version.
Homebrewery is open source, so feel free to clone it, tinker with it, or run your own local version.
#### pre-reqs
1. install [node](https://nodejs.org/en/)
@@ -20,16 +20,14 @@ The Homebrewery is open source, so feel free to clone it, tinker with it, or run
1. `npm start`
#### standalone PHB stylesheet
If you just want the stylesheet that is generated to make pages look like they are from the Player's Handbook, you will find it [here](https://github.com/stolksdorf/homebrewery/blob/master/phb.standalone.css).
If you just want the stylesheet that is generated to make pages look like they are from the PLayer's Handbook, you have find it [here](https://github.com/stolksdorf/homebrewery/blob/master/phb.standalone.css)
If you are developing locally and would like to generate your own, follow the above steps and then run `npm run phb`.
### changelog
You can check out the changelog [here](https://github.com/stolksdorf/homebrewery/blob/master/changelog.md).
You can check out the changelog [here](https://github.com/stolksdorf/homebrewery/blob/master/changelog.md)
### license
This project is licensed under [MIT](./license). Which means you are free to use The Homebrewery in any way that you want, except for claiming that you made it yourself.
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
This project is licensed under [MIT](./license)

View File

@@ -1,15 +1,5 @@
# changelog
### Saturday, 22/04/217 - v2.7.4
- Give ability to hide the render warning notification
### Friday, 03/03/2017 - v2.7.3
- Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews.
### Saturday, 18/02/2017 - v2.7.2
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
## BIG NEWS
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues.
@@ -36,7 +26,8 @@ All brews made previous to the release of v3.0.0 will still render normally.
- Fixed realtime renderer not functioning if loaded with malformed html on load (thanks u/RattiganIV re:247)
- Removed a lot of unused files in shared
- 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
table.brewTable{
th{
padding : 10px;
background-color : fade(@blue, 20%);
font-weight : 800;
}
tr:nth-child(even){
background-color : fade(@green, 10%);
}
tr.isEmpty{
background-color : fade(@red, 30%);
}
td{
min-width : 100px;
padding : 10px;
text-align : center;
/*
&.preview{
position : relative;
&:hover{
.content{
display : block;
}
}
.content{
position : absolute;
display : none;
top : 100%;
left : 0px;
z-index : 1000;
max-height : 500px;
width : 300px;
padding : 30px;
background-color : white;
font-family : monospace;
text-align : left;
pointer-events : none;
}
}
*/
}
}

View File

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

View File

@@ -1,66 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const BrewSearch = createClass({
getDefaultProps : function() {
return {
admin_key : ''
};
},
getInitialState : function() {
return {
searchTerm : '',
brew : null,
};
},
search : function(){
request.get(`/homebrew/api/search?id=${this.state.searchTerm}`)
.query({
admin_key : this.props.admin_key,
})
.end((err, res)=>{
console.log(err, res, res.body.brews[0]);
this.setState({
brew : res.body.brews[0],
});
});
},
handleChange : function(e){
this.setState({
searchTerm : e.target.value
});
},
handleSearchClick : function(){
this.search();
},
renderBrew : function(){
if(!this.state.brew) return null;
return <div className='brew'>
<div>Edit id : {this.state.brew.editId}</div>
<div>Share id : {this.state.brew.shareId}</div>
</div>;
},
render : function(){
return <div className='search'>
<input type='text' value={this.state.searchTerm} onChange={this.handleChange} />
<button onClick={this.handleSearchClick}>Search</button>
{this.renderBrew()}
</div>;
},
});
module.exports = BrewSearch;

View File

@@ -1,170 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const Moment = require('moment');
const BrewLookup = require('./brewLookup/brewLookup.jsx');
const HomebrewAdmin = createClass({
getDefaultProps : function() {
return {
admin_key : ''
};
},
getInitialState : function() {
return {
page : 0,
count : 20,
brewCache : {},
total : 0,
};
},
fetchBrews : function(page){
request.get('/api/search')
.query({
admin_key : this.props.admin_key,
count : this.state.count,
page : page
})
.end((err, res)=>{
if(err || !res.body || !res.body.brews) return;
const newCache = _.extend({}, this.state.brewCache);
newCache[page] = res.body.brews;
this.setState({
brewCache : newCache,
total : res.body.total,
count : res.body.count
});
});
},
componentDidMount : function() {
this.fetchBrews(this.state.page);
},
changePageTo : function(page){
if(!this.state.brewCache[page]){
this.fetchBrews(page);
}
this.setState({
page : page
});
},
clearInvalidBrews : function(){
request.get('/api/invalid')
.query({ admin_key: this.props.admin_key })
.end((err, res)=>{
if(!confirm(`This will remove ${res.body.count} brews. Are you sure?`)) return;
request.get('/api/invalid')
.query({ admin_key: this.props.admin_key, do_it: true })
.end((err, res)=>{
alert('Done!');
});
});
},
deleteBrew : function(brewId){
if(!confirm(`Are you sure you want to delete '${brewId}'?`)) return;
request.get(`/api/remove/${brewId}`)
.query({ admin_key: this.props.admin_key })
.end(function(err, res){
window.location.reload();
});
},
handlePageChange : function(dir){
this.changePageTo(this.state.page + dir);
},
renderPagnination : function(){
let outOf;
if(this.state.total){
outOf = `${this.state.page} / ${Math.round(this.state.total/this.state.count)}`;
}
return <div className='pagnination'>
<i className='fa fa-chevron-left' onClick={()=>this.handlePageChange(-1)}/>
{outOf}
<i className='fa fa-chevron-right' onClick={()=>this.handlePageChange(1)}/>
</div>;
},
renderBrews : function(){
const brews = this.state.brewCache[this.state.page] || _.times(this.state.count);
return _.map(brews, (brew)=>{
return <tr className={cx('brewRow', { 'isEmpty': brew.text == 'false' })} key={brew.shareId || brew}>
<td><a href={`/edit/${brew.editId}`} target='_blank' rel='noopener noreferrer'>{brew.editId}</a></td>
<td><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>{brew.shareId}</a></td>
<td>{Moment(brew.createdAt).fromNow()}</td>
<td>{Moment(brew.updatedAt).fromNow()}</td>
<td>{Moment(brew.lastViewed).fromNow()}</td>
<td>{brew.views}</td>
<td>
<div className='deleteButton' onClick={()=>this.deleteBrew(brew.editId)}>
<i className='fa fa-trash' />
</div>
</td>
</tr>;
});
},
renderBrewTable : function(){
return <div className='brewTable'>
<table>
<thead>
<tr>
<th>Edit Id</th>
<th>Share Id</th>
<th>Created At</th>
<th>Last Updated</th>
<th>Last Viewed</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{this.renderBrews()}
</tbody>
</table>
</div>;
},
render : function(){
return <div className='homebrewAdmin'>
<BrewLookup adminKey={this.props.admin_key} />
{/*
<h2>
Homebrews - {this.state.total}
</h2>
{this.renderPagnination()}
{this.renderBrewTable()}
<button className='clearOldButton' onClick={this.clearInvalidBrews}>
Clear Old
</button>
<BrewSearch admin_key={this.props.admin_key} />
*/}
</div>;
}
});
module.exports = HomebrewAdmin;

View File

@@ -1,53 +0,0 @@
.homebrewAdmin{
margin-bottom: 80px;
.brewTable{
table{
th{
padding : 10px;
font-weight : 800;
}
tr:nth-child(even){
background-color : fade(@green, 10%);
}
tr.isEmpty{
background-color : fade(@red, 30%);
}
td{
min-width : 100px;
padding : 10px;
text-align : center;
&.preview{
position : relative;
&:hover{
.content{
display : block;
}
}
.content{
position : absolute;
display : none;
top : 100%;
left : 0px;
z-index : 1000;
max-height : 500px;
width : 300px;
padding : 30px;
background-color : white;
font-family : monospace;
text-align : left;
pointer-events : none;
}
}
}
}
}
.deleteButton{
cursor: pointer;
}
button.clearOldButton{
float : right;
}
}

View File

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

View File

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

View File

@@ -1,40 +0,0 @@
@import (less) './client/homebrew/phbStyle/phb.style.less';
.pane{
position : relative;
}
.brewRenderer{
will-change : transform;
overflow-y : scroll;
.pages{
margin : 30px 0px;
&>.phb{
margin-right : auto;
margin-bottom : 30px;
margin-left : auto;
box-shadow : 1px 4px 14px #000;
}
}
}
.pageInfo{
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}
.ppr_msg{
position : absolute;
left : 0px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,50 +1,22 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
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(){
return <Nav.base>
<Nav.section>
<Nav.logo />
<Nav.item href='/' className='homebrewLogo'>
<div>The Homebrewery</div>
</Nav.item>
<Nav.item>{`v${this.state.ver}`}</Nav.item>
{/*this.renderChromeWarning()*/}
</Nav.section>
{this.props.children}
</Nav.base>;
}
});
module.exports = Navbar;
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Navbar = React.createClass({
render : function(){
return <Nav.base>
<Nav.section>
<Nav.logo />
<Nav.item href='/' className='homebrewLogo'>
<div>The Homebrewery</div>
</Nav.item>
<Nav.item>{`v${Store.getVersion()}`}</Nav.item>
</Nav.section>
{this.props.children}
</Nav.base>
}
});
module.exports = Navbar;

View File

@@ -13,32 +13,6 @@
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{
font-size : 12px;
font-weight : 800;
@@ -125,4 +99,34 @@
text-align : center;
}
}
.staticSave.navItem{
background-color : @orange;
&:hover{
background-color : @green;
}
}
.continousSave.navItem{
width : 105px;
text-align : center;
&.saved{
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
.errorContainer{
position : absolute;
top : 29px;
left : -20px;
z-index : 1000;
width : 120px;
padding : 8px;
background-color : #333;
a{
color : @teal;
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,222 +1,63 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
//const RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Markdown = require('naturalcrit/markdown.js');
const SAVE_TIMEOUT = 3000;
const EditPage = createClass({
getDefaultProps : function() {
return {
brew : {
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
}
};
},
getInitialState : function() {
return {
brew : this.props.brew,
isSaving : false,
isPending : false,
errors : null,
htmlErrors : Markdown.validate(this.props.brew.text),
};
},
savedBrew : null,
componentDidMount : function(){
this.trySave();
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){
return 'You have unsaved changes!';
}
};
this.setState((prevState)=>({
htmlErrors : Markdown.validate(prevState.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((prevState)=>({
brew : _.merge({}, prevState.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((prevState)=>({
brew : _.merge({}, prevState.brew, { text: text }),
isPending : true,
htmlErrors : htmlErrors
}), ()=>this.trySave());
},
hasChanges : function(){
const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew;
return !_.isEqual(this.state.brew, savedBrew);
},
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((prevState)=>({
isSaving : true,
errors : null,
htmlErrors : Markdown.validate(prevState.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,
});
}
});
},
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' rel='noopener noreferrer'
href={`https://github.com/stolksdorf/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
here
</a>.
</div>
</Nav.item>;
}
if(this.state.isSaving){
return <Nav.item className='save' icon='fa-spinner fa-spin'>saving...</Nav.item>;
}
if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>;
}
if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item>;
}
},
renderNavbar : function(){
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
{/*<RecentlyEdited brew={this.props.brew} />*/}
<ReportIssue />
<Nav.item newTab={true} href={`/share/${this.props.brew.shareId}`} color='teal' icon='fa-share-alt'>
Share
</Nav.item>
<PrintLink shareId={this.props.brew.shareId} />
<Account />
</Nav.section>
</Navbar>;
},
render : function(){
return <div className='editPage page'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
value={this.state.brew.text}
onChange={this.handleTextChange}
metadata={this.state.brew}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} />
</SplitPane>
</div>
</div>;
}
});
module.exports = EditPage;
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const Items = require('../../navbar/navitems.js');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Utils = require('homebrewery/utils.js');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const EditPage = React.createClass({
componentDidMount: 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 {
brew : {}
};
},
render : function(){
return <Navbar>
<Nav.section>
<Items.BrewTitle />
</Nav.section>
<Nav.section>
<Items.ContinousSave />
<Items.Issue />
<Nav.item newTab={true} href={'/share/' + Store.getBrew().shareId} color='teal' icon='fa-share-alt'>
Share
</Nav.item>
<Items.Print />
<Items.Account />
</Nav.section>
</Navbar>
}
}), ()=>{
return {brew : Store.getBrew()}
});
module.exports = EditPage;

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ 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');
var win = window.open(`/homebrew/print/${shareId}?dialog=true`, '_blank');
win.focus();
event.preventDefault();
}

View File

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

View File

@@ -1,162 +1,62 @@
const React = require('react');
const createClass = require('create-react-class');
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 Navbar = require('../../navbar/navbar.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const KEY = 'homebrewery-new';
const NewPage = createClass({
getInitialState : function() {
return {
metadata : {
title : '',
description : '',
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);
},
componentWillUnmount : 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) 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>;
},
render : function(){
return <div className='newPage page'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
value={this.state.text}
onChange={this.handleTextChange}
metadata={this.state.metadata}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.text} errors={this.state.errors} />
</SplitPane>
</div>
</div>;
}
});
module.exports = NewPage;
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const Items = require('../../navbar/navitems.js');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Utils = require('homebrewery/utils.js');
const KEY = 'homebrewery-new';
const NewPage = React.createClass({
componentDidMount: function() {
try{
const storedBrew = JSON.parse(localStorage.getItem(KEY));
if(storedBrew && storedBrew.text) Actions.setBrew(storedBrew);
}catch(e){}
Store.updateEmitter.on('change', this.saveToLocal);
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount: function() {
Store.updateEmitter.removeListener('change', this.saveToLocal);
document.removeEventListener('keydown', this.handleControlKeys);
},
saveToLocal : function(){
localStorage.setItem(KEY, JSON.stringify(Store.getBrew()));
},
handleControlKeys : Utils.controlKeys({
s : Actions.saveNew,
p : Actions.localPrint
}),
render : function(){
return <div className='newPage page'>
<Navbar>
<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 />
<Items.Account />
</Nav.section>
</Navbar>
<div className='content'>
<BrewInterface />
</div>
</div>
}
});
module.exports = NewPage;

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,13 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const moment = require('moment');
const request = require('superagent');
const BrewItem = createClass({
getDefaultProps : function() {
const BrewItem = React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
title : '',
description : '',
authors : []
@@ -17,30 +15,12 @@ const BrewItem = createClass({
};
},
deleteBrew : function(){
if(!confirm('are you sure you want to delete this brew?')) return;
if(!confirm('are you REALLY sure? You will not be able to recover it')) return;
request.get(`/api/remove/${this.props.brew.editId}`)
.send()
.end(function(err, res){
location.reload();
});
},
renderDeleteBrewLink : function(){
renderEditLink: 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;
return <a href={`/edit/${this.props.brew.editId}`} target='_blank' rel='noopener noreferrer'>
return <a href={`/edit/${this.props.brew.editId}`} target='_blank'>
<i className='fa fa-pencil' />
</a>;
</a>
},
render : function(){
@@ -63,13 +43,12 @@ const BrewItem = createClass({
</div>
<div className='links'>
<a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>
<a href={`/share/${brew.shareId}`} target='_blank'>
<i className='fa fa-share-alt' />
</a>
{this.renderEditLink()}
{this.renderDeleteBrewLink()}
</div>
</div>;
</div>
}
});

View File

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

View File

@@ -156,7 +156,6 @@ body {
margin-bottom : 1em;
font-size : 10pt;
thead{
display: table-row-group;
font-weight : 800;
th{
vertical-align : bottom;
@@ -338,8 +337,7 @@ body {
p,blockquote,table{
z-index : 15;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
column-break-inside : avoid;
overflow: hidden; /* Firefox fix */
}
//Better spacing for spell blocks
@@ -357,8 +355,7 @@ body {
}
li{
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
column-break-inside : avoid;
}
}
//*****************************
@@ -383,8 +380,20 @@ body {
text-indent : -1em;
list-style-type : none;
-webkit-column-break-inside : auto;
page-break-inside : auto;
break-inside : auto;
column-break-inside : auto;
}
}
//*****************************
// * PRINT
// *****************************/
.phb.print{
blockquote{
box-shadow : none;
}
}
@media print {
.phb .descriptive, .phb blockquote{
box-shadow : none;
}
}
//*****************************
@@ -406,7 +415,7 @@ body {
border : initial;
border-style : solid;
border-image-outset : 25px 17px;
border-image-repeat : stretch;
border-image-repeat : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
@@ -414,9 +423,9 @@ body {
margin-bottom : 10px;
}
}
//************************************
// * DESCRIPTIVE TEXT BOX
// ************************************/
//*****************************
// * CLASS TABLE
// *****************************/
.phb .descriptive{
display : block-inline;
margin-bottom : 1em;
@@ -424,7 +433,7 @@ body {
font-family : ScalySans;
border-style : solid;
border-width : 7px;
border-image : @descriptiveBoxImage 12 stretch;
border-image : @descriptiveBoxImage 12 round;
border-image-outset : 4px;
box-shadow : 0px 0px 6px #faf7ea;
p{
@@ -453,8 +462,7 @@ body {
// *****************************/
.phb .toc{
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
column-break-inside : avoid;
a{
color : black;
text-decoration : none;
@@ -469,4 +477,4 @@ body {
&>ul>li{
margin-bottom : 10px;
}
}
}

View File

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

View File

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

4
config/production.json Normal file
View File

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

4
config/staging.json Normal file
View File

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

View File

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

View File

@@ -1,17 +0,0 @@
version: '2'
services:
mongodb:
image: mongo:latest
volumes:
- mongodata:/data/db
homebrewery:
build:
context: .
dockerfile: Dockerfile
image: homebrewery
environment:
MONGODB_URI: mongodb://mongodb/homebrewery
ports:
- "8000:8000"
volumes:
mongodata:

8118
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@@ -1,10 +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)=>{
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)=>{
}, (err) => {
console.error(err);
});

22
scripts/populate.js Normal file
View File

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

View File

@@ -5,7 +5,6 @@
"libs" : [
"react",
"react-dom",
"create-react-class",
"lodash",
"classnames",
"codemirror",
@@ -14,6 +13,7 @@
"moment",
"superagent",
"marked",
"pico-router"
"pico-router",
"pico-flux"
]
}

176
server.js
View File

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

View File

@@ -1,85 +0,0 @@
const _ = require('lodash');
const auth = require('basic-auth');
const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
const mw = {
adminOnly : (req, res, next)=>{
if(req.query && req.query.admin_key == process.env.ADMIN_KEY) return next();
return res.status(401).send('Access denied');
}
};
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password';
process.env.ADMIN_KEY = process.env.ADMIN_KEY || 'admin_key';
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
router.get('/api/invalid', mw.adminOnly, (req, res)=>{
const invalidBrewQuery = HomebrewModel.find({
'$where' : 'this.text.length < 140',
createdAt : {
$lt : Moment().subtract(3, 'days').toDate()
}
});
if(req.query.do_it){
invalidBrewQuery.remove().exec((err, objs)=>{
refreshCount();
return res.send(200);
});
} else {
invalidBrewQuery.exec((err, objs)=>{
if(err) console.log(err);
return res.json({
count : objs.length
});
});
}
});
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
//search for mathcing edit id
//search for matching share id
// search for partial match
HomebrewModel.findOne({ $or : [
{ editId: { '$regex': req.params.id, '$options': 'i' } },
{ shareId: { '$regex': req.params.id, '$options': 'i' } },
] }).exec((err, brew)=>{
return res.json(brew);
});
});
//Admin route
const render = require('vitreum/steps/render');
const templateFn = require('../client/template.js');
router.get('/admin', function(req, res){
const credentials = auth(req);
if(!credentials || credentials.name !== process.env.ADMIN_USER || credentials.pass !== process.env.ADMIN_PASS) {
res.setHeader('WWW-Authenticate', 'Basic realm="example"');
return res.status(401).send('Access denied');
}
render('admin', templateFn, {
url : req.originalUrl,
admin_key : process.env.ADMIN_KEY,
})
.then((page)=>{
return res.send(page);
})
.catch((err)=>{
console.log(err);
return res.sendStatus(500);
});
});
module.exports = router;

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

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

28
server/app.js Normal file
View File

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

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

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

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

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

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

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

36
server/db.js Normal file
View File

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

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

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

25
server/error.js Normal file
View File

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

View File

@@ -1,130 +1,146 @@
const _ = require('lodash');
const 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;
}
const _ = require('lodash');
const Moment = require('moment');
const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
//TODO: Possiblity remove
let homebrewTotal = 0;
const refreshCount = ()=>{
HomebrewModel.count({}, (err, total)=>{
homebrewTotal = total;
});
};
refreshCount();
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");
var 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;
}
*/

View File

@@ -1,81 +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,
};
var mongoose = require('mongoose');
var shortid = require('shortid');
var _ = require('lodash');
var 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) => {
let 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);
}));
});
});
};
var Homebrew = mongoose.model('Homebrew', HomebrewSchema);
module.exports = {
schema : HomebrewSchema,
model : Homebrew,
}

View File

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

69
server/middleware.js Normal file
View File

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

23
server/utils.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,146 +1,130 @@
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;
const React = require('react');
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 BrewEditor = React.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
})
},
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();
},
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
},
renderMetadataEditor : function(){
if(!this.state.showMetadataEditor) return;
return <MetadataEditor
metadata={this.props.metadata}
onChange={this.props.onMetadataChange}
/>
},
render : function(){
this.highlightPageLines();
return<div className='brewEditor' 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>
/*
<div className='brewJump' onClick={this.brewJump}>
<i className='fa fa-arrow-right' />
</div>
*/
}
});
module.exports = BrewEditor;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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