mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 01:13:15 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6cd6b3c1c6 | ||
|
|
907dbe4416 | ||
|
|
47429d804a | ||
|
|
1d54ab49e6 | ||
|
|
f634c451ff | ||
|
|
778c0ca0b1 | ||
|
|
f4985b68ca | ||
|
|
c8875cff94 | ||
|
|
40f1d4c790 | ||
|
|
7818b6db46 | ||
|
|
4163e95938 | ||
|
|
fed0536673 | ||
|
|
a4b92af351 | ||
|
|
4584b83c60 | ||
|
|
c57ca0b27f | ||
|
|
c04cd23213 | ||
|
|
140f064786 | ||
|
|
643f6f933d | ||
|
|
a1140f75d8 | ||
|
|
b68c6a4ad2 | ||
|
|
6e0f042b42 | ||
|
|
c47d492ed3 | ||
|
|
18852932e8 |
33
.circleci/config.yml
Normal file
33
.circleci/config.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
# Javascript Node CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
|
||||
#
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:8.9
|
||||
- image: circleci/mongo:3.4.4
|
||||
|
||||
working_directory: ~/repo
|
||||
|
||||
steps:
|
||||
- checkout
|
||||
|
||||
# Download and cache dependencies
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-dependencies-{{ checksum "package.json" }}
|
||||
# fallback to using the latest cache if no exact match is found
|
||||
- v1-dependencies-
|
||||
|
||||
- run: npm install
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
# run tests!
|
||||
- run: npm run circleci
|
||||
|
||||
78
.eslintrc.js
Normal file
78
.eslintrc.js
Normal file
@@ -0,0 +1,78 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parserOptions : {
|
||||
ecmaVersion : 9,
|
||||
sourceType : 'module',
|
||||
ecmaFeatures : {
|
||||
jsx : true
|
||||
}
|
||||
},
|
||||
env : {
|
||||
browser : true,
|
||||
},
|
||||
plugins : ['react'],
|
||||
rules : {
|
||||
/** Errors **/
|
||||
'camelcase' : ['error', { properties: 'never' }],
|
||||
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||
'no-array-constructor' : 'error',
|
||||
'no-iterator' : 'error',
|
||||
'no-nested-ternary' : 'error',
|
||||
'no-new-object' : 'error',
|
||||
'no-proto' : 'error',
|
||||
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
||||
'react/jsx-uses-react' : 'error',
|
||||
'react/prefer-es6-class' : ['error', 'never'],
|
||||
|
||||
/** Warnings **/
|
||||
'max-lines' : ['warn', {
|
||||
max : 200,
|
||||
skipComments : true,
|
||||
skipBlankLines : true,
|
||||
}],
|
||||
'max-depth' : ['warn', { max: 4 }],
|
||||
'max-params' : ['warn', { max: 4 }],
|
||||
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
|
||||
'no-unused-vars' : ['warn', {
|
||||
vars : 'all',
|
||||
args : 'none',
|
||||
varsIgnorePattern : 'config|_|cx|createClass'
|
||||
}],
|
||||
'react/jsx-uses-vars' : 'warn',
|
||||
|
||||
/** Fixable **/
|
||||
'arrow-parens' : ['warn', 'always'],
|
||||
'brace-style' : ['warn', '1tbs', { allowSingleLine: true }],
|
||||
'jsx-quotes' : ['warn', 'prefer-single'],
|
||||
'linebreak-style' : ['warn', 'unix'],
|
||||
'no-var' : 'warn',
|
||||
'prefer-const' : 'warn',
|
||||
'prefer-template' : 'warn',
|
||||
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true } ],
|
||||
'semi' : ['warn', 'always'],
|
||||
|
||||
/** Whitespace **/
|
||||
'array-bracket-spacing' : ['warn', 'never'],
|
||||
'arrow-spacing' : ['warn', { before: false, after: false }],
|
||||
'comma-spacing' : ['warn', { before: false, after: true }],
|
||||
'indent' : ['warn', 'tab'],
|
||||
'keyword-spacing' : ['warn', {
|
||||
before : true,
|
||||
after : true,
|
||||
overrides : {
|
||||
if : { 'before': false, 'after': false }
|
||||
}
|
||||
}],
|
||||
'key-spacing' : ['warn', {
|
||||
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
|
||||
singleLine : { beforeColon: false, afterColon: true }
|
||||
}],
|
||||
'linebreak-style' : ['warn', 'unix'],
|
||||
'no-trailing-spaces' : 'warn',
|
||||
'no-whitespace-before-property' : 'warn',
|
||||
'object-curly-spacing' : ['warn', 'always'],
|
||||
'react/jsx-indent-props' : ['warn', 'tab'],
|
||||
'space-in-parens' : ['warn', 'never'],
|
||||
'template-curly-spacing' : ['warn', 'never'],
|
||||
}
|
||||
};
|
||||
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
package-lock.json binary
|
||||
25
.github/issue_template.md
vendored
25
.github/issue_template.md
vendored
@@ -1,5 +1,28 @@
|
||||
Share link to issue brew: http://homebrewery.naturalcrit.com/share/XXXXXXX
|
||||
<!-- CLICK "Preview" FOR INSTRUCTIONS IN A MORE READABLE FORMAT -->
|
||||
|
||||
## Before you submit
|
||||
|
||||
- Support questions are better asked on the subreddit [r/homebrewery](https://www.reddit.com/r/homebrewery/)
|
||||
- Read the [contributing guidelines](https://github.com/stolksdorf/homebrewery/blob/master/contributing.md).
|
||||
- If it's an issue, please make sure it's reproducible
|
||||
- Ensure the issue isn't already reported.
|
||||
|
||||
|
||||
*Delete the above section and the instructions in the sections below before submitting*
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
If this is a *feature request*, explain why it should be added. Specific use-cases are best.
|
||||
|
||||
For *bug reports*, please provide as much *relevant* info as possible.
|
||||
|
||||
**Share Link** :
|
||||
|
||||
or
|
||||
|
||||
**Brew code to reproduce** : <details><summary>Click to expand</summary><code><pre>
|
||||
|
||||
PASTE BREW CODE HERE
|
||||
|
||||
</pre></code></details>
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,16 +1,10 @@
|
||||
# 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
|
||||
@@ -1,8 +1,14 @@
|
||||
# changelog
|
||||
|
||||
### Saturday, 22/04/217 - v2.7.4
|
||||
- Give ability to hide the render warning notification
|
||||
|
||||
The self-discovery aspect of the snippets isn't working out.
|
||||
|
||||
### Friday, 03/03/2017 - v2.7.3
|
||||
- Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews.
|
||||
|
||||
### Saturday, 18/02/2017 - v2.7.2
|
||||
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
|
||||
|
||||
## BIG NEWS
|
||||
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues.
|
||||
@@ -30,8 +36,7 @@ All brews made previous to the release of v3.0.0 will still render normally.
|
||||
- Fixed realtime renderer not functioning if loaded with malformed html on load (thanks u/RattiganIV re:247)
|
||||
- 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
|
||||
@@ -1,42 +1,39 @@
|
||||
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;
|
||||
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;
|
||||
|
||||
@@ -1,53 +1,39 @@
|
||||
@import 'naturalcrit/styles/reset.less';
|
||||
@import 'naturalcrit/styles/elements.less';
|
||||
@import 'naturalcrit/styles/animations.less';
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@import 'naturalcrit/styles/tooltip.less';
|
||||
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
html,body, #reactRoot{
|
||||
@import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
html,body, #reactContainer, .naturalCrit{
|
||||
min-height : 100%;
|
||||
}
|
||||
|
||||
@sidebarWidth : 250px;
|
||||
|
||||
body{
|
||||
height : 100%;
|
||||
background-color : #eee;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
color : #4b5055;
|
||||
font-weight : 100;
|
||||
text-rendering : optimizeLegibility;
|
||||
margin : 0;
|
||||
padding : 0;
|
||||
background-color : #ddd;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-weight : 100;
|
||||
color : #4b5055;
|
||||
text-rendering : optimizeLegibility;
|
||||
height : 100%;
|
||||
}
|
||||
.admin {
|
||||
nav {
|
||||
|
||||
.admin{
|
||||
|
||||
header{
|
||||
background-color : @red;
|
||||
.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;
|
||||
font-size: 2em;
|
||||
padding : 20px 0px;
|
||||
color : white;
|
||||
margin-bottom: 30px;
|
||||
i{
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
|
||||
const BrewTable = require('../brewTable/brewTable.jsx');
|
||||
|
||||
const LIMIT = 10;
|
||||
|
||||
const AdminSearch = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
adminKey : '',
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
totalBrews : 1,
|
||||
brews: [],
|
||||
|
||||
searching : false,
|
||||
error : null,
|
||||
|
||||
page : 1,
|
||||
searchTerms : ''
|
||||
};
|
||||
},
|
||||
|
||||
handleSearch : function(e){
|
||||
this.setState({
|
||||
searchTerms : e.target.value
|
||||
});
|
||||
},
|
||||
handlePage : function(e){
|
||||
this.setState({
|
||||
page : e.target.value
|
||||
});
|
||||
},
|
||||
|
||||
search : function(){
|
||||
this.setState({ searching : true, error : null });
|
||||
|
||||
request.get(`/api/brew`)
|
||||
.query({
|
||||
terms : this.state.searchTerms,
|
||||
limit : LIMIT,
|
||||
page : this.state.page - 1
|
||||
})
|
||||
.set('x-homebrew-admin', this.props.adminKey)
|
||||
.end((err, res) => {
|
||||
if(err){
|
||||
this.setState({
|
||||
searching : false,
|
||||
error : err && err.toString()
|
||||
});
|
||||
}else{
|
||||
this.setState({
|
||||
brews : res.body.brews,
|
||||
totalBrews : res.body.total
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render: function(){
|
||||
return <div className='adminSearch'>
|
||||
<h1>Admin Search</h1>
|
||||
<div className='controls'>
|
||||
<input className='search' type='text' value={this.state.searchTerms} onChange={this.handleSearch} />
|
||||
|
||||
<button onClick={this.search}> <i className='fa fa-search' /> search </button>
|
||||
|
||||
|
||||
<div className='page'>
|
||||
page:
|
||||
<input type='text' value={this.state.page} onChange={this.handlePage} />
|
||||
/ {Math.ceil(this.state.totalBrews / LIMIT)}
|
||||
</div>
|
||||
</div>
|
||||
<BrewTable brews={this.state.brews} />
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AdminSearch;
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
.adminSearch{
|
||||
.controls{
|
||||
margin-bottom : 20px;
|
||||
input.search{
|
||||
height : 33px;
|
||||
padding : 10px;
|
||||
}
|
||||
.page {
|
||||
float : right;
|
||||
font-weight : 800;
|
||||
input{
|
||||
width : 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
.brewLookup{
|
||||
height : 200px;
|
||||
input{
|
||||
height : 33px;
|
||||
margin-bottom : 20px;
|
||||
padding : 0px 10px;
|
||||
}
|
||||
.error{
|
||||
font-weight : 800;
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Moment = require('moment');
|
||||
|
||||
//TODO: Add in delete
|
||||
|
||||
const BrewTable = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
brews : []
|
||||
};
|
||||
},
|
||||
renderRows : function(){
|
||||
return _.map(this.props.brews, (brew) => {
|
||||
let authors = 'None.';
|
||||
if(brew.authors && brew.authors.length) authors = brew.authors.join(', ');
|
||||
|
||||
return <tr className={cx('brewRow', {'isEmpty' : brew.text == "false"})} key={brew.shareId || brew}>
|
||||
<td>{brew.title}</td>
|
||||
<td>{authors}</td>
|
||||
<td><a href={'/edit/' + brew.editId} target='_blank'>{brew.editId}</a></td>
|
||||
<td><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></td>
|
||||
<td>{Moment(brew.updatedAt).fromNow()}</td>
|
||||
<td>{brew.views}</td>
|
||||
<td className='deleteButton'>
|
||||
<i className='fa fa-trash' />
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
});
|
||||
},
|
||||
render: function(){
|
||||
return <table className='brewTable'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Authors</th>
|
||||
<th>Edit Link</th>
|
||||
<th>Share Link</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Views</th>
|
||||
<th>Remove</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderRows()}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewTable;
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
table.brewTable{
|
||||
th{
|
||||
padding : 10px;
|
||||
background-color : fade(@blue, 20%);
|
||||
font-weight : 800;
|
||||
}
|
||||
tr:nth-child(even){
|
||||
background-color : fade(@green, 10%);
|
||||
}
|
||||
tr.isEmpty{
|
||||
background-color : fade(@red, 30%);
|
||||
}
|
||||
td{
|
||||
min-width : 100px;
|
||||
padding : 10px;
|
||||
text-align : center;
|
||||
|
||||
/*
|
||||
&.preview{
|
||||
position : relative;
|
||||
&:hover{
|
||||
.content{
|
||||
display : block;
|
||||
}
|
||||
}
|
||||
.content{
|
||||
position : absolute;
|
||||
display : none;
|
||||
top : 100%;
|
||||
left : 0px;
|
||||
z-index : 1000;
|
||||
max-height : 500px;
|
||||
width : 300px;
|
||||
padding : 30px;
|
||||
background-color : white;
|
||||
font-family : monospace;
|
||||
text-align : left;
|
||||
pointer-events : none;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -1,85 +1,67 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
const BrewTable = require('../brewTable/brewTable.jsx');
|
||||
const Moment = require('moment');
|
||||
|
||||
const BrewLookup = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
|
||||
const BrewLookup = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
adminKey : '',
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
getInitialState : function() {
|
||||
return {
|
||||
query:'',
|
||||
query : '',
|
||||
resultBrew : null,
|
||||
searching : false,
|
||||
error : null
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
handleChange : function(e){
|
||||
this.setState({
|
||||
query : e.target.value
|
||||
})
|
||||
});
|
||||
},
|
||||
lookup : function(){
|
||||
this.setState({ searching : true, error : null });
|
||||
this.setState({ searching: true });
|
||||
|
||||
request.get(`/admin/lookup/${this.state.query}`)
|
||||
.set('x-homebrew-admin', this.props.adminKey)
|
||||
.end((err, res) => {
|
||||
.query({ admin_key: this.props.adminKey })
|
||||
.end((err, res)=>{
|
||||
this.setState({
|
||||
searching : false,
|
||||
error : err && err.toString(),
|
||||
searching : false,
|
||||
resultBrew : (err ? null : res.body)
|
||||
});
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
renderFoundBrew : function(){
|
||||
if(this.state.searching) return <div className='searching'><i className='fa fa-spin fa-spinner' /></div>;
|
||||
if(!this.state.resultBrew) return <div className='noBrew'>No brew found.</div>;
|
||||
|
||||
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'>{brew.editId}</a></div>
|
||||
<div><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></div>
|
||||
<div><a href={`/edit/${brew.editId}`} target='_blank'>/edit/{brew.editId}</a></div>
|
||||
<div><a href={`/share/${brew.shareId}`} target='_blank'>/share/{brew.shareId}</a></div>
|
||||
<div>{Moment(brew.updatedAt).fromNow()}</div>
|
||||
<div>{brew.views}</div>
|
||||
<div>
|
||||
<div className='deleteButton'>
|
||||
<i className='fa fa-trash' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
*/
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderError : function(){
|
||||
if(!this.state.error) return;
|
||||
|
||||
return <div className='error'>
|
||||
{this.state.error}
|
||||
</div>
|
||||
},
|
||||
|
||||
render: function(){
|
||||
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()}
|
||||
{this.renderError()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
8
client/admin/homebrewAdmin/brewLookup/brewLookup.less
Normal file
8
client/admin/homebrewAdmin/brewLookup/brewLookup.less
Normal file
@@ -0,0 +1,8 @@
|
||||
.brewLookup{
|
||||
height : 200px;
|
||||
input{
|
||||
height : 33px;
|
||||
padding : 0px 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
73
client/admin/homebrewAdmin/brewSearch.jsx
Normal file
73
client/admin/homebrewAdmin/brewSearch.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
|
||||
const BrewSearch = createClass({
|
||||
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
admin_key : ''
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
searchTerm : '',
|
||||
brew : null,
|
||||
searching : false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
search : function(){
|
||||
this.setState({
|
||||
searching : true
|
||||
});
|
||||
|
||||
request.get(`/homebrew/api/search?id=${this.state.searchTerm}`)
|
||||
.query({
|
||||
admin_key : this.props.admin_key,
|
||||
})
|
||||
.end((err, res)=>{
|
||||
console.log(err, res, res.body.brews[0]);
|
||||
this.setState({
|
||||
brew : res.body.brews[0],
|
||||
|
||||
searching : false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleChange : function(e){
|
||||
this.setState({
|
||||
searchTerm : e.target.value
|
||||
});
|
||||
},
|
||||
handleSearchClick : function(){
|
||||
this.search();
|
||||
},
|
||||
|
||||
renderBrew : function(){
|
||||
if(!this.state.brew) return null;
|
||||
return <div className='brew'>
|
||||
<div>Edit id : {this.state.brew.editId}</div>
|
||||
<div>Share id : {this.state.brew.shareId}</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='search'>
|
||||
<input type='text' value={this.state.searchTerm} onChange={this.handleChange} />
|
||||
|
||||
<button onClick={this.handleSearchClick}>Search</button>
|
||||
|
||||
{this.renderBrew()}
|
||||
</div>;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = BrewSearch;
|
||||
171
client/admin/homebrewAdmin/homebrewAdmin.jsx
Normal file
171
client/admin/homebrewAdmin/homebrewAdmin.jsx
Normal file
@@ -0,0 +1,171 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require('superagent');
|
||||
|
||||
const Moment = require('moment');
|
||||
|
||||
|
||||
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
||||
|
||||
|
||||
const HomebrewAdmin = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
admin_key : ''
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
page : 0,
|
||||
count : 20,
|
||||
brewCache : {},
|
||||
total : 0,
|
||||
|
||||
processingOldBrews : false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
fetchBrews : function(page){
|
||||
request.get('/api/search')
|
||||
.query({
|
||||
admin_key : this.props.admin_key,
|
||||
count : this.state.count,
|
||||
page : page
|
||||
})
|
||||
.end((err, res)=>{
|
||||
if(err || !res.body || !res.body.brews) return;
|
||||
this.state.brewCache[page] = res.body.brews;
|
||||
this.setState({
|
||||
brewCache : this.state.brewCache,
|
||||
total : res.body.total,
|
||||
count : res.body.count
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
this.fetchBrews(this.state.page);
|
||||
},
|
||||
|
||||
changePageTo : function(page){
|
||||
if(!this.state.brewCache[page]){
|
||||
this.fetchBrews(page);
|
||||
}
|
||||
this.setState({
|
||||
page : page
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
clearInvalidBrews : function(){
|
||||
request.get('/api/invalid')
|
||||
.query({ admin_key: this.props.admin_key })
|
||||
.end((err, res)=>{
|
||||
if(!confirm(`This will remove ${res.body.count} brews. Are you sure?`)) return;
|
||||
request.get('/api/invalid')
|
||||
.query({ admin_key: this.props.admin_key, do_it: true })
|
||||
.end((err, res)=>{
|
||||
alert('Done!');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
deleteBrew : function(brewId){
|
||||
if(!confirm(`Are you sure you want to delete '${brewId}'?`)) return;
|
||||
request.get(`/api/remove/${brewId}`)
|
||||
.query({ admin_key: this.props.admin_key })
|
||||
.end(function(err, res){
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
handlePageChange : function(dir){
|
||||
this.changePageTo(this.state.page + dir);
|
||||
},
|
||||
|
||||
|
||||
renderPagnination : function(){
|
||||
let outOf;
|
||||
if(this.state.total){
|
||||
outOf = `${this.state.page} / ${Math.round(this.state.total/this.state.count)}`;
|
||||
}
|
||||
return <div className='pagnination'>
|
||||
<i className='fa fa-chevron-left' onClick={()=>this.handlePageChange(-1)}/>
|
||||
{outOf}
|
||||
<i className='fa fa-chevron-right' onClick={()=>this.handlePageChange(1)}/>
|
||||
</div>;
|
||||
},
|
||||
|
||||
|
||||
renderBrews : function(){
|
||||
const brews = this.state.brewCache[this.state.page] || _.times(this.state.count);
|
||||
return _.map(brews, (brew)=>{
|
||||
return <tr className={cx('brewRow', { 'isEmpty': brew.text == 'false' })} key={brew.shareId || brew}>
|
||||
<td><a href={`/edit/${brew.editId}`} target='_blank'>{brew.editId}</a></td>
|
||||
<td><a href={`/share/${brew.shareId}`} target='_blank'>{brew.shareId}</a></td>
|
||||
<td>{Moment(brew.createdAt).fromNow()}</td>
|
||||
<td>{Moment(brew.updatedAt).fromNow()}</td>
|
||||
<td>{Moment(brew.lastViewed).fromNow()}</td>
|
||||
<td>{brew.views}</td>
|
||||
<td>
|
||||
<div className='deleteButton' onClick={()=>this.deleteBrew(brew.editId)}>
|
||||
<i className='fa fa-trash' />
|
||||
</div>
|
||||
</td>
|
||||
</tr>;
|
||||
});
|
||||
},
|
||||
|
||||
renderBrewTable : function(){
|
||||
return <div className='brewTable'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Edit Id</th>
|
||||
<th>Share Id</th>
|
||||
<th>Created At</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Last Viewed</th>
|
||||
<th>Views</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderBrews()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='homebrewAdmin'>
|
||||
|
||||
<BrewLookup adminKey={this.props.admin_key} />
|
||||
|
||||
{/*
|
||||
<h2>
|
||||
Homebrews - {this.state.total}
|
||||
</h2>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{this.renderPagnination()}
|
||||
{this.renderBrewTable()}
|
||||
|
||||
<button className='clearOldButton' onClick={this.clearInvalidBrews}>
|
||||
Clear Old
|
||||
</button>
|
||||
|
||||
<BrewSearch admin_key={this.props.admin_key} />
|
||||
*/}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = HomebrewAdmin;
|
||||
53
client/admin/homebrewAdmin/homebrewAdmin.less
Normal file
53
client/admin/homebrewAdmin/homebrewAdmin.less
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
.homebrewAdmin{
|
||||
margin-bottom: 80px;
|
||||
.brewTable{
|
||||
table{
|
||||
|
||||
th{
|
||||
padding : 10px;
|
||||
font-weight : 800;
|
||||
}
|
||||
tr:nth-child(even){
|
||||
background-color : fade(@green, 10%);
|
||||
}
|
||||
tr.isEmpty{
|
||||
background-color : fade(@red, 30%);
|
||||
}
|
||||
td{
|
||||
min-width : 100px;
|
||||
padding : 10px;
|
||||
text-align : center;
|
||||
|
||||
&.preview{
|
||||
position : relative;
|
||||
&:hover{
|
||||
.content{
|
||||
display : block;
|
||||
}
|
||||
}
|
||||
.content{
|
||||
position : absolute;
|
||||
display : none;
|
||||
top : 100%;
|
||||
left : 0px;
|
||||
z-index : 1000;
|
||||
max-height : 500px;
|
||||
width : 300px;
|
||||
padding : 30px;
|
||||
background-color : white;
|
||||
font-family : monospace;
|
||||
text-align : left;
|
||||
pointer-events : none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.deleteButton{
|
||||
cursor: pointer;
|
||||
}
|
||||
button.clearOldButton{
|
||||
float : right;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
const BrewTable = require('../brewTable/brewTable.jsx');
|
||||
|
||||
|
||||
const InvalidBrew = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
adminKey : '',
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
brews: []
|
||||
};
|
||||
},
|
||||
getInvalid : function(){
|
||||
request.get(`/admin/invalid`)
|
||||
.set('x-homebrew-admin', this.props.adminKey)
|
||||
.end((err, res) => {
|
||||
this.setState({
|
||||
brews : res.body
|
||||
});
|
||||
})
|
||||
},
|
||||
removeInvalid : function(){
|
||||
if(!this.state.brews.length) return;
|
||||
if(!confirm(`Are you sure you want to remove ${this.state.brews.length} brews`)) return;
|
||||
if(!confirm('Sure you are sure?')) return;
|
||||
|
||||
request.delete(`/admin/invalid`)
|
||||
.set('x-homebrew-admin', this.props.adminKey)
|
||||
.end((err, res) => {
|
||||
console.log(err, res.body);
|
||||
alert('Invalid brews removed!');
|
||||
this.getInvalid();
|
||||
})
|
||||
},
|
||||
render: function(){
|
||||
return <div className='invalidBrew'>
|
||||
<h1>Remove Invalid Brews</h1>
|
||||
<div>This will removes all brews older than 3 days and shorter than a tweet.</div>
|
||||
<button className='get' onClick={this.getInvalid}> Get Invalid Brews</button>
|
||||
<button className='remove' disabled={this.state.brews.length == 0} onClick={this.removeInvalid}> Remove invalid Brews</button>
|
||||
|
||||
<BrewTable brews={this.state.brews} />
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = InvalidBrew;
|
||||
@@ -1,5 +0,0 @@
|
||||
.invalidBrew{
|
||||
button{
|
||||
margin: 10px 4px;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,60 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const OldBrewRenderer = require('depricated/brewRendererOld/brewRendererOld.jsx');
|
||||
|
||||
const Markdown = require('homebrewery/markdown.js');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||
|
||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx')
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
|
||||
//TODO: move to the brew renderer
|
||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||
|
||||
const PAGE_HEIGHT = 1056;
|
||||
const PPR_THRESHOLD = 50;
|
||||
|
||||
const BrewRenderer = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
const BrewRenderer = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : '',
|
||||
style : ''
|
||||
},
|
||||
|
||||
|
||||
//TODO: maybe remove?
|
||||
text : '',
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
const pages = this.props.brew.text.split('\\page');
|
||||
getInitialState : function() {
|
||||
const pages = this.props.text.split('\\page');
|
||||
|
||||
return {
|
||||
viewablePageNumber: 0,
|
||||
height : 0,
|
||||
isMounted : false,
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD
|
||||
viewablePageNumber : 0,
|
||||
height : 0,
|
||||
isMounted : false,
|
||||
|
||||
usePPR : true,
|
||||
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD,
|
||||
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
height : 0,
|
||||
height : 0,
|
||||
pageHeight : PAGE_HEIGHT,
|
||||
lastRender : <div></div>,
|
||||
|
||||
componentDidMount: function() {
|
||||
componentDidMount : function() {
|
||||
this.updateSize();
|
||||
window.addEventListener('resize', this.updateSize);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.updateSize);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
componentWillReceiveProps : function(nextProps) {
|
||||
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
|
||||
|
||||
const pages = nextProps.brew.text.split('\\page');
|
||||
const pages = nextProps.text.split('\\page');
|
||||
this.setState({
|
||||
pages : pages,
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
updateSize : function() {
|
||||
@@ -65,9 +62,8 @@ const BrewRenderer = React.createClass({
|
||||
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
|
||||
}, 1);
|
||||
|
||||
|
||||
this.setState({
|
||||
height : this.refs.main.parentNode.clientHeight,
|
||||
height : this.refs.main.parentNode.clientHeight,
|
||||
isMounted : true
|
||||
});
|
||||
},
|
||||
@@ -81,10 +77,14 @@ const BrewRenderer = React.createClass({
|
||||
shouldRender : function(pageText, index){
|
||||
if(!this.state.isMounted) return false;
|
||||
|
||||
var viewIndex = this.state.viewablePageNumber;
|
||||
const viewIndex = this.state.viewablePageNumber;
|
||||
if(index == viewIndex - 3) return true;
|
||||
if(index == viewIndex - 2) return true;
|
||||
if(index == viewIndex - 1) return true;
|
||||
if(index == viewIndex) return true;
|
||||
if(index == viewIndex + 1) return true;
|
||||
if(index == viewIndex + 2) return true;
|
||||
if(index == viewIndex + 3) return true;
|
||||
|
||||
//Check for style tages
|
||||
if(pageText.indexOf('<style>') !== -1) return true;
|
||||
@@ -95,7 +95,7 @@ const BrewRenderer = React.createClass({
|
||||
renderPageInfo : function(){
|
||||
return <div className='pageInfo'>
|
||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderPPRmsg : function(){
|
||||
@@ -103,17 +103,17 @@ const BrewRenderer = React.createClass({
|
||||
|
||||
return <div className='ppr_msg'>
|
||||
Partial Page Renderer enabled, because your brew is so large. May effect rendering.
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderDummyPage : function(index){
|
||||
return <div className='phb v2' id={`p${index + 1}`} key={index}>
|
||||
return <div className='phb' id={`p${index + 1}`} key={index}>
|
||||
<i className='fa fa-spinner fa-spin' />
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderPage : function(pageText, index){
|
||||
return <div className='phb v2' id={`p${index + 1}`} dangerouslySetInnerHTML={{__html:Markdown.render(pageText)}} key={index} />
|
||||
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
@@ -121,7 +121,7 @@ const BrewRenderer = React.createClass({
|
||||
return _.map(this.state.pages, (page, index)=>{
|
||||
if(this.shouldRender(page, index)){
|
||||
return this.renderPage(page, index);
|
||||
}else{
|
||||
} else {
|
||||
return this.renderDummyPage(index);
|
||||
}
|
||||
});
|
||||
@@ -133,32 +133,21 @@ const BrewRenderer = React.createClass({
|
||||
return this.lastRender;
|
||||
},
|
||||
|
||||
//TODO: This is pretty bad
|
||||
renderStyle : function(){
|
||||
return <style>{this.props.brew.style.replace(/;/g, ' !important;')}</style>
|
||||
},
|
||||
|
||||
render : function(){
|
||||
if(this.props.brew.version == 1) return <OldBrewRenderer value={this.props.brew.text} />;
|
||||
|
||||
|
||||
return <div className='brewRenderer'
|
||||
onScroll={this.handleScroll}
|
||||
ref='main'
|
||||
style={{height : this.state.height}}>
|
||||
style={{ height: this.state.height }}>
|
||||
|
||||
<ErrorBar errors={this.props.errors} />
|
||||
<RenderWarnings />
|
||||
|
||||
|
||||
{this.renderStyle()}
|
||||
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
{this.renderPageInfo()}
|
||||
{this.renderPPRmsg()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
@import (less) './shared/homebrewery/phb_style/phb.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.style.less';
|
||||
.pane{
|
||||
position : relative;
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
var ErrorBar = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
const ErrorBar = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
|
||||
hasOpenError : false,
|
||||
hasOpenError : false,
|
||||
hasCloseError : false,
|
||||
hasMatchError : false,
|
||||
|
||||
@@ -19,20 +20,20 @@ var ErrorBar = React.createClass({
|
||||
this.hasMatchError = false;
|
||||
|
||||
|
||||
var errors = _.map(this.props.errors, (err, idx) => {
|
||||
const errors = _.map(this.props.errors, (err, idx)=>{
|
||||
if(err.id == 'OPEN') this.hasOpenError = true;
|
||||
if(err.id == 'CLOSE') this.hasCloseError = true;
|
||||
if(err.id == 'MISMATCH') this.hasMatchError = true;
|
||||
return <li key={idx}>
|
||||
Line {err.line} : {err.text}, '{err.type}' tag
|
||||
</li>
|
||||
</li>;
|
||||
});
|
||||
|
||||
return <ul>{errors}</ul>
|
||||
return <ul>{errors}</ul>;
|
||||
},
|
||||
|
||||
renderProtip : function(){
|
||||
var msg = [];
|
||||
const msg = [];
|
||||
if(this.hasOpenError){
|
||||
msg.push(<div>
|
||||
An unmatched opening tag means there's an opened tag that isn't closed, you need to close a tag, like this {'</div>'}. Make sure to match types!
|
||||
@@ -53,7 +54,7 @@ var ErrorBar = React.createClass({
|
||||
return <div className='protips'>
|
||||
<h4>Protips!</h4>
|
||||
{msg}
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
@@ -66,7 +67,7 @@ var ErrorBar = React.createClass({
|
||||
{this.renderErrors()}
|
||||
<hr />
|
||||
{this.renderProtip()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
146
client/homebrew/editor/editor.jsx
Normal file
146
client/homebrew/editor/editor.jsx
Normal file
@@ -0,0 +1,146 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||
|
||||
|
||||
const splice = function(str, index, inject){
|
||||
return str.slice(0, index) + inject + str.slice(index);
|
||||
};
|
||||
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
|
||||
const Editor = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
value : '',
|
||||
onChange : ()=>{},
|
||||
|
||||
metadata : {},
|
||||
onMetadataChange : ()=>{},
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
showMetadataEditor : false
|
||||
};
|
||||
},
|
||||
cursorPosition : {
|
||||
line : 0,
|
||||
ch : 0
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
this.updateEditorSize();
|
||||
this.highlightPageLines();
|
||||
window.addEventListener('resize', this.updateEditorSize);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.removeEventListener('resize', this.updateEditorSize);
|
||||
},
|
||||
|
||||
updateEditorSize : function() {
|
||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.props.onChange(text);
|
||||
},
|
||||
handleCursorActivty : function(curpos){
|
||||
this.cursorPosition = curpos;
|
||||
},
|
||||
handleInject : function(injectText){
|
||||
const lines = this.props.value.split('\n');
|
||||
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
|
||||
|
||||
this.handleTextChange(lines.join('\n'));
|
||||
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
|
||||
},
|
||||
handgleToggle : function(){
|
||||
this.setState({
|
||||
showMetadataEditor : !this.state.showMetadataEditor
|
||||
});
|
||||
},
|
||||
|
||||
getCurrentPage : function(){
|
||||
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1);
|
||||
return _.reduce(lines, (r, line)=>{
|
||||
if(line.indexOf('\\page') !== -1) r++;
|
||||
return r;
|
||||
}, 1);
|
||||
},
|
||||
|
||||
highlightPageLines : function(){
|
||||
if(!this.refs.codeEditor) return;
|
||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||
|
||||
const lineNumbers = _.reduce(this.props.value.split('\n'), (r, line, lineNumber)=>{
|
||||
if(line.indexOf('\\page') !== -1){
|
||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||
r.push(lineNumber);
|
||||
}
|
||||
return r;
|
||||
}, []);
|
||||
return lineNumbers;
|
||||
},
|
||||
|
||||
|
||||
brewJump : function(){
|
||||
const currentPage = this.getCurrentPage();
|
||||
window.location.hash = `p${currentPage}`;
|
||||
},
|
||||
|
||||
//Called when there are changes to the editor's dimensions
|
||||
update : function(){
|
||||
this.refs.codeEditor.updateSize();
|
||||
},
|
||||
|
||||
renderMetadataEditor : function(){
|
||||
if(!this.state.showMetadataEditor) return;
|
||||
return <MetadataEditor
|
||||
metadata={this.props.metadata}
|
||||
onChange={this.props.onMetadataChange}
|
||||
/>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
this.highlightPageLines();
|
||||
return (
|
||||
<div className='editor' ref='main'>
|
||||
<SnippetBar
|
||||
brew={this.props.value}
|
||||
onInject={this.handleInject}
|
||||
onToggle={this.handgleToggle}
|
||||
showmeta={this.state.showMetadataEditor} />
|
||||
{this.renderMetadataEditor()}
|
||||
<CodeEditor
|
||||
ref='codeEditor'
|
||||
wrap={true}
|
||||
language='gfm'
|
||||
value={this.props.value}
|
||||
onChange={this.handleTextChange}
|
||||
onCursorActivity={this.handleCursorActivty} />
|
||||
|
||||
{/*
|
||||
<div className='brewJump' onClick={this.brewJump}>
|
||||
<i className='fa fa-arrow-right' />
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Editor;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
|
||||
.brewEditor{
|
||||
.editor{
|
||||
position : relative;
|
||||
width : 100%;
|
||||
|
||||
.codeEditor{
|
||||
height : 100%;
|
||||
.pageLine{
|
||||
background-color : fade(#333, 15%);
|
||||
border-bottom : #333 solid 1px;
|
||||
}
|
||||
.block{
|
||||
color : purple;
|
||||
//font-style: italic;
|
||||
}
|
||||
.columnSplit{
|
||||
font-style : italic;
|
||||
color : grey;
|
||||
}
|
||||
}
|
||||
|
||||
.brewJump{
|
||||
@@ -32,4 +25,5 @@
|
||||
justify-content:center;
|
||||
.tooltipLeft("Jump to brew page");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
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 = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
const MetadataEditor = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
metadata: {
|
||||
editId : null,
|
||||
title : '',
|
||||
metadata : {
|
||||
editId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
},
|
||||
onChange : ()=>{}
|
||||
};
|
||||
@@ -24,12 +25,12 @@ const MetadataEditor = React.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);
|
||||
@@ -41,10 +42,10 @@ const MetadataEditor = React.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 = '/';
|
||||
@@ -67,22 +68,21 @@ const MetadataEditor = React.createClass({
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={_.includes(this.props.metadata.systems, val)}
|
||||
onChange={this.handleSystem.bind(null, val)} />
|
||||
onChange={()=>this.handleSystem(val)} />
|
||||
{val}
|
||||
</label>
|
||||
</label>;
|
||||
});
|
||||
},
|
||||
|
||||
renderPublish : function(){
|
||||
//TODO: Move the publish element into here
|
||||
if(this.props.metadata.published){
|
||||
return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}>
|
||||
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
|
||||
<i className='fa fa-ban' /> unpublish
|
||||
</button>
|
||||
}else{
|
||||
return <button className='publish' onClick={this.handlePublish.bind(null, true)}>
|
||||
</button>;
|
||||
} else {
|
||||
return <button className='publish' onClick={()=>this.handlePublish(true)}>
|
||||
<i className='fa fa-globe' /> publish
|
||||
</button>
|
||||
</button>;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -96,7 +96,7 @@ const MetadataEditor = React.createClass({
|
||||
<i className='fa fa-trash' /> delete brew
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderAuthors : function(){
|
||||
@@ -109,7 +109,7 @@ const MetadataEditor = React.createClass({
|
||||
<div className='value'>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderShareToReddit : function(){
|
||||
@@ -124,7 +124,7 @@ const MetadataEditor = React.createClass({
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
@@ -133,19 +133,20 @@ const MetadataEditor = React.createClass({
|
||||
<label>title</label>
|
||||
<input type='text' className='value'
|
||||
value={this.props.metadata.title}
|
||||
onChange={this.handleFieldChange.bind(null, 'title')} />
|
||||
onChange={()=>this.handleFieldChange('title')} />
|
||||
</div>
|
||||
<div className='field description'>
|
||||
<label>description</label>
|
||||
<textarea value={this.props.metadata.description} className='value'
|
||||
onChange={this.handleFieldChange.bind(null, 'description')} />
|
||||
onChange={()=>this.handleFieldChange('description')} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text' className='value'
|
||||
value={this.props.metadata.thumbnail}
|
||||
onChange={this.handleFieldChange.bind(null, 'thumbnail')} />
|
||||
{/*}
|
||||
<div className='field tags'>
|
||||
<label>tags</label>
|
||||
<textarea value={this.props.metadata.tags}
|
||||
onChange={()=>this.handleFieldChange('tags')} />
|
||||
</div>
|
||||
*/}
|
||||
|
||||
<div className='field systems'>
|
||||
<label>systems</label>
|
||||
@@ -168,7 +169,7 @@ const MetadataEditor = React.createClass({
|
||||
|
||||
{this.renderDelete()}
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
.metadataEditor{
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 25px;
|
||||
// background-color : #999;
|
||||
background-color: white;
|
||||
background-color : #999;
|
||||
.field{
|
||||
display : flex;
|
||||
width : 100%;
|
||||
@@ -76,7 +76,4 @@
|
||||
font-size: 0.8em;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
.thumbnail.field{
|
||||
|
||||
}
|
||||
}
|
||||
95
client/homebrew/editor/snippetbar/snippetbar.jsx
Normal file
95
client/homebrew/editor/snippetbar/snippetbar.jsx
Normal file
@@ -0,0 +1,95 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
|
||||
const Snippets = require('./snippets/snippets.js');
|
||||
|
||||
const execute = function(val, brew){
|
||||
if(_.isFunction(val)) return val(brew);
|
||||
return val;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Snippetbar = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : '',
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showmeta : false
|
||||
};
|
||||
},
|
||||
|
||||
handleSnippetClick : function(injectedText){
|
||||
this.props.onInject(injectedText);
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
return _.map(Snippets, (snippetGroup)=>{
|
||||
return <SnippetGroup
|
||||
brew={this.props.brew}
|
||||
groupName={snippetGroup.groupName}
|
||||
icon={snippetGroup.icon}
|
||||
snippets={snippetGroup.snippets}
|
||||
key={snippetGroup.groupName}
|
||||
onSnippetClick={this.handleSnippetClick}
|
||||
/>;
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='snippetBar'>
|
||||
{this.renderSnippetGroups()}
|
||||
<div className={cx('toggleMeta', { selected: this.props.showmeta })}
|
||||
onClick={this.props.onToggle}>
|
||||
<i className='fa fa-bars' />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Snippetbar;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const SnippetGroup = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : '',
|
||||
groupName : '',
|
||||
icon : 'fa-rocket',
|
||||
snippets : [],
|
||||
onSnippetClick : function(){},
|
||||
};
|
||||
},
|
||||
handleSnippetClick : function(snippet){
|
||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
||||
},
|
||||
renderSnippets : function(){
|
||||
return _.map(this.props.snippets, (snippet)=>{
|
||||
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
|
||||
<i className={`fa fa-fw ${snippet.icon}`} />
|
||||
{snippet.name}
|
||||
</div>;
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='snippetGroup'>
|
||||
<div className='text'>
|
||||
<i className={`fa fa-fw ${this.props.icon}`} />
|
||||
<span className='groupName'>{this.props.groupName}</span>
|
||||
</div>
|
||||
<div className='dropdown'>
|
||||
{this.renderSnippets()}
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
});
|
||||
73
client/homebrew/editor/snippetbar/snippetbar.less
Normal file
73
client/homebrew/editor/snippetbar/snippetbar.less
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
.snippetBar{
|
||||
@height : 25px;
|
||||
position : relative;
|
||||
height : @height;
|
||||
background-color : #ddd;
|
||||
.toggleMeta{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
right : 0px;
|
||||
height : @height;
|
||||
width : @height;
|
||||
cursor : pointer;
|
||||
line-height : @height;
|
||||
text-align : center;
|
||||
.tooltipLeft("Edit Brew Metadata");
|
||||
&:hover, &.selected{
|
||||
background-color : #999;
|
||||
}
|
||||
}
|
||||
.snippetGroup{
|
||||
display : inline-block;
|
||||
height : @height;
|
||||
padding : 0px 5px;
|
||||
cursor : pointer;
|
||||
font-size : 0.6em;
|
||||
font-weight : 800;
|
||||
line-height : @height;
|
||||
text-transform : uppercase;
|
||||
border-right : 1px solid black;
|
||||
i{
|
||||
vertical-align : middle;
|
||||
margin-right : 3px;
|
||||
font-size : 1.2em;
|
||||
}
|
||||
&:hover, &.selected{
|
||||
background-color : #999;
|
||||
}
|
||||
.text{
|
||||
line-height : @height;
|
||||
.groupName{
|
||||
font-size : 10px;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
.dropdown{
|
||||
visibility : visible;
|
||||
}
|
||||
}
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
top : 100%;
|
||||
visibility : hidden;
|
||||
z-index : 1000;
|
||||
margin-left : -5px;
|
||||
padding : 0px;
|
||||
background-color : #ddd;
|
||||
.snippet{
|
||||
.animate(background-color);
|
||||
padding : 5px;
|
||||
cursor : pointer;
|
||||
font-size : 10px;
|
||||
i{
|
||||
margin-right : 8px;
|
||||
font-size : 13px;
|
||||
}
|
||||
&:hover{
|
||||
background-color : #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = function(classname){
|
||||
|
||||
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
|
||||
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']);
|
||||
|
||||
classname = classname.toLowerCase();
|
||||
|
||||
const hitDie = _.sample([4, 6, 8, 10, 12]);
|
||||
|
||||
const abilityList = ['Strength', 'Dexerity', 'Constitution', 'Wisdom', 'Charisma', 'Intelligence'];
|
||||
const skillList = ['Acrobatics ', 'Animal Handling', 'Arcana', 'Athletics', 'Deception', 'History', 'Insight', 'Intimidation', 'Investigation', 'Medicine', 'Nature', 'Perception', 'Performance', 'Persuasion', 'Religion', 'Sleight of Hand', 'Stealth', 'Survival'];
|
||||
|
||||
|
||||
return [
|
||||
'## Class Features',
|
||||
`As a ${classname}, you gain the following class features`,
|
||||
'#### Hit Points',
|
||||
'___',
|
||||
`- **Hit Dice:** 1d${hitDie} per ${classname} level`,
|
||||
`- **Hit Points at 1st Level:** ${hitDie} + your Constitution modifier`,
|
||||
`- **Hit Points at Higher Levels:** 1d${hitDie} (or ${hitDie/2 + 1}) + your Constitution modifier per ${classname} level after 1st`,
|
||||
'',
|
||||
'#### Proficiencies',
|
||||
'___',
|
||||
`- **Armor:** ${_.sampleSize(['Light armor', 'Medium armor', 'Heavy armor', 'Shields'], _.random(0, 3)).join(', ') || 'None'}`,
|
||||
`- **Weapons:** ${_.sampleSize(['Squeegee', 'Rubber Chicken', 'Simple weapons', 'Martial weapons'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||
`- **Tools:** ${_.sampleSize(['Artian\'s tools', 'one musical instrument', 'Thieve\'s tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||
'',
|
||||
'___',
|
||||
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
||||
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
||||
'',
|
||||
'#### Equipment',
|
||||
'You start with the following equipment, in addition to the equipment granted by your background:',
|
||||
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',
|
||||
'- *(a)* five javelins or *(b)* any simple melee weapon',
|
||||
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
|
||||
'\n\n\n'
|
||||
].join('\n');
|
||||
};
|
||||
114
client/homebrew/editor/snippetbar/snippets/classtable.gen.js
Normal file
114
client/homebrew/editor/snippetbar/snippets/classtable.gen.js
Normal file
@@ -0,0 +1,114 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const features = [
|
||||
'Astrological Botany',
|
||||
'Astrological Chemistry',
|
||||
'Biochemical Sorcery',
|
||||
'Civil Alchemy',
|
||||
'Consecrated Biochemistry',
|
||||
'Demonic Anthropology',
|
||||
'Divinatory Mineralogy',
|
||||
'Genetic Banishing',
|
||||
'Hermetic Geography',
|
||||
'Immunological Incantations',
|
||||
'Nuclear Illusionism',
|
||||
'Ritual Astronomy',
|
||||
'Seismological Divination',
|
||||
'Spiritual Biochemistry',
|
||||
'Statistical Occultism',
|
||||
'Police Necromancer',
|
||||
'Sixgun Poisoner',
|
||||
'Pharmaceutical Gunslinger',
|
||||
'Infernal Banker',
|
||||
'Spell Analyst',
|
||||
'Gunslinger Corruptor',
|
||||
'Torque Interfacer',
|
||||
'Exo Interfacer',
|
||||
'Gunpowder Torturer',
|
||||
'Orbital Gravedigger',
|
||||
'Phased Linguist',
|
||||
'Mathematical Pharmacist',
|
||||
'Plasma Outlaw',
|
||||
'Malefic Chemist',
|
||||
'Police Cultist'
|
||||
];
|
||||
|
||||
const classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
|
||||
|
||||
const levels = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th', '10th', '11th', '12th', '13th', '14th', '15th', '16th', '17th', '18th', '19th', '20th'];
|
||||
|
||||
const profBonus = [2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6];
|
||||
|
||||
const getFeature = (level)=>{
|
||||
let res = [];
|
||||
if(_.includes([4, 6, 8, 12, 14, 16, 19], level+1)){
|
||||
res = ['Ability Score Improvement'];
|
||||
}
|
||||
res = _.union(res, _.sampleSize(features, _.sample([0, 1, 1, 1, 1, 1])));
|
||||
if(!res.length) return '─';
|
||||
return res.join(', ');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
full : function(){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
const maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
||||
const drawSlots = function(Slots){
|
||||
let slots = Number(Slots);
|
||||
return _.times(9, function(i){
|
||||
const max = maxes[i];
|
||||
if(slots < 1) return '—';
|
||||
const res = _.min([max, slots]);
|
||||
slots -= res;
|
||||
return res;
|
||||
}).join(' | ');
|
||||
};
|
||||
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `<div class='classTable wide'>\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n`+
|
||||
`|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
levelName,
|
||||
`+${profBonus[level]}`,
|
||||
getFeature(level),
|
||||
cantrips,
|
||||
spells,
|
||||
drawSlots(slots)
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 2);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n</div>\n\n`;
|
||||
},
|
||||
|
||||
half : function(){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let featureScore = 1;
|
||||
return `<div class='classTable'>\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | ${_.sample(features)}|\n` +
|
||||
`|:---:|:---:|:---|:---:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
levelName,
|
||||
`+${profBonus[level]}`,
|
||||
getFeature(level),
|
||||
`+${featureScore}`
|
||||
].join(' | ');
|
||||
|
||||
featureScore += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n</div>\n\n`;
|
||||
}
|
||||
};
|
||||
117
client/homebrew/editor/snippetbar/snippets/coverpage.gen.js
Normal file
117
client/homebrew/editor/snippetbar/snippets/coverpage.gen.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const titles = [
|
||||
'The Burning Gallows',
|
||||
'The Ring of Nenlast',
|
||||
'Below the Blind Tavern',
|
||||
'Below the Hungering River',
|
||||
'Before Bahamut\'s Land',
|
||||
'The Cruel Grave from Within',
|
||||
'The Strength of Trade Road',
|
||||
'Through The Raven Queen\'s Worlds',
|
||||
'Within the Settlement',
|
||||
'The Crown from Within',
|
||||
'The Merchant Within the Battlefield',
|
||||
'Ioun\'s Fading Traveler',
|
||||
'The Legion Ingredient',
|
||||
'The Explorer Lure',
|
||||
'Before the Charming Badlands',
|
||||
'The Living Dead Above the Fearful Cage',
|
||||
'Vecna\'s Hidden Sage',
|
||||
'Bahamut\'s Demonspawn',
|
||||
'Across Gruumsh\'s Elemental Chaos',
|
||||
'The Blade of Orcus',
|
||||
'Beyond Revenge',
|
||||
'Brain of Insanity',
|
||||
'Breed Battle!, A New Beginning',
|
||||
'Evil Lake, A New Beginning',
|
||||
'Invasion of the Gigantic Cat, Part II',
|
||||
'Kraken War 2020',
|
||||
'The Body Whisperers',
|
||||
'The Diabolical Tales of the Ape-Women',
|
||||
'The Doctor Immortal',
|
||||
'The Doctor from Heaven',
|
||||
'The Graveyard',
|
||||
'Azure Core',
|
||||
'Core Battle',
|
||||
'Core of Heaven: The Guardian of Amazement',
|
||||
'Deadly Amazement III',
|
||||
'Dry Chaos IX',
|
||||
'Gate Thunder',
|
||||
'Guardian: Skies of the Dark Wizard',
|
||||
'Lute of Eternity',
|
||||
'Mercury\'s Planet: Brave Evolution',
|
||||
'Ruby of Atlantis: The Quake of Peace',
|
||||
'Sky of Zelda: The Thunder of Force',
|
||||
'Vyse\'s Skies',
|
||||
'White Greatness III',
|
||||
'Yellow Divinity',
|
||||
'Zidane\'s Ghost'
|
||||
];
|
||||
|
||||
const subtitles = [
|
||||
'In an ominous universe, a botanist opposes terrorism.',
|
||||
'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.',
|
||||
'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.',
|
||||
'In an evil empire of horror, two rangers battle the forces of hell.',
|
||||
'In a lost city, in an age of sorcery, a librarian quests for revenge.',
|
||||
'In a universe of illusions and danger, three time travellers and an adventurer search for justice.',
|
||||
'In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.',
|
||||
'In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.',
|
||||
'In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.',
|
||||
'In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.',
|
||||
'In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.',
|
||||
'In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.',
|
||||
'In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.',
|
||||
'In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.',
|
||||
'In a kingdom of deception, a reporter searches for fame.',
|
||||
'In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.',
|
||||
'In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.',
|
||||
'In a dark city of confusion, three swordswomen and a singer battle lawlessness.',
|
||||
'In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.',
|
||||
'In a kingdom of panic, six adventurers oppose lawlessness.',
|
||||
'In a land of dreams and hopelessness, three hackers and a cyborg search for justice.',
|
||||
'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.',
|
||||
'In a wicked universe, five seers fight lawlessness.',
|
||||
'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.',
|
||||
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.',
|
||||
'In a cursed empire, five inventors oppose terrorism.',
|
||||
'On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.',
|
||||
'In a forgotten land, a reporter and a spy try to stop the apocalypse.',
|
||||
'In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.',
|
||||
'On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.',
|
||||
'In a galaxy of dark magic, four fighters seek freedom.',
|
||||
'In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.',
|
||||
'In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.',
|
||||
'In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.',
|
||||
'In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.',
|
||||
'On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.',
|
||||
'In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.',
|
||||
'In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.',
|
||||
'In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.',
|
||||
'In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.',
|
||||
'In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.',
|
||||
'In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.',
|
||||
'In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.',
|
||||
'In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.',
|
||||
'In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime.'
|
||||
];
|
||||
|
||||
|
||||
module.exports = ()=>{
|
||||
return `<style>
|
||||
.phb#p1{ text-align:center; }
|
||||
.phb#p1:after{ display:none; }
|
||||
</style>
|
||||
|
||||
<div style='margin-top:450px;'></div>
|
||||
|
||||
# ${_.sample(titles)}
|
||||
|
||||
<div style='margin-top:25px'></div>
|
||||
<div class='wide'>
|
||||
##### ${_.sample(subtitles)}
|
||||
</div>
|
||||
|
||||
\\page`;
|
||||
};
|
||||
43
client/homebrew/editor/snippetbar/snippets/fullclass.gen.js
Normal file
43
client/homebrew/editor/snippetbar/snippets/fullclass.gen.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||
|
||||
const ClassTableGen = require('./classtable.gen.js');
|
||||
|
||||
module.exports = function(){
|
||||
|
||||
const classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge']);
|
||||
|
||||
|
||||
const image = _.sample(_.map([
|
||||
'http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png',
|
||||
'http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png',
|
||||
'http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png',
|
||||
'http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png',
|
||||
'http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png',
|
||||
'http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png',
|
||||
'http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png',
|
||||
'http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png',
|
||||
], function(url){
|
||||
return `<img src = '${url}' style='max-width:8cm;max-height:25cm' />`;
|
||||
}));
|
||||
|
||||
|
||||
return `${[
|
||||
image,
|
||||
'',
|
||||
'```',
|
||||
'```',
|
||||
'<div style=\'margin-top:240px\'></div>\n\n',
|
||||
`## ${classname}`,
|
||||
'Cool intro stuff will go here',
|
||||
|
||||
'\\page',
|
||||
ClassTableGen(classname),
|
||||
ClassFeatureGen(classname),
|
||||
|
||||
|
||||
|
||||
].join('\n')}\n\n\n`;
|
||||
};
|
||||
91
client/homebrew/editor/snippetbar/snippets/magic.gen.js
Normal file
91
client/homebrew/editor/snippetbar/snippets/magic.gen.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const spellNames = [
|
||||
'Astral Rite of Acne',
|
||||
'Create Acne',
|
||||
'Cursed Ramen Erruption',
|
||||
'Dark Chant of the Dentists',
|
||||
'Erruption of Immaturity',
|
||||
'Flaming Disc of Inconvenience',
|
||||
'Heal Bad Hygene',
|
||||
'Heavenly Transfiguration of the Cream Devil',
|
||||
'Hellish Cage of Mucus',
|
||||
'Irritate Peanut Butter Fairy',
|
||||
'Luminous Erruption of Tea',
|
||||
'Mystic Spell of the Poser',
|
||||
'Sorcerous Enchantment of the Chimneysweep',
|
||||
'Steak Sauce Ray',
|
||||
'Talk to Groupie',
|
||||
'Astonishing Chant of Chocolate',
|
||||
'Astounding Pasta Puddle',
|
||||
'Ball of Annoyance',
|
||||
'Cage of Yarn',
|
||||
'Control Noodles Elemental',
|
||||
'Create Nervousness',
|
||||
'Cure Baldness',
|
||||
'Cursed Ritual of Bad Hair',
|
||||
'Dispell Piles in Dentist',
|
||||
'Eliminate Florists',
|
||||
'Illusionary Transfiguration of the Babysitter',
|
||||
'Necromantic Armor of Salad Dressing',
|
||||
'Occult Transfiguration of Foot Fetish',
|
||||
'Protection from Mucus Giant',
|
||||
'Tinsel Blast',
|
||||
'Alchemical Evocation of the Goths',
|
||||
'Call Fangirl',
|
||||
'Divine Spell of Crossdressing',
|
||||
'Dominate Ramen Giant',
|
||||
'Eliminate Vindictiveness in Gym Teacher',
|
||||
'Extra-Planar Spell of Irritation',
|
||||
'Induce Whining in Babysitter',
|
||||
'Invoke Complaining',
|
||||
'Magical Enchantment of Arrogance',
|
||||
'Occult Globe of Salad Dressing',
|
||||
'Overwhelming Enchantment of the Chocolate Fairy',
|
||||
'Sorcerous Dandruff Globe',
|
||||
'Spiritual Invocation of the Costumers',
|
||||
'Ultimate Rite of the Confetti Angel',
|
||||
'Ultimate Ritual of Mouthwash',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
spellList : function(){
|
||||
const levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
||||
|
||||
const content = _.map(levels, (level)=>{
|
||||
const spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
||||
return `- ${spell}`;
|
||||
}).join('\n');
|
||||
return `##### ${level} \n${spells} \n`;
|
||||
}).join('\n');
|
||||
|
||||
return `<div class='spellList'>\n${content}\n</div>`;
|
||||
},
|
||||
|
||||
spell : function(){
|
||||
const level = ['1st', '2nd', '3rd', '4th', '5th', '6th', '7th', '8th', '9th'];
|
||||
const spellSchools = ['abjuration', 'conjuration', 'divination', 'enchantment', 'evocation', 'illusion', 'necromancy', 'transmutation'];
|
||||
|
||||
|
||||
let components = _.sampleSize(['V', 'S', 'M'], _.random(1, 3)).join(', ');
|
||||
if(components.indexOf('M') !== -1){
|
||||
components += ` (${_.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1, 3)).join(', ')})`;
|
||||
}
|
||||
|
||||
return [
|
||||
`#### ${_.sample(spellNames)}`,
|
||||
`*${_.sample(level)}-level ${_.sample(spellSchools)}*`,
|
||||
'___',
|
||||
'- **Casting Time:** 1 action',
|
||||
`- **Range:** ${_.sample(['Self', 'Touch', '30 feet', '60 feet'])}`,
|
||||
`- **Components:** ${components}`,
|
||||
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
|
||||
'',
|
||||
'A flame, equivalent in brightness to a torch, springs from from an object that you touch. ',
|
||||
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
|
||||
'A *continual flame* can be covered or hidden but not smothered or quenched.',
|
||||
'\n\n\n'
|
||||
].join('\n');
|
||||
}
|
||||
};
|
||||
196
client/homebrew/editor/snippetbar/snippets/monsterblock.gen.js
Normal file
196
client/homebrew/editor/snippetbar/snippets/monsterblock.gen.js
Normal file
@@ -0,0 +1,196 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const genList = function(list, max){
|
||||
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
|
||||
};
|
||||
|
||||
const getMonsterName = function(){
|
||||
return _.sample([
|
||||
'All-devouring Baseball Imp',
|
||||
'All-devouring Gumdrop Wraith',
|
||||
'Chocolate Hydra',
|
||||
'Devouring Peacock',
|
||||
'Economy-sized Colossus of the Lemonade Stand',
|
||||
'Ghost Pigeon',
|
||||
'Gibbering Duck',
|
||||
'Sparklemuffin Peacock Spider',
|
||||
'Gum Elemental',
|
||||
'Illiterate Construct of the Candy Store',
|
||||
'Ineffable Chihuahua',
|
||||
'Irritating Death Hamster',
|
||||
'Irritating Gold Mouse',
|
||||
'Juggernaut Snail',
|
||||
'Juggernaut of the Sock Drawer',
|
||||
'Koala of the Cosmos',
|
||||
'Mad Koala of the West',
|
||||
'Milk Djinni of the Lemonade Stand',
|
||||
'Mind Ferret',
|
||||
'Mystic Salt Spider',
|
||||
'Necrotic Halitosis Angel',
|
||||
'Pinstriped Famine Sheep',
|
||||
'Ritalin Leech',
|
||||
'Shocker Kangaroo',
|
||||
'Stellar Tennis Juggernaut',
|
||||
'Wailing Quail of the Sun',
|
||||
'Angel Pigeon',
|
||||
'Anime Sphinx',
|
||||
'Bored Avalanche Sheep of the Wasteland',
|
||||
'Devouring Nougat Sphinx of the Sock Drawer',
|
||||
'Djinni of the Footlocker',
|
||||
'Ectoplasmic Jazz Devil',
|
||||
'Flatuent Angel',
|
||||
'Gelatinous Duck of the Dream-Lands',
|
||||
'Gelatinous Mouse',
|
||||
'Golem of the Footlocker',
|
||||
'Lich Wombat',
|
||||
'Mechanical Sloth of the Past',
|
||||
'Milkshake Succubus',
|
||||
'Puffy Bone Peacock of the East',
|
||||
'Rainbow Manatee',
|
||||
'Rune Parrot',
|
||||
'Sand Cow',
|
||||
'Sinister Vanilla Dragon',
|
||||
'Snail of the North',
|
||||
'Spider of the Sewer',
|
||||
'Stellar Sawdust Leech',
|
||||
'Storm Anteater of Hell',
|
||||
'Stupid Spirit of the Brewery',
|
||||
'Time Kangaroo',
|
||||
'Tomb Poodle',
|
||||
]);
|
||||
};
|
||||
|
||||
const getType = function(){
|
||||
return `${_.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'])} ${_.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])}`;
|
||||
};
|
||||
|
||||
const getAlignment = function(){
|
||||
return _.sample([
|
||||
'annoying evil',
|
||||
'chaotic gossipy',
|
||||
'chaotic sloppy',
|
||||
'depressed neutral',
|
||||
'lawful bogus',
|
||||
'lawful coy',
|
||||
'manic-depressive evil',
|
||||
'narrow-minded neutral',
|
||||
'neutral annoying',
|
||||
'neutral ignorant',
|
||||
'oedpipal neutral',
|
||||
'silly neutral',
|
||||
'unoriginal neutral',
|
||||
'weird neutral',
|
||||
'wordy evil',
|
||||
'unaligned'
|
||||
]);
|
||||
};
|
||||
|
||||
const getStats = function(){
|
||||
return `>|${_.times(6, function(){
|
||||
const num = _.random(1, 20);
|
||||
const mod = Math.ceil(num/2 - 5);
|
||||
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
|
||||
}).join('|')}|`;
|
||||
};
|
||||
|
||||
const genAbilities = function(){
|
||||
return _.sample([
|
||||
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
|
||||
'> ***False Appearance. *** While the armor reamin motionless, it is indistinguishable from a normal suit of armor.',
|
||||
]);
|
||||
};
|
||||
|
||||
const genAction = function(){
|
||||
const name = _.sample([
|
||||
'Abdominal Drop',
|
||||
'Airplane Hammer',
|
||||
'Atomic Death Throw',
|
||||
'Bulldog Rake',
|
||||
'Corkscrew Strike',
|
||||
'Crossed Splash',
|
||||
'Crossface Suplex',
|
||||
'DDT Powerbomb',
|
||||
'Dual Cobra Wristlock',
|
||||
'Dual Throw',
|
||||
'Elbow Hold',
|
||||
'Gory Body Sweep',
|
||||
'Heel Jawbreaker',
|
||||
'Jumping Driver',
|
||||
'Open Chin Choke',
|
||||
'Scorpion Flurry',
|
||||
'Somersault Stump Fists',
|
||||
'Suffering Wringer',
|
||||
'Super Hip Submission',
|
||||
'Super Spin',
|
||||
'Team Elbow',
|
||||
'Team Foot',
|
||||
'Tilt-a-whirl Chin Sleeper',
|
||||
'Tilt-a-whirl Eye Takedown',
|
||||
'Turnbuckle Roll'
|
||||
]);
|
||||
|
||||
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
full : function(){
|
||||
return `${[
|
||||
'___',
|
||||
'___',
|
||||
`> ## ${getMonsterName()}`,
|
||||
`>*${getType()}, ${getAlignment()}*`,
|
||||
'> ___',
|
||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||
'>___',
|
||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||
getStats(),
|
||||
'>___',
|
||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||
'> ___',
|
||||
_.times(_.random(3, 6), function(){
|
||||
return genAbilities();
|
||||
}).join('\n>\n'),
|
||||
'> ### Actions',
|
||||
_.times(_.random(4, 6), function(){
|
||||
return genAction();
|
||||
}).join('\n>\n'),
|
||||
].join('\n')}\n\n\n`;
|
||||
},
|
||||
|
||||
half : function(){
|
||||
return `${[
|
||||
'___',
|
||||
`> ## ${getMonsterName()}`,
|
||||
`>*${getType()}, ${getAlignment()}*`,
|
||||
'> ___',
|
||||
`> - **Armor Class** ${_.random(10, 20)}`,
|
||||
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
|
||||
`> - **Speed** ${_.random(0, 50)}ft.`,
|
||||
'>___',
|
||||
'>|STR|DEX|CON|INT|WIS|CHA|',
|
||||
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
|
||||
getStats(),
|
||||
'>___',
|
||||
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
|
||||
`> - **Senses** passive Perception ${_.random(3, 20)}`,
|
||||
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
|
||||
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
|
||||
'> ___',
|
||||
_.times(_.random(0, 2), function(){
|
||||
return genAbilities();
|
||||
}).join('\n>\n'),
|
||||
'> ### Actions',
|
||||
_.times(_.random(1, 2), function(){
|
||||
return genAction();
|
||||
}).join('\n>\n'),
|
||||
].join('\n')}\n\n\n`;
|
||||
}
|
||||
};
|
||||
268
client/homebrew/editor/snippetbar/snippets/snippets.js
Normal file
268
client/homebrew/editor/snippetbar/snippets/snippets.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
const MagicGen = require('./magic.gen.js');
|
||||
const ClassTableGen = require('./classtable.gen.js');
|
||||
const MonsterBlockGen = require('./monsterblock.gen.js');
|
||||
const ClassFeatureGen = require('./classfeature.gen.js');
|
||||
const CoverPageGen = require('./coverpage.gen.js');
|
||||
const TableOfContentsGen = require('./tableOfContents.gen.js');
|
||||
|
||||
|
||||
module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'Editor',
|
||||
icon : 'fa-pencil',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Column Break',
|
||||
icon : 'fa-columns',
|
||||
gen : '```\n```\n\n'
|
||||
},
|
||||
{
|
||||
name : 'New Page',
|
||||
icon : 'fa-file-text',
|
||||
gen : '\\page\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Vertical Spacing',
|
||||
icon : 'fa-arrows-v',
|
||||
gen : '<div style=\'margin-top:140px\'></div>\n\n'
|
||||
},
|
||||
{
|
||||
name : 'Wide Block',
|
||||
icon : 'fa-arrows-h',
|
||||
gen : '<div class=\'wide\'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n'
|
||||
},
|
||||
{
|
||||
name : 'Image',
|
||||
icon : 'fa-image',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg\' ',
|
||||
' style=\'width:325px\' />',
|
||||
'Credit: Kyounghwan Kim'
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fa-tree',
|
||||
gen : [
|
||||
'<img ',
|
||||
' src=\'http://i.imgur.com/hMna6G0.png\' ',
|
||||
' style=\'position:absolute; top:50px; right:30px; width:280px\' />'
|
||||
].join('\n')
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Page Number',
|
||||
icon : 'fa-bookmark',
|
||||
gen : '<div class=\'pageNumber\'>1</div>\n<div class=\'footnote\'>PART 1 | FANCINESS</div>\n\n'
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Auto-incrementing Page Number',
|
||||
icon : 'fa-sort-numeric-asc',
|
||||
gen : '<div class=\'pageNumber auto\'></div>\n'
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Link to page',
|
||||
icon : 'fa-link',
|
||||
gen : '[Click here](#p3) to go to page 3\n'
|
||||
},
|
||||
|
||||
{
|
||||
name : 'Table of Contents',
|
||||
icon : 'fa-book',
|
||||
gen : TableOfContentsGen
|
||||
},
|
||||
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fa-book',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Spell',
|
||||
icon : 'fa-magic',
|
||||
gen : MagicGen.spell,
|
||||
},
|
||||
{
|
||||
name : 'Spell List',
|
||||
icon : 'fa-list',
|
||||
gen : MagicGen.spellList,
|
||||
},
|
||||
{
|
||||
name : 'Class Feature',
|
||||
icon : 'fa-trophy',
|
||||
gen : ClassFeatureGen,
|
||||
},
|
||||
{
|
||||
name : 'Note',
|
||||
icon : 'fa-sticky-note',
|
||||
gen : function(){
|
||||
return [
|
||||
'> ##### Time to Drop Knowledge',
|
||||
'> Use notes to point out some interesting information. ',
|
||||
'> ',
|
||||
'> **Tables and lists** both work within a note.'
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Descriptive Text Box',
|
||||
icon : 'fa-sticky-note-o',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div class=\'descriptive\'>',
|
||||
'##### Time to Drop Knowledge',
|
||||
'Use notes to point out some interesting information. ',
|
||||
'',
|
||||
'**Tables and lists** both work within a note.',
|
||||
'</div>'
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Monster Stat Block',
|
||||
icon : 'fa-bug',
|
||||
gen : MonsterBlockGen.half,
|
||||
},
|
||||
{
|
||||
name : 'Wide Monster Stat Block',
|
||||
icon : 'fa-paw',
|
||||
gen : MonsterBlockGen.full,
|
||||
},
|
||||
{
|
||||
name : 'Cover Page',
|
||||
icon : 'fa-file-word-o',
|
||||
gen : CoverPageGen,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
/********************* TABLES *********************/
|
||||
|
||||
{
|
||||
groupName : 'Tables',
|
||||
icon : 'fa-table',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fa-table',
|
||||
gen : ClassTableGen.full,
|
||||
},
|
||||
{
|
||||
name : 'Half Class Table',
|
||||
icon : 'fa-list-alt',
|
||||
gen : ClassTableGen.half,
|
||||
},
|
||||
{
|
||||
name : 'Table',
|
||||
icon : 'fa-th-list',
|
||||
gen : function(){
|
||||
return [
|
||||
'##### Cookie Tastiness',
|
||||
'| Tastiness | Cookie Type |',
|
||||
'|:----:|:-------------|',
|
||||
'| -5 | Raisin |',
|
||||
'| 8th | Chocolate Chip |',
|
||||
'| 11th | 2 or lower |',
|
||||
'| 14th | 3 or lower |',
|
||||
'| 17th | 4 or lower |\n\n',
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Wide Table',
|
||||
icon : 'fa-list',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div class=\'wide\'>',
|
||||
'##### Cookie Tastiness',
|
||||
'| Tastiness | Cookie Type |',
|
||||
'|:----:|:-------------|',
|
||||
'| -5 | Raisin |',
|
||||
'| 8th | Chocolate Chip |',
|
||||
'| 11th | 2 or lower |',
|
||||
'| 14th | 3 or lower |',
|
||||
'| 17th | 4 or lower |',
|
||||
'</div>\n\n'
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
name : 'Split Table',
|
||||
icon : 'fa-th-large',
|
||||
gen : function(){
|
||||
return [
|
||||
'<div style=\'column-count:2\'>',
|
||||
'| d10 | Damage Type |',
|
||||
'|:---:|:------------|',
|
||||
'| 1 | Acid |',
|
||||
'| 2 | Cold |',
|
||||
'| 3 | Fire |',
|
||||
'| 4 | Force |',
|
||||
'| 5 | Lightning |',
|
||||
'',
|
||||
'```',
|
||||
'```',
|
||||
'',
|
||||
'| d10 | Damage Type |',
|
||||
'|:---:|:------------|',
|
||||
'| 6 | Necrotic |',
|
||||
'| 7 | Poison |',
|
||||
'| 8 | Psychic |',
|
||||
'| 9 | Radiant |',
|
||||
'| 10 | Thunder |',
|
||||
'</div>\n\n',
|
||||
].join('\n');
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** PRINT *************/
|
||||
|
||||
{
|
||||
groupName : 'Print',
|
||||
icon : 'fa-print',
|
||||
snippets : [
|
||||
{
|
||||
name : 'A4 PageSize',
|
||||
icon : 'fa-file-o',
|
||||
gen : ['<style>',
|
||||
' .phb{',
|
||||
' width : 210mm;',
|
||||
' height : 296.8mm;',
|
||||
' }',
|
||||
'</style>'
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Ink Friendly',
|
||||
icon : 'fa-tint',
|
||||
gen : ['<style>',
|
||||
' .phb{ background : white;}',
|
||||
' .phb img{ display : none;}',
|
||||
' .phb hr+blockquote{background : white;}',
|
||||
'</style>',
|
||||
''
|
||||
].join('\n')
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
];
|
||||
@@ -1,38 +1,38 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const getTOC = (pages) => {
|
||||
const getTOC = (pages)=>{
|
||||
const add1 = (title, page)=>{
|
||||
res.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
}
|
||||
};
|
||||
const add2 = (title, page)=>{
|
||||
if(!_.last(res)) add1('', page);
|
||||
_.last(res).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
}
|
||||
};
|
||||
const add3 = (title, page)=>{
|
||||
if(!_.last(res)) add1('', page);
|
||||
if(!_.last(_.last(res).children)) add2('', page);
|
||||
_.last(_.last(res).children).children.push({
|
||||
title : title,
|
||||
page : page + 1,
|
||||
title : title,
|
||||
page : page + 1,
|
||||
children : []
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let res = [];
|
||||
const res = [];
|
||||
_.each(pages, (page, pageNum)=>{
|
||||
const lines = page.split('\n');
|
||||
_.each(lines, (line) => {
|
||||
_.each(lines, (line)=>{
|
||||
if(_.startsWith(line, '# ')){
|
||||
const title = line.replace('# ', '');
|
||||
add1(title, pageNum)
|
||||
add1(title, pageNum);
|
||||
}
|
||||
if(_.startsWith(line, '## ')){
|
||||
const title = line.replace('## ', '');
|
||||
@@ -42,21 +42,21 @@ const getTOC = (pages) => {
|
||||
const title = line.replace('### ', '');
|
||||
add3(title, pageNum);
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
return res;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = function(brew){
|
||||
|
||||
const pages = brew.split('\\page');
|
||||
const TOC = getTOC(pages);
|
||||
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
|
||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`)
|
||||
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
|
||||
if(g1.children.length){
|
||||
_.each(g1.children, (g2, idx2) => {
|
||||
_.each(g1.children, (g2, idx2)=>{
|
||||
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
|
||||
if(g2.children.length){
|
||||
_.each(g2.children, (g3, idx3) => {
|
||||
_.each(g2.children, (g3, idx3)=>{
|
||||
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
|
||||
});
|
||||
}
|
||||
@@ -69,4 +69,4 @@ module.exports = function(brew){
|
||||
##### Table Of Contents
|
||||
${markdown}
|
||||
</div>\n`;
|
||||
}
|
||||
};
|
||||
@@ -1,73 +1,90 @@
|
||||
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 />,
|
||||
'/test' : <SharePage />,
|
||||
'/test_old' : <SharePage />,
|
||||
|
||||
|
||||
'*' : <HomePage />,
|
||||
});
|
||||
},
|
||||
render : function(){
|
||||
return <div className='homebrew'>
|
||||
<Router initialUrl={this.props.url}/>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Homebrew;
|
||||
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 initialUrl={this.props.url}/>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Homebrew;
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
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>
|
||||
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>;
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
const React = require('react');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
|
||||
module.exports = Store.createSmartComponent((props) => {
|
||||
return <Nav.item className='brewTitle'>{props.title}</Nav.item>
|
||||
}, (props) => {
|
||||
return {title : Store.getMetaData().title};
|
||||
})
|
||||
@@ -1,76 +0,0 @@
|
||||
const flux = require('pico-flux')
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
const Actions = require('homebrewery/brew.actions.js');
|
||||
|
||||
const onStoreChange = () => {
|
||||
return {
|
||||
status : Store.getStatus(),
|
||||
errors : Store.getErrors()
|
||||
}
|
||||
};
|
||||
|
||||
const ContinousSave = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
status : 'ready',
|
||||
errors : undefined
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
flux.actionEmitter.on('dispatch', this.actionHandler);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.props.status !== 'ready') return 'You have unsaved changes!';
|
||||
};
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
flux.actionEmitter.removeListener('dispatch', this.actionHandler);
|
||||
window.onbeforeunload = function(){};
|
||||
},
|
||||
actionHandler : function(actionType){
|
||||
if(actionType == 'UPDATE_BREW_TEXT' || actionType == 'UPDATE_META'){
|
||||
Actions.pendingSave();
|
||||
}
|
||||
},
|
||||
handleClick : function(){
|
||||
Actions.save();
|
||||
},
|
||||
renderError : function(){
|
||||
let errMsg = '';
|
||||
try{
|
||||
errMsg += this.state.errors.toString() + '\n\n';
|
||||
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
|
||||
}catch(e){}
|
||||
|
||||
return <Nav.item className='continousSave error' icon="fa-warning">
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>
|
||||
},
|
||||
render : function(){
|
||||
if(this.props.status == 'error') return this.renderError();
|
||||
|
||||
if(this.props.status == 'saving'){
|
||||
return <Nav.item className='continousSave' icon="fa-spinner fa-spin">saving...</Nav.item>
|
||||
}
|
||||
if(this.props.status == 'pending'){
|
||||
return <Nav.item className='continousSave' onClick={this.handleClick} color='blue' icon='fa-save'>Save Now</Nav.item>
|
||||
}
|
||||
if(this.props.status == 'ready'){
|
||||
return <Nav.item className='continousSave saved'>saved.</Nav.item>
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = Store.createSmartComponent(ContinousSave, onStoreChange);
|
||||
@@ -1,79 +0,0 @@
|
||||
const flux = require('pico-flux')
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
const Actions = require('homebrewery/brew.actions.js');
|
||||
|
||||
const onStoreChange = () => {
|
||||
return {
|
||||
status : Store.getStatus(),
|
||||
errors : Store.getErrors()
|
||||
}
|
||||
};
|
||||
|
||||
const ContinousSave = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
status : 'ready',
|
||||
errors : undefined
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
flux.actionEmitter.on('dispatch', this.actionHandler);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.props.status !== 'ready') return 'You have unsaved changes!';
|
||||
};
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
flux.actionEmitter.removeListener('dispatch', this.actionHandler);
|
||||
window.onbeforeunload = function(){};
|
||||
},
|
||||
actionHandler : function(actionType){
|
||||
if(actionType == 'UPDATE_BREW_CODE' || actionType == 'UPDATE_META' || actionType == 'UPDATE_BREW_STYLE'){
|
||||
Actions.pendingSave();
|
||||
}
|
||||
},
|
||||
handleClick : function(){
|
||||
Actions.save();
|
||||
},
|
||||
renderError : function(){
|
||||
let errMsg = '';
|
||||
try{
|
||||
errMsg += this.state.errors.toString() + '\n\n';
|
||||
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
|
||||
}catch(e){}
|
||||
|
||||
return <Nav.item className='continousSave error' icon="fa-warning">
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Back up your brew in a text file, just in case.
|
||||
<br /><br />
|
||||
|
||||
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>
|
||||
},
|
||||
render : function(){
|
||||
if(this.props.status == 'error') return this.renderError();
|
||||
|
||||
if(this.props.status == 'saving'){
|
||||
return <Nav.item className='continousSave' icon="fa-spinner fa-spin">saving...</Nav.item>
|
||||
}
|
||||
if(this.props.status == 'pending'){
|
||||
return <Nav.item className='continousSave' onClick={this.handleClick} color='blue' icon='fa-save'>Save Now</Nav.item>
|
||||
}
|
||||
if(this.props.status == 'ready'){
|
||||
return <Nav.item className='continousSave saved'>saved.</Nav.item>
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = Store.createSmartComponent(ContinousSave, onStoreChange);
|
||||
@@ -1,33 +1,34 @@
|
||||
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>
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
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>;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = EditTitle;
|
||||
@@ -1,13 +1,9 @@
|
||||
var React = require('react');
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item
|
||||
{...props}
|
||||
newTab={true}
|
||||
href='https://github.com/stolksdorf/homebrewery/issues'
|
||||
color='red'
|
||||
icon='fa-bug'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
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='https://www.reddit.com/r/homebrewery/submit?selftext=true&title=[Issue]' color='red' icon='fa-bug'>
|
||||
report issue
|
||||
</Nav.item>;
|
||||
};
|
||||
@@ -1,22 +1,50 @@
|
||||
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;
|
||||
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;
|
||||
|
||||
@@ -13,6 +13,32 @@
|
||||
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;
|
||||
@@ -99,34 +125,4 @@
|
||||
text-align : center;
|
||||
}
|
||||
}
|
||||
.staticSave.navItem{
|
||||
background-color : @orange;
|
||||
&:hover{
|
||||
background-color : @green;
|
||||
}
|
||||
}
|
||||
.continousSave.navItem{
|
||||
width : 105px;
|
||||
text-align : center;
|
||||
&.saved{
|
||||
cursor : initial;
|
||||
color : #666;
|
||||
}
|
||||
&.error{
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
.errorContainer{
|
||||
position : absolute;
|
||||
top : 29px;
|
||||
left : -20px;
|
||||
z-index : 1000;
|
||||
width : 170px;
|
||||
padding : 8px;
|
||||
background-color : #333;
|
||||
a{
|
||||
color : @teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
module.exports = {
|
||||
Account : require('./account.navitem.jsx'),
|
||||
BrewTitle : require('./brewTitle.navitem.jsx'),
|
||||
ContinousSave : require('./continousSave.navitem.jsx'),
|
||||
Issue : require('./issue.navitem.jsx'),
|
||||
Patreon : require('./patreon.navitem.jsx'),
|
||||
Print : require('./print.navitem.jsx'),
|
||||
Recent : require('./recent.navitem.jsx'),
|
||||
StaticSave : require('./staticSave.navitem.jsx'),
|
||||
};
|
||||
@@ -1,14 +1,14 @@
|
||||
var React = require('react');
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item
|
||||
{...props}
|
||||
className='patreon'
|
||||
newTab={true}
|
||||
href='https://www.patreon.com/stolksdorf'
|
||||
color='green'
|
||||
icon='fa-heart'>
|
||||
help out
|
||||
</Nav.item>
|
||||
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>;
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
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>
|
||||
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>;
|
||||
};
|
||||
@@ -1,204 +1,200 @@
|
||||
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';
|
||||
|
||||
//DEPRICATED
|
||||
|
||||
|
||||
var BaseItem = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
storageKey : '',
|
||||
text : '',
|
||||
currentBrew:{
|
||||
title : '',
|
||||
id : '',
|
||||
url : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
showDropdown: false,
|
||||
brews : []
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
console.log('Recent nav item is depricated');
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
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'>
|
||||
<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'>
|
||||
<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>;
|
||||
}
|
||||
|
||||
})
|
||||
};
|
||||
52
client/homebrew/navbar/reddit.navitem.jsx
Normal file
52
client/homebrew/navbar/reddit.navitem.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
//var striptags = require('striptags');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const MAX_URL_SIZE = 2083;
|
||||
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';
|
||||
|
||||
|
||||
const RedditShare = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
sharedId : '',
|
||||
text : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getText : function(){
|
||||
|
||||
},
|
||||
|
||||
|
||||
handleClick : function(){
|
||||
const url = [
|
||||
MAIN_URL,
|
||||
`title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`,
|
||||
|
||||
`text=${encodeURIComponent(this.props.brew.text)}`
|
||||
|
||||
|
||||
].join('&');
|
||||
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fa-reddit-alien' color='red' onClick={this.handleClick}>
|
||||
share on reddit
|
||||
</Nav.item>;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = RedditShare;
|
||||
@@ -1,37 +0,0 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
const Actions = require('homebrewery/brew.actions.js');
|
||||
|
||||
const StaticSave = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
status : 'ready'
|
||||
};
|
||||
},
|
||||
handleClick : function(){
|
||||
Actions.create();
|
||||
},
|
||||
render : function(){
|
||||
if(this.props.status === 'saving'){
|
||||
return <Nav.item icon='fa-spinner fa-spin' className='staticSave'>
|
||||
save...
|
||||
</Nav.item>
|
||||
}
|
||||
if(this.props.status === 'ready'){
|
||||
return <Nav.item icon='fa-save' className='staticSave' onClick={this.handleClick}>
|
||||
save
|
||||
</Nav.item>
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = Store.createSmartComponent(StaticSave, ()=>{
|
||||
return {
|
||||
status : Store.getStatus()
|
||||
}
|
||||
});
|
||||
@@ -1,63 +1,231 @@
|
||||
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;
|
||||
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),
|
||||
lastUpdated : this.props.brew.updatedAt
|
||||
};
|
||||
},
|
||||
savedBrew : null,
|
||||
|
||||
componentDidMount : function(){
|
||||
this.trySave();
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
return 'You have unsaved changes!';
|
||||
}
|
||||
};
|
||||
|
||||
this.setState({
|
||||
htmlErrors : Markdown.validate(this.state.brew.text)
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
window.onbeforeunload = function(){};
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
|
||||
|
||||
handleControlKeys : function(e){
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const S_KEY = 83;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == S_KEY) this.save();
|
||||
if(e.keyCode == P_KEY) window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
|
||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.refs.editor.update();
|
||||
},
|
||||
|
||||
handleMetadataChange : function(metadata){
|
||||
this.setState({
|
||||
brew : _.merge({}, this.state.brew, metadata),
|
||||
isPending : true,
|
||||
}, ()=>{
|
||||
this.trySave();
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
|
||||
//If there are errors, run the validator on everychange to give quick feedback
|
||||
let htmlErrors = this.state.htmlErrors;
|
||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||
|
||||
this.setState({
|
||||
brew : _.merge({}, this.state.brew, { text: text }),
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors
|
||||
});
|
||||
|
||||
this.trySave();
|
||||
},
|
||||
|
||||
hasChanges : function(){
|
||||
if(this.savedBrew){
|
||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||
} else {
|
||||
return !_.isEqual(this.state.brew, this.props.brew);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
trySave : function(){
|
||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
if(this.hasChanges()){
|
||||
this.debounceSave();
|
||||
} else {
|
||||
this.debounceSave.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
save : function(){
|
||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||
|
||||
this.setState({
|
||||
isSaving : true,
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(this.state.brew.text)
|
||||
});
|
||||
|
||||
request
|
||||
.put(`/api/update/${this.props.brew.editId}`)
|
||||
.send(this.state.brew)
|
||||
.end((err, res)=>{
|
||||
if(err){
|
||||
this.setState({
|
||||
errors : err,
|
||||
});
|
||||
} else {
|
||||
this.savedBrew = res.body;
|
||||
this.setState({
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
lastUpdated : res.body.updatedAt
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.errors){
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
} catch (e){}
|
||||
|
||||
return <Nav.item className='save error' icon='fa-warning'>
|
||||
Oops!
|
||||
<div className='errorContainer'>
|
||||
Looks like there was a problem saving. <br />
|
||||
Report the issue <a target='_blank' href={`https://github.com/stolksdorf/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item className='save' icon='fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
}
|
||||
if(this.state.isPending && this.hasChanges()){
|
||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
if(!this.state.isPending && !this.state.isSaving){
|
||||
return <Nav.item className='save saved'>saved.</Nav.item>;
|
||||
}
|
||||
},
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
<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;
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
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 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 BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
var ErrorPage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
const ErrorPage = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
ver : '0.0.0',
|
||||
errorId: ''
|
||||
ver : '0.0.0',
|
||||
errorId : ''
|
||||
};
|
||||
},
|
||||
|
||||
@@ -39,7 +40,7 @@ var ErrorPage = React.createClass({
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.text} />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
12
client/homebrew/pages/hijackPrint.js
Normal file
12
client/homebrew/pages/hijackPrint.js
Normal file
@@ -0,0 +1,12 @@
|
||||
//TODO: Depricate
|
||||
|
||||
module.exports = function(shareId){
|
||||
return function(event){
|
||||
event = event || window.event;
|
||||
if((event.ctrlKey || event.metaKey) && event.keyCode == 80){
|
||||
const win = window.open(`/homebrew/print/${shareId}?dialog=true`, '_blank');
|
||||
win.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,63 +1,95 @@
|
||||
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 collaspe={true} />
|
||||
<IssueNavItem collaspe={true} />
|
||||
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-star' collaspe={true}>
|
||||
What's new
|
||||
</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;
|
||||
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;
|
||||
|
||||
@@ -1,62 +1,162 @@
|
||||
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 collaspe={true} />
|
||||
<Items.Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewInterface />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NewPage;
|
||||
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;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
.newPage{
|
||||
|
||||
.saveButton{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,66 +1,47 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const Markdown = require('homebrewery/markdown.js');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const Headtags = require('vitreum/headtags');
|
||||
|
||||
const PrintPage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
const PrintPage = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
query : {},
|
||||
brew : {
|
||||
brew : {
|
||||
text : '',
|
||||
style : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew: this.props.brew
|
||||
brewText : this.props.brew.text
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
|
||||
componentDidMount : function() {
|
||||
if(this.props.query.local){
|
||||
try{
|
||||
this.setState({
|
||||
brew : JSON.parse(
|
||||
localStorage.getItem(this.props.query.local)
|
||||
)
|
||||
});
|
||||
}catch(e){}
|
||||
this.setState({ brewText: localStorage.getItem(this.props.query.local) });
|
||||
}
|
||||
|
||||
if(this.props.query.dialog) window.print();
|
||||
},
|
||||
//TODO: Print page shouldn't replicate functionality in brew renderer
|
||||
renderStyle : function(){
|
||||
if(!this.state.brew.style) return;
|
||||
return <style>{this.state.brew.style.replace(/;/g, ' !important;')}</style>
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
return _.map(this.state.brew.text.split('\\page'), (page, index) => {
|
||||
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
|
||||
return <div
|
||||
className='phb v2'
|
||||
className='phb'
|
||||
id={`p${index + 1}`}
|
||||
dangerouslySetInnerHTML={{__html:Markdown.render(page)}}
|
||||
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
|
||||
key={index} />;
|
||||
});
|
||||
},
|
||||
|
||||
renderPrintInstructions : function(){
|
||||
return <div className='printInstructions'>
|
||||
Hey, I'm really cool instructions!!!!!
|
||||
|
||||
</div>
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='printPage'>
|
||||
<Headtags.title>{this.state.brew.title}</Headtags.title>
|
||||
{this.renderPrintInstructions()}
|
||||
{this.renderStyle()}
|
||||
return <div>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
|
||||
.printPage{
|
||||
position : relative;
|
||||
@media print{
|
||||
.printInstructions{
|
||||
display : none;
|
||||
}
|
||||
}
|
||||
.printInstructions{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
right : 0px;
|
||||
z-index : 100000;
|
||||
padding : 30px;
|
||||
background-color : @blue;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,74 +1,72 @@
|
||||
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 Headtags = require('vitreum/headtags');
|
||||
|
||||
const SharePage = React.createClass({
|
||||
componentDidMount: function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
handleControlKeys : Utils.controlKeys({
|
||||
p : Actions.print
|
||||
}),
|
||||
|
||||
renderMetatags : function(brew){
|
||||
let metatags = [
|
||||
<Headtags.meta key='site_name' property='og:site_name' content='Homebrewery'/>,
|
||||
<Headtags.meta key='type' property='og:type' content='article' />
|
||||
];
|
||||
if(brew.title){
|
||||
metatags.push(<Headtags.meta key='title' property='og:title' content={brew.title} />);
|
||||
}
|
||||
if(brew.description){
|
||||
metatags.push(<Headtags.meta key='description' name='description' content={brew.description} />);
|
||||
}
|
||||
if(brew.thumbnail){
|
||||
metatags.push(<Headtags.meta key='image' property='og:image' content={brew.thumbnail} />);
|
||||
}
|
||||
return metatags;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const brew = Store.getBrew();
|
||||
return <div className='sharePage page'>
|
||||
{this.renderMetatags(brew)}
|
||||
|
||||
<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 brew={brew} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SharePage;
|
||||
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;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
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 = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
const BrewItem = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
title : '',
|
||||
description : '',
|
||||
|
||||
authors : []
|
||||
@@ -15,12 +17,30 @@ const BrewItem = React.createClass({
|
||||
};
|
||||
},
|
||||
|
||||
renderEditLink: function(){
|
||||
deleteBrew : function(){
|
||||
if(!confirm('are you sure you want to delete this brew?')) return;
|
||||
if(!confirm('are you REALLY sure? You will not be able to recover it')) return;
|
||||
|
||||
request.get(`/api/remove/${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
renderDeleteBrewLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
return <a onClick={this.deleteBrew}>
|
||||
<i className='fa fa-trash' />
|
||||
</a>;
|
||||
},
|
||||
renderEditLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
return <a href={`/edit/${this.props.brew.editId}`} target='_blank'>
|
||||
<i className='fa fa-pencil' />
|
||||
</a>
|
||||
</a>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
@@ -47,8 +67,9 @@ const BrewItem = React.createClass({
|
||||
<i className='fa fa-share-alt' />
|
||||
</a>
|
||||
{this.renderEditLink()}
|
||||
{this.renderDeleteBrewLink()}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
@@ -9,19 +10,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 = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
const UserPage = createClass({
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
username : '',
|
||||
brews : []
|
||||
brews : []
|
||||
};
|
||||
},
|
||||
|
||||
@@ -30,14 +31,14 @@ const UserPage = React.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');
|
||||
});
|
||||
},
|
||||
|
||||
@@ -52,13 +53,12 @@ const UserPage = React.createClass({
|
||||
|
||||
render : function(){
|
||||
const brews = this.getSortedBrews();
|
||||
console.log('user brews', brews);
|
||||
|
||||
return <div className='userPage page'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<RecentNavItem.both />
|
||||
<Account userPage={this.props.username} />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
@@ -69,7 +69,7 @@ const UserPage = React.createClass({
|
||||
{this.renderPrivateBrews(brews.private)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 327 B After Width: | Height: | Size: 327 B |
|
Before Width: | Height: | Size: 530 B After Width: | Height: | Size: 530 B |
31
client/homebrew/phbStyle/phb.depricated.less
Normal file
31
client/homebrew/phbStyle/phb.depricated.less
Normal file
@@ -0,0 +1,31 @@
|
||||
.phb{
|
||||
//Double hr for full width elements
|
||||
hr+hr+blockquote{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
hr+table{
|
||||
margin-top : -5px;
|
||||
margin-bottom : 50px;
|
||||
padding-top : 10px;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 37px 17px;
|
||||
border-image-repeat : round;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
}
|
||||
h5+hr+table{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,47 @@
|
||||
@media print {
|
||||
.phb.v1{
|
||||
.descriptive, blockquote{
|
||||
box-shadow : none;
|
||||
}
|
||||
|
||||
@import (less) 'shared/naturalcrit/styles/reset.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
|
||||
@import (less) './client/homebrew/phbStyle/phb.assets.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.depricated.less';
|
||||
//Colors
|
||||
@background : #EEE5CE;
|
||||
@noteGreen : #e0e5c1;
|
||||
@headerUnderline : #c9ad6a;
|
||||
@horizontalRule : #9c2b1b;
|
||||
@headerText : #58180D;
|
||||
@monsterStatBackground : #FDF1DC;
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
}
|
||||
*{
|
||||
-webkit-print-color-adjust : exact;
|
||||
}
|
||||
.useSansSerif(){
|
||||
font-family : ScalySans;
|
||||
em{
|
||||
font-family : ScalySans;
|
||||
font-style : italic;
|
||||
}
|
||||
strong{
|
||||
font-family : ScalySans;
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
|
||||
.phb.v1{
|
||||
@import (less) './phb.fonts.v1.css';
|
||||
@import (less) './phb.assets.v1.less';
|
||||
|
||||
|
||||
//Colors
|
||||
@background : #EEE5CE;
|
||||
@noteGreen : #e0e5c1;
|
||||
@headerUnderline : #c9ad6a;
|
||||
@horizontalRule : #9c2b1b;
|
||||
@headerText : #58180D;
|
||||
@monsterStatBackground : #FDF1DC;
|
||||
|
||||
@page { margin: 0; }
|
||||
|
||||
|
||||
.useSansSerif(){
|
||||
font-family : ScalySans;
|
||||
em{
|
||||
font-family : ScalySans;
|
||||
font-style : italic;
|
||||
}
|
||||
strong{
|
||||
font-family : ScalySans;
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
.useColumns(@multiplier : 1){
|
||||
column-count : 2;
|
||||
column-fill : auto;
|
||||
column-gap : 1cm;
|
||||
column-width : 8cm * @multiplier;
|
||||
-webkit-column-count : 2;
|
||||
-moz-column-count : 2;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
-webkit-column-gap : 1cm;
|
||||
-moz-column-gap : 1cm;
|
||||
}
|
||||
& *{
|
||||
-webkit-print-color-adjust : exact;
|
||||
}
|
||||
.useColumns(@multiplier : 1){
|
||||
column-count : 2;
|
||||
column-fill : auto;
|
||||
column-gap : 1cm;
|
||||
column-width : 8cm * @multiplier;
|
||||
-webkit-column-count : 2;
|
||||
-moz-column-count : 2;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
-webkit-column-gap : 1cm;
|
||||
-moz-column-gap : 1cm;
|
||||
}
|
||||
.phb{
|
||||
.useColumns();
|
||||
counter-increment : phb-page-numbers;
|
||||
position : relative;
|
||||
@@ -66,7 +59,6 @@
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
@@ -365,157 +357,111 @@
|
||||
-webkit-column-break-inside : avoid;
|
||||
column-break-inside : avoid;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * SPELL LIST
|
||||
// *****************************/
|
||||
.spellList{
|
||||
.useSansSerif();
|
||||
column-count : 4;
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
ul+h5{
|
||||
margin-top : 15px;
|
||||
}
|
||||
p, ul{
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.3em;
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.5em;
|
||||
padding-left : 1em;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
-webkit-column-break-inside : auto;
|
||||
column-break-inside : auto;
|
||||
}
|
||||
//*****************************
|
||||
// * SPELL LIST
|
||||
// *****************************/
|
||||
.phb .spellList{
|
||||
.useSansSerif();
|
||||
column-count : 4;
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
ul+h5{
|
||||
margin-top : 15px;
|
||||
}
|
||||
p, ul{
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.3em;
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.5em;
|
||||
padding-left : 1em;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
-webkit-column-break-inside : auto;
|
||||
column-break-inside : auto;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.phb .wide{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
.phb .classTable{
|
||||
margin-top : 25px;
|
||||
margin-bottom : 40px;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 25px 17px;
|
||||
border-image-repeat : stretch;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
h5{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
}
|
||||
//************************************
|
||||
// * DESCRIPTIVE TEXT BOX
|
||||
// ************************************/
|
||||
.phb .descriptive{
|
||||
display : block-inline;
|
||||
margin-bottom : 1em;
|
||||
background-color : #faf7ea;
|
||||
font-family : ScalySans;
|
||||
border-style : solid;
|
||||
border-width : 7px;
|
||||
border-image : @descriptiveBoxImage 12 stretch;
|
||||
border-image-outset : 4px;
|
||||
box-shadow : 0px 0px 6px #faf7ea;
|
||||
p{
|
||||
display : block;
|
||||
padding-bottom : 0px;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
p + p {
|
||||
padding-top : .8em;
|
||||
}
|
||||
em {
|
||||
font-family : ScalySans;
|
||||
font-style : italic;
|
||||
}
|
||||
strong {
|
||||
font-family : ScalySans;
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
.phb pre+.descriptive{
|
||||
margin-top : 8px;
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.phb .toc{
|
||||
-webkit-column-break-inside : avoid;
|
||||
column-break-inside : avoid;
|
||||
a{
|
||||
color : black;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * PRINT
|
||||
// *****************************/
|
||||
&.print{
|
||||
blockquote{
|
||||
box-shadow : none;
|
||||
}
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.wide{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
&>ul>li{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
.classTable{
|
||||
margin-top : 25px;
|
||||
margin-bottom : 40px;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 25px 17px;
|
||||
border-image-repeat : round;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
h5{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
.descriptive{
|
||||
display : block-inline;
|
||||
margin-bottom : 1em;
|
||||
background-color : #faf7ea;
|
||||
font-family : ScalySans;
|
||||
border-style : solid;
|
||||
border-width : 7px;
|
||||
border-image : @descriptiveBoxImage 12 round;
|
||||
border-image-outset : 4px;
|
||||
box-shadow : 0px 0px 6px #faf7ea;
|
||||
p{
|
||||
display : block;
|
||||
padding-bottom : 0px;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
p + p {
|
||||
padding-top : .8em;
|
||||
}
|
||||
em {
|
||||
font-family : ScalySans;
|
||||
font-style : italic;
|
||||
}
|
||||
strong {
|
||||
font-family : ScalySans;
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
pre+.descriptive{
|
||||
margin-top : 8px;
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.toc{
|
||||
-webkit-column-break-inside : avoid;
|
||||
column-break-inside : avoid;
|
||||
a{
|
||||
color : black;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
}
|
||||
&>ul>li{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//*****************************
|
||||
// * Old Stuff
|
||||
// *****************************/
|
||||
|
||||
//Double hr for full width elements
|
||||
hr+hr+blockquote{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
hr+table{
|
||||
margin-top : -5px;
|
||||
margin-bottom : 50px;
|
||||
padding-top : 10px;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 37px 17px;
|
||||
border-image-repeat : round;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
}
|
||||
h5+hr+table{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
client/homebrew/phbStyle/red.png
Normal file
BIN
client/homebrew/phbStyle/red.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 864 B |
@@ -1,22 +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>
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
{
|
||||
"log_level" : "info",
|
||||
"login_path" : "/dev/login",
|
||||
"jwt_secret" : "secretsecret",
|
||||
"admin" : {
|
||||
"user" : "admin",
|
||||
"pass" : "password",
|
||||
"key" : "adminadminadmin"
|
||||
}
|
||||
"host" : "homebrewery.local.naturalcrit.com:8000",
|
||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||
"secret" : "secret"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"login_path" : "http://naturalcrit.com/login",
|
||||
"log_level" : "warn"
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"login_path" : "http://staging.naturalcrit.com/login",
|
||||
"log_level" : "trace"
|
||||
}
|
||||
45
contributing.md
Normal file
45
contributing.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Contributing to Homebrewery
|
||||
|
||||
## How can I contribute?
|
||||
|
||||
### Improve documentation
|
||||
|
||||
As a user of Homebrewery you're the perfect candidate to help us improve our documentation. Typo corrections, error fixes, better explanations, more examples, etc. Open issues for things that could be improved. Anything. Even improvements to this document.
|
||||
|
||||
|
||||
### Improve issues
|
||||
|
||||
Some issues are created with missing information, not reproducible, or plain invalid. Help make them easier to resolve. Handling issues takes a lot of time that we could rather spend on fixing bugs and adding features.
|
||||
|
||||
|
||||
### Write code
|
||||
|
||||
You can use issue labels to discover issues you could help out with:
|
||||
|
||||
* [`blocked` issues](https://github.com/stolksdorf/homebrewery/labels/blocked) need help getting unstuck
|
||||
* [`bug` issues](https://github.com/stolksdorf/homebrewery/labels/bug) are known bugs we'd like to fix
|
||||
* [`feature` issues](https://github.com/stolksdorf/homebrewery/labels/feature) are features we're open to including
|
||||
* [`help wanted`](https://github.com/stolksdorf/homebrewery/labels/help%20wanted) labels are especially useful.
|
||||
|
||||
If you're updating dependencies, please make sure you use npm@5.6.0 and commit the updated `package-lock.json` file.
|
||||
|
||||
You can also refer to the [Development Roadmap on Trello](https://trello.com/b/q6kE29F8/development-roadmap)
|
||||
|
||||
|
||||
## Submitting an issue
|
||||
|
||||
- The issue tracker is for issues. Use the [subreddit](https://www.reddit.com/r/homebrewery/) for support.
|
||||
- Search the issue tracker before opening an issue.
|
||||
- Use a clear and descriptive title.
|
||||
- Include as much information as possible: Steps to reproduce the issue, error message, browser type and version, etc.
|
||||
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
- Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work.
|
||||
- For ambitious tasks, you should try to get your work in front of the community for feedback as soon as possible. Open a pull request as soon as you have done the minimum needed to demonstrate your idea. At this early stage, don't worry about making things perfect, or 100% complete. Add a [WIP] prefix to the title, and describe what you still need to do. This lets reviewers know not to nit-pick small details or point out improvements you already know you need to make.
|
||||
- New features should be accompanied with tests and documentation if applicable.
|
||||
- Lint and test before submitting the pull request by running `$ npm run verify`.
|
||||
- If your code is not passing Linting checks due to a non-fixable warning, and you feel it's valid (eg. we lint on a file being too long, but sometimes a file just _has_ to be long), add `/* eslint-disable [rule-name] */` to the top of the file. Be sure to justfiy your lint override in your PR description.
|
||||
- Use a clear and descriptive title for the pull request and commits.
|
||||
- You might be asked to do changes to your pull request. There's never a need to open another pull request. [Just update the existing one.](https://github.com/RichardLitt/knowledge/blob/master/github/amending-a-commit-guide.md)
|
||||
8158
package-lock.json
generated
Normal file
8158
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
package.json
63
package.json
@@ -1,55 +1,68 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "3.0.0",
|
||||
"version": "2.8.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/stolksdorf/homebrewery.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"quick": "node scripts/quick.js",
|
||||
"build": "node scripts/build.js",
|
||||
"populate": "node scripts/populate.js",
|
||||
"prod": "set NODE_ENV=production&& npm run build",
|
||||
"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",
|
||||
"postinstall": "npm run build",
|
||||
"start": "node server.js",
|
||||
"snippet": "nodemon scripts/snippet.test.js",
|
||||
"todo": "./node_modules/.bin/fixme -i node_modules/** -i build/**",
|
||||
"test": "mocha tests",
|
||||
"test:dev": "nodemon -x mocha tests || exit 0",
|
||||
"test:markdown": "nodemon -x mocha tests/markdown.test.js || exit 0"
|
||||
"start": "node server.js"
|
||||
},
|
||||
"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": "^1.0.3",
|
||||
"body-parser": "^1.14.2",
|
||||
"classnames": "^2.2.0",
|
||||
"codemirror": "^5.22.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"egads": "^1.0.1",
|
||||
"create-react-class": "^15.6.3",
|
||||
"express": "^4.13.3",
|
||||
"jwt-simple": "^0.5.1",
|
||||
"lodash": "^4.17.3",
|
||||
"loglevel": "^1.4.1",
|
||||
"lodash": "^4.11.2",
|
||||
"marked": "^0.3.5",
|
||||
"moment": "^2.11.0",
|
||||
"mongoose": "^4.3.3",
|
||||
"nconf": "^0.8.4",
|
||||
"pico-flux": "^2.1.2",
|
||||
"pico-flux": "^1.1.0",
|
||||
"pico-router": "^1.1.0",
|
||||
"react": "^15.4.1",
|
||||
"react-dom": "^15.4.1",
|
||||
"react": "^16.3.1",
|
||||
"react-dom": "^16.3.1",
|
||||
"shortid": "^2.2.4",
|
||||
"striptags": "^2.1.1",
|
||||
"superagent": "^1.6.1",
|
||||
"vitreum": "^4.0.12"
|
||||
"superagent": "^3.8.2",
|
||||
"vitreum": "^4.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"app-module-path": "^2.1.0",
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"chai-subset": "^1.4.0",
|
||||
"fixme": "^0.4.3",
|
||||
"mocha": "^3.2.0",
|
||||
"supertest": "^2.0.1",
|
||||
"supertest-as-promised": "^4.0.2"
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"pico-check": "^1.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
1
phb.standalone.css
Normal file
1
phb.standalone.css
Normal file
File diff suppressed because one or more lines are too long
@@ -9,13 +9,12 @@ const asset = require('vitreum/steps/assets.js');
|
||||
|
||||
const Proj = require('./project.json');
|
||||
|
||||
Promise.resolve()
|
||||
.then(()=>clean())
|
||||
.then(()=>lib(Proj.libs))
|
||||
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, ['./shared']))
|
||||
.then((deps)=>less('homebrew', ['./shared'], deps))
|
||||
.then(()=>jsx('admin', './client/admin/admin.jsx', Proj.libs, ['./shared']))
|
||||
.then((deps)=>less('admin', ['./shared'], deps))
|
||||
clean()
|
||||
.then(lib(Proj.libs))
|
||||
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', { libs: Proj.libs, shared: ['./shared'] }))
|
||||
.then((deps)=>less('homebrew', { shared: ['./shared'] }, deps))
|
||||
.then(()=>jsx('admin', './client/admin/admin.jsx', { libs: Proj.libs, shared: ['./shared'] }))
|
||||
.then((deps)=>less('admin', { shared: ['./shared'] }, deps))
|
||||
.then(()=>asset(Proj.assets, ['./shared', './client']))
|
||||
.then(()=>console.timeEnd.bind(console, label))
|
||||
.then(console.timeEnd.bind(console, label))
|
||||
.catch(console.error);
|
||||
@@ -10,12 +10,13 @@ const livereload = require('vitreum/steps/livereload.js');
|
||||
const Proj = require('./project.json');
|
||||
|
||||
Promise.resolve()
|
||||
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared'))
|
||||
.then((deps)=>less('homebrew', './shared', deps))
|
||||
.then(()=>jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared'))
|
||||
.then((deps)=>less('admin', './shared', deps))
|
||||
.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(()=>console.timeEnd.bind(console, label))
|
||||
.catch(console.error)
|
||||
.then(console.timeEnd.bind(console, label))
|
||||
.catch(console.error);
|
||||
@@ -1,8 +0,0 @@
|
||||
|
||||
require('fixme')({
|
||||
path: process.cwd(),
|
||||
ignored_directories: ['node_modules/**', '.git/**', 'build/**'],
|
||||
file_patterns: ['**/*.js', '**/*.jsx', '**/*.less'],
|
||||
file_encoding: 'utf8',
|
||||
line_length_limit: 200
|
||||
});
|
||||
10
scripts/phb.js
Normal file
10
scripts/phb.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const less = require('less');
|
||||
const fs = require('fs');
|
||||
|
||||
less.render(fs.readFileSync('./client/homebrew/phbStyle/phb.style.less', 'utf8'), { compress: true })
|
||||
.then((output)=>{
|
||||
fs.writeFileSync('./phb.standalone.css', output.css);
|
||||
console.log('phb.standalone.css created!');
|
||||
}, (err)=>{
|
||||
console.error(err);
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
//Populates the DB with a bunch of brews for UI testing
|
||||
const _ = require('lodash');
|
||||
|
||||
const DB = require('../server/db.js');
|
||||
const BrewData = require('../server/brew.data.js');
|
||||
const BrewGen = require('../test/brew.gen.js');
|
||||
|
||||
return Promise.resolve()
|
||||
.then(DB.connect)
|
||||
.then(BrewData.removeAll)
|
||||
.then(() => {
|
||||
console.log('Adding random brews...');
|
||||
return BrewGen.populateDB(BrewGen.random(50));
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Adding specific brews...');
|
||||
return BrewGen.populateDB(BrewGen.static());
|
||||
})
|
||||
.then(() => {
|
||||
return DB.close();
|
||||
})
|
||||
.catch(console.error);
|
||||
@@ -1,15 +1,16 @@
|
||||
{
|
||||
"assets": ["*.png","*.otf", "*.ico", "*.jpg"],
|
||||
"shared":["./shared"],
|
||||
"assets": ["*.png","*.otf", "*.ico"],
|
||||
"shared":[
|
||||
],
|
||||
"libs" : [
|
||||
"react",
|
||||
"react-dom",
|
||||
"create-react-class",
|
||||
"lodash",
|
||||
"classnames",
|
||||
"codemirror",
|
||||
"codemirror/mode/gfm/gfm.js",
|
||||
"codemirror/mode/javascript/javascript.js",
|
||||
"codemirror/mode/css/css.js",
|
||||
"moment",
|
||||
"superagent",
|
||||
"marked",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const snippets = require('../shared/homebrewery/snippets');
|
||||
|
||||
console.log(snippets);
|
||||
|
||||
//console.log(snippets.brew.spell());
|
||||
//console.log(snippets.brew.table());
|
||||
|
||||
console.log(snippets.brew.noncasterTable());
|
||||
176
server.js
176
server.js
@@ -1,36 +1,140 @@
|
||||
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'));
|
||||
|
||||
//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();
|
||||
});
|
||||
*/
|
||||
|
||||
require('./server/db.js').connect()
|
||||
.then(()=>{
|
||||
const PORT = process.env.PORT || 8000;
|
||||
const httpServer = app.listen(PORT, () => {
|
||||
log.info(`server on port:${PORT}`);
|
||||
});
|
||||
})
|
||||
.catch((err)=>console.error(err))
|
||||
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
|
||||
require('mongoose')
|
||||
.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/naturalcrit')
|
||||
.connection.on('error', ()=>{
|
||||
console.log('Error : Could not connect to a Mongo Database.');
|
||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||
});
|
||||
|
||||
|
||||
//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('<', '<').replaceAll('>', '>');
|
||||
return res.send(`<code><pre>${text}</pre></code>`);
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(404).send('Could not find Homebrew with that id');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
app.get('/user/:username', (req, res, next)=>{
|
||||
const fullAccess = req.account && (req.account.username == req.params.username);
|
||||
HomebrewModel.getByUser(req.params.username, fullAccess)
|
||||
.then((brews)=>{
|
||||
req.brews = brews;
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
app.get('/edit/:id', (req, res, next)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
req.brew = brew.sanatize();
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send(`Can't get that`);
|
||||
});
|
||||
});
|
||||
|
||||
//Share Page
|
||||
app.get('/share/:id', (req, res, next)=>{
|
||||
HomebrewModel.get({ shareId: req.params.id })
|
||||
.then((brew)=>{
|
||||
return brew.increaseView();
|
||||
})
|
||||
.then((brew)=>{
|
||||
req.brew = brew.sanatize(true);
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send(`Can't get that`);
|
||||
});
|
||||
});
|
||||
|
||||
//Print Page
|
||||
app.get('/print/:id', (req, res, next)=>{
|
||||
HomebrewModel.get({ shareId: req.params.id })
|
||||
.then((brew)=>{
|
||||
req.brew = brew.sanatize(true);
|
||||
return next();
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(400).send(`Can't get that`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//Render Page
|
||||
const render = require('vitreum/steps/render');
|
||||
const templateFn = require('./client/template.js');
|
||||
app.use((req, res)=>{
|
||||
render('homebrew', templateFn, {
|
||||
version : require('./package.json').version,
|
||||
url : req.originalUrl,
|
||||
welcomeText : welcomeText,
|
||||
changelog : changelogText,
|
||||
brew : req.brew,
|
||||
brews : req.brews,
|
||||
account : req.account
|
||||
})
|
||||
.then((page)=>{
|
||||
return res.send(page);
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const PORT = process.env.PORT || 8000;
|
||||
app.listen(PORT);
|
||||
console.log(`server on port:${PORT}`);
|
||||
85
server/admin.api.js
Normal file
85
server/admin.api.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const _ = require('lodash');
|
||||
const auth = require('basic-auth');
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const router = require('express').Router();
|
||||
|
||||
|
||||
const mw = {
|
||||
adminOnly : (req, res, next)=>{
|
||||
if(req.query && req.query.admin_key == process.env.ADMIN_KEY) return next();
|
||||
return res.status(401).send('Access denied');
|
||||
}
|
||||
};
|
||||
|
||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password';
|
||||
process.env.ADMIN_KEY = process.env.ADMIN_KEY || 'admin_key';
|
||||
|
||||
|
||||
|
||||
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
|
||||
router.get('/api/invalid', mw.adminOnly, (req, res)=>{
|
||||
const invalidBrewQuery = HomebrewModel.find({
|
||||
'$where' : 'this.text.length < 140',
|
||||
createdAt : {
|
||||
$lt : Moment().subtract(3, 'days').toDate()
|
||||
}
|
||||
});
|
||||
|
||||
if(req.query.do_it){
|
||||
invalidBrewQuery.remove().exec((err, objs)=>{
|
||||
refreshCount();
|
||||
return res.send(200);
|
||||
});
|
||||
} else {
|
||||
invalidBrewQuery.exec((err, objs)=>{
|
||||
if(err) console.log(err);
|
||||
return res.json({
|
||||
count : objs.length
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
|
||||
//search for mathcing edit id
|
||||
//search for matching share id
|
||||
// search for partial match
|
||||
|
||||
HomebrewModel.findOne({ $or : [
|
||||
{ editId: { '$regex': req.params.id, '$options': 'i' } },
|
||||
{ shareId: { '$regex': req.params.id, '$options': 'i' } },
|
||||
] }).exec((err, brew)=>{
|
||||
return res.json(brew);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
//Admin route
|
||||
|
||||
const render = require('vitreum/steps/render');
|
||||
const templateFn = require('../client/template.js');
|
||||
router.get('/admin', function(req, res){
|
||||
const credentials = auth(req);
|
||||
if(!credentials || credentials.name !== process.env.ADMIN_USER || credentials.pass !== process.env.ADMIN_PASS) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="example"');
|
||||
return res.status(401).send('Access denied');
|
||||
}
|
||||
render('admin', templateFn, {
|
||||
url : req.originalUrl,
|
||||
admin_key : process.env.ADMIN_KEY,
|
||||
})
|
||||
.then((page)=>{
|
||||
return res.send(page);
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.sendStatus(500);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,60 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const router = require('express').Router();
|
||||
const vitreumRender = require('vitreum/steps/render');
|
||||
const templateFn = require('../client/template.js');
|
||||
const config = require('nconf');
|
||||
const Moment = require('moment');
|
||||
|
||||
const mw = require('./middleware.js');
|
||||
const BrewData = require('./brew.data.js');
|
||||
|
||||
|
||||
const getInvalidBrewQuery = ()=>{
|
||||
return BrewData.model.find({
|
||||
'$where' : "this.text.length < 140",
|
||||
createdAt: {
|
||||
$lt: Moment().subtract(3, 'days').toDate()
|
||||
}
|
||||
}).select({ text : false });
|
||||
}
|
||||
|
||||
router.get('/admin', mw.adminLogin, (req, res, next) => {
|
||||
return vitreumRender('admin', templateFn, {
|
||||
url : req.originalUrl,
|
||||
admin_key : config.get('admin:key')
|
||||
})
|
||||
.then((page) => {
|
||||
return res.send(page)
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
|
||||
router.get('/admin/invalid', mw.adminOnly, (req, res, next)=>{
|
||||
getInvalidBrewQuery().exec()
|
||||
.then((brews) => {
|
||||
return res.json(brews);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
router.delete('/admin/invalid', mw.adminOnly, (req, res, next)=>{
|
||||
getInvalidBrewQuery().remove()
|
||||
.then(()=>{
|
||||
return res.status(200).send();
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
router.get('/admin/lookup/:search', mw.adminOnly, (req, res, next) => {
|
||||
BrewData.get({ $or:[
|
||||
//Searches for partial matches on either edit or share
|
||||
{editId : { "$regex": req.params.search, "$options": "i" }},
|
||||
{shareId : { "$regex": req.params.search, "$options": "i" }},
|
||||
]})
|
||||
.then((brews) => {
|
||||
return res.json(brews);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,28 +0,0 @@
|
||||
const config = require('nconf');
|
||||
const express = require("express");
|
||||
const app = express();
|
||||
|
||||
app.use(express.static(__dirname + '/../build'));
|
||||
app.use(require('body-parser').json({limit: '25mb'}));
|
||||
app.use(require('cookie-parser')());
|
||||
|
||||
|
||||
//Middleware
|
||||
const mw = require('./middleware.js');
|
||||
app.use(mw.account);
|
||||
app.use(mw.admin);
|
||||
|
||||
|
||||
//Routes
|
||||
app.use(require('./brew.api.js'));
|
||||
app.use(require('./interface.routes.js'));
|
||||
app.use(require('./admin.routes.js'));
|
||||
|
||||
if(config.get('NODE_ENV') !== 'staging' && config.get('NODE_ENV') !== 'production'){
|
||||
app.use(require('./dev.routes.js'));
|
||||
}
|
||||
|
||||
//Error Handler
|
||||
app.use(require('./error.js').expressHandler);
|
||||
|
||||
module.exports = app;
|
||||
@@ -1,68 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const router = require('express').Router();
|
||||
|
||||
const BrewData = require('./brew.data.js');
|
||||
const mw = require('./middleware.js');
|
||||
|
||||
//Search
|
||||
router.get('/api/brew', (req, res, next) => {
|
||||
const opts = _.pick(req.query, ['limit', 'sort', 'page']);
|
||||
|
||||
BrewData.termSearch(req.query.terms, opts, req.admin)
|
||||
.then((result) => {
|
||||
return res.status(200).json(result);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
//User
|
||||
router.get('/api/user/:username', (req, res, next) => {
|
||||
const fullAccess = req.admin ||
|
||||
!!(req.account && req.params.username == req.account.username);
|
||||
|
||||
BrewData.userSearch(req.params.username, fullAccess)
|
||||
.then((result) => {
|
||||
return res.status(200).json(result);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
//Get
|
||||
router.get('/api/brew/:shareId', mw.viewBrew, (req, res, next) => {
|
||||
return res.json(req.brew.toJSON());
|
||||
});
|
||||
|
||||
//Create
|
||||
router.post('/api/brew', (req, res, next)=>{
|
||||
const brew = req.body;
|
||||
if(req.account) brew.authors = [req.account.username];
|
||||
BrewData.create(brew)
|
||||
.then((brew) => {
|
||||
return res.json(brew.toJSON());
|
||||
})
|
||||
.catch(next)
|
||||
});
|
||||
|
||||
//Update
|
||||
router.put('/api/brew/:editId', mw.loadBrew, (req, res, next)=>{
|
||||
const brew = req.body || {};
|
||||
if(req.account){
|
||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
}
|
||||
BrewData.update(req.params.editId, brew)
|
||||
.then((brew) => {
|
||||
return res.json(brew.toJSON());
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
//Delete
|
||||
router.delete('/api/brew/:editId', mw.loadBrew, (req, res, next) => {
|
||||
BrewData.remove(req.params.editId)
|
||||
.then(()=>{
|
||||
return res.sendStatus(200);
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,102 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const shortid = require('shortid');
|
||||
const mongoose = require('./db.js').instance;
|
||||
|
||||
const Error = require('./error.js');
|
||||
const utils = require('./utils.js');
|
||||
|
||||
const BrewSchema = mongoose.Schema({
|
||||
shareId : {type : String, default: shortid.generate, index: { unique: true }},
|
||||
editId : {type : String, default: shortid.generate, index: { unique: true }},
|
||||
|
||||
text : {type : String, default : ""},
|
||||
style : {type : String, default : ""},
|
||||
|
||||
title : {type : String, default : ""},
|
||||
description : {type : String, default : ""},
|
||||
tags : {type : String, default : ""},
|
||||
thumbnail : {type : String, default : ""},
|
||||
systems : [String],
|
||||
authors : [String],
|
||||
published : {type : Boolean, default : false},
|
||||
|
||||
createdAt : { type: Date, default: Date.now },
|
||||
updatedAt : { type: Date, default: Date.now},
|
||||
lastViewed : { type: Date, default: Date.now},
|
||||
views : {type:Number, default:0},
|
||||
version : {type: Number, default:2}
|
||||
}, {
|
||||
versionKey: false,
|
||||
toJSON : {
|
||||
transform: (doc, ret, options) => {
|
||||
delete ret._id;
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Index these fields for fast text searching
|
||||
BrewSchema.index({ title: "text", description: "text" });
|
||||
|
||||
BrewSchema.methods.increaseView = function(){
|
||||
this.views = this.views + 1;
|
||||
return this.save();
|
||||
};
|
||||
|
||||
|
||||
const Brew = mongoose.model('Brew', BrewSchema);
|
||||
const BrewData = {
|
||||
schema : BrewSchema,
|
||||
model : Brew,
|
||||
|
||||
get : (query) => {
|
||||
return Brew.findOne(query).exec()
|
||||
.then((brew) => {
|
||||
if(!brew) throw Error.noBrew();
|
||||
return brew;
|
||||
});
|
||||
},
|
||||
create : (brew) => {
|
||||
delete brew.shareId;
|
||||
delete brew.editId;
|
||||
if(!brew.title) brew.title = utils.getGoodBrewTitle(brew.text);
|
||||
return (new Brew(brew)).save();
|
||||
},
|
||||
update : (editId, newBrew) => {
|
||||
return BrewData.get({ editId })
|
||||
.then((brew) => {
|
||||
delete newBrew.shareId;
|
||||
delete newBrew.editId;
|
||||
brew = _.merge(brew, newBrew, { updatedAt : Date.now() });
|
||||
|
||||
brew.markModified('authors');
|
||||
brew.markModified('systems');
|
||||
return brew.save();
|
||||
});
|
||||
},
|
||||
remove : (editId) => {
|
||||
return Brew.find({ editId }).remove().exec();
|
||||
},
|
||||
removeAll : ()=>{
|
||||
return Brew.find({}).remove().exec();
|
||||
},
|
||||
|
||||
//////// Special
|
||||
|
||||
getByShare : (shareId) => {
|
||||
return BrewData.get({ shareId : shareId})
|
||||
.then((brew) => {
|
||||
brew.increaseView();
|
||||
const brewJSON = brew.toJSON();
|
||||
delete brewJSON.editId;
|
||||
return brewJSON;
|
||||
});
|
||||
},
|
||||
getByEdit : (editId) => {
|
||||
return BrewData.get({ editId });
|
||||
},
|
||||
};
|
||||
|
||||
const BrewSearch = require('./brew.search.js')(Brew);
|
||||
|
||||
module.exports = _.merge(BrewData, BrewSearch);
|
||||
@@ -1,66 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = (Brew) => {
|
||||
const cmds = {
|
||||
termSearch : (terms='', opts, fullAccess) => {
|
||||
let query = {};
|
||||
if(terms){
|
||||
query = {$text: {
|
||||
//Wrap terms in quotes to perform an AND operation
|
||||
$search: _.map(terms.split(' '), (term)=>{
|
||||
return `\"${term}\"`;
|
||||
}).join(' '),
|
||||
$caseSensitive : false
|
||||
}};
|
||||
}
|
||||
return cmds.search(query, opts, fullAccess);
|
||||
},
|
||||
|
||||
userSearch : (username, fullAccess) => {
|
||||
const query = {
|
||||
authors : username
|
||||
};
|
||||
|
||||
return cmds.search(query, {}, fullAccess);
|
||||
},
|
||||
|
||||
search : (queryObj={}, options={}, fullAccess = true) => {
|
||||
const opts = _.merge({
|
||||
limit : 25,
|
||||
page : 0,
|
||||
sort : {}
|
||||
}, options);
|
||||
opts.limit = _.toNumber(opts.limit);
|
||||
opts.page = _.toNumber(opts.page);
|
||||
|
||||
let filter = {
|
||||
text : 0
|
||||
};
|
||||
|
||||
if(!fullAccess){
|
||||
filter.editId = 0;
|
||||
queryObj.published = true;
|
||||
}
|
||||
|
||||
const searchQuery = Brew
|
||||
.find(queryObj)
|
||||
.sort(opts.sort)
|
||||
.select(filter)
|
||||
.limit(opts.limit)
|
||||
.skip(opts.page * opts.limit)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
const countQuery = Brew.count(queryObj).exec();
|
||||
|
||||
return Promise.all([searchQuery, countQuery])
|
||||
.then((result) => {
|
||||
return {
|
||||
brews : result[0],
|
||||
total : result[1]
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return cmds;
|
||||
};
|
||||
36
server/db.js
36
server/db.js
@@ -1,36 +0,0 @@
|
||||
const log = require('loglevel');
|
||||
const mongoose = require('mongoose');
|
||||
mongoose.Promise = Promise;
|
||||
|
||||
const dbPath = process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/homebrewery';
|
||||
|
||||
module.exports = {
|
||||
connect : ()=>{
|
||||
return new Promise((resolve, reject)=>{
|
||||
if(mongoose.connection.readyState !== 0){
|
||||
log.warn('DB already connected');
|
||||
return resolve();
|
||||
}
|
||||
mongoose.connect(dbPath,
|
||||
(err) => {
|
||||
if(err){
|
||||
log.error('Error : Could not connect to a Mongo Database.');
|
||||
log.error(' If you are running locally, make sure mongodb.exe is running.');
|
||||
return reject(err);
|
||||
}
|
||||
log.info('DB connected.');
|
||||
return resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
close : ()=>{
|
||||
return new Promise((resolve, reject) => {
|
||||
mongoose.connection.close(()=>{
|
||||
log.info('DB connection closed.');
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
instance : mongoose
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
const router = require('express').Router();
|
||||
const jwt = require('jwt-simple');
|
||||
const auth = require('basic-auth');
|
||||
const config = require('nconf');
|
||||
|
||||
if(process.env.NODE_ENV == 'production') throw 'Loading dev routes in prod. ABORT ABORT';
|
||||
|
||||
|
||||
router.get('/dev/login', (req, res, next) => {
|
||||
const user = req.query.user;
|
||||
if(!user){
|
||||
return res.send(`
|
||||
<html>
|
||||
<body>dev login</body>
|
||||
<script>
|
||||
var user = prompt('enter username');
|
||||
if(user) window.location = '/dev/login?user=' + encodeURIComponent(user);
|
||||
</script></html>
|
||||
`);
|
||||
}
|
||||
res.cookie('nc_session', jwt.encode({username : req.query.user}, config.get('jwt_secret')));
|
||||
return res.redirect('/');
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
const Error = require('egads').extend('Server Error', 500, 'Generic Server Error');
|
||||
|
||||
Error.noBrew = Error.extend('Can not find a brew with that id', 404, 'No Brew Found');
|
||||
Error.noAuth = Error.extend('You can not access this route', 401, 'Unauthorized');
|
||||
Error.noAdmin = Error.extend('You need admin credentials to access this route', 401, 'Unauthorized');
|
||||
|
||||
|
||||
Error.expressHandler = (err, req, res, next) => {
|
||||
if(err instanceof Error){
|
||||
return res.status(err.status).send({
|
||||
type : err.name,
|
||||
message : err.message
|
||||
});
|
||||
}
|
||||
//If server error, print the whole stack for debugging
|
||||
return res.status(500).send({
|
||||
message : err.message,
|
||||
stack : err.stack
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
module.exports = Error;
|
||||
130
server/homebrew.api.js
Normal file
130
server/homebrew.api.js
Normal file
@@ -0,0 +1,130 @@
|
||||
const _ = require('lodash');
|
||||
const HomebrewModel = require('./homebrew.model.js').model;
|
||||
const router = require('express').Router();
|
||||
|
||||
// const getTopBrews = (cb)=>{
|
||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||
// cb(brews);
|
||||
// });
|
||||
// };
|
||||
|
||||
const getGoodBrewTitle = (text)=>{
|
||||
const titlePos = text.indexOf('# ');
|
||||
if(titlePos !== -1){
|
||||
const ending = text.indexOf('\n', titlePos);
|
||||
return text.substring(titlePos + 2, ending);
|
||||
} else {
|
||||
return _.find(text.split('\n'), (line)=>{
|
||||
return line;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
router.post('/api', (req, res)=>{
|
||||
|
||||
let authors = [];
|
||||
if(req.account) authors = [req.account.username];
|
||||
|
||||
const newHomebrew = new HomebrewModel(_.merge({},
|
||||
req.body,
|
||||
{ authors: authors }
|
||||
));
|
||||
if(!newHomebrew.title){
|
||||
newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
|
||||
}
|
||||
newHomebrew.save((err, obj)=>{
|
||||
if(err){
|
||||
console.error(err, err.toString(), err.stack);
|
||||
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
|
||||
}
|
||||
return res.json(obj);
|
||||
});
|
||||
});
|
||||
|
||||
router.put('/api/update/:id', (req, res)=>{
|
||||
HomebrewModel.get({ editId: req.params.id })
|
||||
.then((brew)=>{
|
||||
brew = _.merge(brew, req.body);
|
||||
brew.updatedAt = new Date();
|
||||
if(req.account) brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||
|
||||
brew.markModified('authors');
|
||||
brew.markModified('systems');
|
||||
|
||||
brew.save((err, obj)=>{
|
||||
if(err) throw err;
|
||||
return res.status(200).send(obj);
|
||||
});
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
return res.status(500).send('Error while saving');
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/api/remove/:id', (req, res)=>{
|
||||
HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{
|
||||
if(!objs.length || err) return res.status(404).send('Can not find homebrew with that id');
|
||||
const resEntry = objs[0];
|
||||
resEntry.remove((err)=>{
|
||||
if(err) return res.status(500).send('Error while removing');
|
||||
return res.status(200).send();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
|
||||
module.exports = function(app){
|
||||
|
||||
app;
|
||||
|
||||
|
||||
|
||||
|
||||
app.get('/api/search', mw.adminOnly, function(req, res){
|
||||
|
||||
var page = req.query.page || 0;
|
||||
var count = req.query.count || 20;
|
||||
|
||||
var query = {};
|
||||
if(req.query && req.query.id){
|
||||
query = {
|
||||
"$or" : [{
|
||||
editId : req.query.id
|
||||
},{
|
||||
shareId : req.query.id
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
HomebrewModel.find(query, {
|
||||
text : 0 //omit the text
|
||||
}, {
|
||||
skip: page*count,
|
||||
limit: count*1
|
||||
}, function(err, objs){
|
||||
if(err) console.log(err);
|
||||
return res.json({
|
||||
page : page,
|
||||
count : count,
|
||||
total : homebrewTotal,
|
||||
brews : objs
|
||||
});
|
||||
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
return app;
|
||||
}
|
||||
*/
|
||||
81
server/homebrew.model.js
Normal file
81
server/homebrew.model.js
Normal file
@@ -0,0 +1,81 @@
|
||||
const mongoose = require('mongoose');
|
||||
const shortid = require('shortid');
|
||||
const _ = require('lodash');
|
||||
|
||||
const HomebrewSchema = mongoose.Schema({
|
||||
shareId : { type: String, default: shortid.generate, index: { unique: true } },
|
||||
editId : { type: String, default: shortid.generate, index: { unique: true } },
|
||||
title : { type: String, default: '' },
|
||||
text : { type: String, default: '' },
|
||||
|
||||
description : { type: String, default: '' },
|
||||
tags : { type: String, default: '' },
|
||||
systems : [String],
|
||||
authors : [String],
|
||||
published : { type: Boolean, default: false },
|
||||
|
||||
createdAt : { type: Date, default: Date.now },
|
||||
updatedAt : { type: Date, default: Date.now },
|
||||
lastViewed : { type: Date, default: Date.now },
|
||||
views : { type: Number, default: 0 },
|
||||
version : { type: Number, default: 1 }
|
||||
}, { versionKey: false });
|
||||
|
||||
|
||||
|
||||
HomebrewSchema.methods.sanatize = function(full=false){
|
||||
const brew = this.toJSON();
|
||||
delete brew._id;
|
||||
delete brew.__v;
|
||||
if(full){
|
||||
delete brew.editId;
|
||||
}
|
||||
return brew;
|
||||
};
|
||||
|
||||
|
||||
HomebrewSchema.methods.increaseView = function(){
|
||||
return new Promise((resolve, reject)=>{
|
||||
this.lastViewed = new Date();
|
||||
this.views = this.views + 1;
|
||||
this.save((err)=>{
|
||||
if(err) return reject(err);
|
||||
return resolve(this);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
HomebrewSchema.statics.get = function(query){
|
||||
return new Promise((resolve, reject)=>{
|
||||
Homebrew.find(query, (err, brews)=>{
|
||||
if(err || !brews.length) return reject('Can not find brew');
|
||||
return resolve(brews[0]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
|
||||
return new Promise((resolve, reject)=>{
|
||||
const query = { authors: username, published: true };
|
||||
if(allowAccess){
|
||||
delete query.published;
|
||||
}
|
||||
Homebrew.find(query, (err, brews)=>{
|
||||
if(err) return reject('Can not find brew');
|
||||
return resolve(_.map(brews, (brew)=>{
|
||||
return brew.sanatize(!allowAccess);
|
||||
}));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||
|
||||
module.exports = {
|
||||
schema : HomebrewSchema,
|
||||
model : Homebrew,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user