mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-13 21:42:45 +00:00
newPage is now working, working on editpage
This commit is contained in:
@@ -1,145 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const Markdown = require('homebrewery/markdown.js');
|
|
||||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
|
||||||
|
|
||||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx')
|
|
||||||
const Store = require('homebrewery/brew.store.js');
|
|
||||||
|
|
||||||
|
|
||||||
const PAGE_HEIGHT = 1056;
|
|
||||||
const PPR_THRESHOLD = 50;
|
|
||||||
|
|
||||||
const BrewRenderer = React.createClass({
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
brewText : '',
|
|
||||||
errors : []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
const pages = this.props.brewText.split('\\page');
|
|
||||||
|
|
||||||
return {
|
|
||||||
viewablePageNumber: 0,
|
|
||||||
height : 0,
|
|
||||||
isMounted : false,
|
|
||||||
pages : pages,
|
|
||||||
usePPR : pages.length >= PPR_THRESHOLD
|
|
||||||
};
|
|
||||||
},
|
|
||||||
height : 0,
|
|
||||||
pageHeight : PAGE_HEIGHT,
|
|
||||||
lastRender : <div></div>,
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.updateSize();
|
|
||||||
window.addEventListener("resize", this.updateSize);
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
window.removeEventListener("resize", this.updateSize);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillReceiveProps: function(nextProps) {
|
|
||||||
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
|
|
||||||
|
|
||||||
const pages = nextProps.brewText.split('\\page');
|
|
||||||
this.setState({
|
|
||||||
pages : pages,
|
|
||||||
usePPR : pages.length >= PPR_THRESHOLD
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
updateSize : function() {
|
|
||||||
setTimeout(()=>{
|
|
||||||
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
|
|
||||||
}, 1);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
height : this.refs.main.parentNode.clientHeight,
|
|
||||||
isMounted : true
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleScroll : function(e){
|
|
||||||
this.setState({
|
|
||||||
viewablePageNumber : Math.floor(e.target.scrollTop / this.pageHeight)
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
shouldRender : function(pageText, index){
|
|
||||||
if(!this.state.isMounted) return false;
|
|
||||||
|
|
||||||
var viewIndex = this.state.viewablePageNumber;
|
|
||||||
if(index == viewIndex - 1) return true;
|
|
||||||
if(index == viewIndex) return true;
|
|
||||||
if(index == viewIndex + 1) return true;
|
|
||||||
|
|
||||||
//Check for style tages
|
|
||||||
if(pageText.indexOf('<style>') !== -1) return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPageInfo : function(){
|
|
||||||
return <div className='pageInfo'>
|
|
||||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPPRmsg : function(){
|
|
||||||
if(!this.state.usePPR) return;
|
|
||||||
|
|
||||||
return <div className='ppr_msg'>
|
|
||||||
Partial Page Renderer enabled, because your brew is so large. May effect rendering.
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDummyPage : function(index){
|
|
||||||
return <div className='phb' id={`p${index + 1}`} key={index}>
|
|
||||||
<i className='fa fa-spinner fa-spin' />
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
|
||||||
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{__html:Markdown.render(pageText)}} key={index} />
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPages : function(){
|
|
||||||
if(this.state.usePPR){
|
|
||||||
return _.map(this.state.pages, (page, index)=>{
|
|
||||||
if(this.shouldRender(page, index)){
|
|
||||||
return this.renderPage(page, index);
|
|
||||||
}else{
|
|
||||||
return this.renderDummyPage(index);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if(this.props.errors && this.props.errors.length) return this.lastRender;
|
|
||||||
this.lastRender = _.map(this.state.pages, (page, index)=>{
|
|
||||||
return this.renderPage(page, index);
|
|
||||||
});
|
|
||||||
return this.lastRender;
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
|
||||||
return <div className='brewRenderer'
|
|
||||||
onScroll={this.handleScroll}
|
|
||||||
ref='main'
|
|
||||||
style={{height : this.state.height}}>
|
|
||||||
|
|
||||||
<ErrorBar errors={this.props.errors} />
|
|
||||||
<RenderWarnings />
|
|
||||||
|
|
||||||
<div className='pages' ref='pages'>
|
|
||||||
{this.renderPages()}
|
|
||||||
</div>
|
|
||||||
{this.renderPageInfo()}
|
|
||||||
{this.renderPPRmsg()}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = BrewRenderer;
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
|
|
||||||
@import (less) './client/homebrew/phbStyle/phb.style.less';
|
|
||||||
.pane{
|
|
||||||
position : relative;
|
|
||||||
}
|
|
||||||
.brewRenderer{
|
|
||||||
overflow-y : scroll;
|
|
||||||
.pageInfo{
|
|
||||||
position : absolute;
|
|
||||||
right : 17px;
|
|
||||||
bottom : 0;
|
|
||||||
z-index : 1000;
|
|
||||||
padding : 8px 10px;
|
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
}
|
|
||||||
.ppr_msg{
|
|
||||||
position : absolute;
|
|
||||||
left : 0px;
|
|
||||||
bottom : 0;
|
|
||||||
z-index : 1000;
|
|
||||||
padding : 8px 10px;
|
|
||||||
background-color : #333;
|
|
||||||
font-size : 10px;
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
}
|
|
||||||
.pages{
|
|
||||||
margin : 30px 0px;
|
|
||||||
&>.phb{
|
|
||||||
margin-right : auto;
|
|
||||||
margin-bottom : 30px;
|
|
||||||
margin-left : auto;
|
|
||||||
box-shadow : 1px 4px 14px #000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
const Store = require('homebrewery/brew.store.js');
|
|
||||||
const BrewRenderer = require('./brewRenderer.jsx');
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = Store.createSmartComponent(BrewRenderer, () => {
|
|
||||||
return {
|
|
||||||
brewText : Store.getBrewText(),
|
|
||||||
errors : Store.getErrors()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
var React = require('react');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var cx = require('classnames');
|
|
||||||
|
|
||||||
var ErrorBar = React.createClass({
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
errors : []
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
hasOpenError : false,
|
|
||||||
hasCloseError : false,
|
|
||||||
hasMatchError : false,
|
|
||||||
|
|
||||||
renderErrors : function(){
|
|
||||||
this.hasOpenError = false;
|
|
||||||
this.hasCloseError = false;
|
|
||||||
this.hasMatchError = false;
|
|
||||||
|
|
||||||
|
|
||||||
var 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>
|
|
||||||
});
|
|
||||||
|
|
||||||
return <ul>{errors}</ul>
|
|
||||||
},
|
|
||||||
|
|
||||||
renderProtip : function(){
|
|
||||||
var 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!
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasCloseError){
|
|
||||||
msg.push(<div>
|
|
||||||
An unmatched closing tag means you closed a tag without opening it. Either remove it, you check to where you think you opened it.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(this.hasMatchError){
|
|
||||||
msg.push(<div>
|
|
||||||
A type mismatch means you closed a tag, but the last open tag was a different type.
|
|
||||||
</div>);
|
|
||||||
}
|
|
||||||
return <div className='protips'>
|
|
||||||
<h4>Protips!</h4>
|
|
||||||
{msg}
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
|
||||||
if(!this.props.errors.length) return null;
|
|
||||||
|
|
||||||
return <div className='errorBar'>
|
|
||||||
<i className='fa fa-exclamation-triangle' />
|
|
||||||
<h3> There are HTML errors in your markup</h3>
|
|
||||||
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
|
|
||||||
{this.renderErrors()}
|
|
||||||
<hr />
|
|
||||||
{this.renderProtip()}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = ErrorBar;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
|
|
||||||
.errorBar{
|
|
||||||
position : absolute;
|
|
||||||
z-index : 10000;
|
|
||||||
box-sizing : border-box;
|
|
||||||
width : 100%;
|
|
||||||
margin-right : 13px;
|
|
||||||
padding : 20px;
|
|
||||||
padding-bottom : 10px;
|
|
||||||
padding-left : 100px;
|
|
||||||
background-color : @red;
|
|
||||||
color : white;
|
|
||||||
i{
|
|
||||||
position : absolute;
|
|
||||||
left : 30px;
|
|
||||||
opacity : 0.8;
|
|
||||||
font-size : 3em;
|
|
||||||
}
|
|
||||||
h3{
|
|
||||||
font-size : 1.1em;
|
|
||||||
font-weight : 800;
|
|
||||||
}
|
|
||||||
ul{
|
|
||||||
margin-top : 15px;
|
|
||||||
font-size : 0.8em;
|
|
||||||
list-style-position : inside;
|
|
||||||
list-style-type : disc;
|
|
||||||
li{
|
|
||||||
line-height : 1.6em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hr{
|
|
||||||
box-sizing : border-box;
|
|
||||||
height : 2px;
|
|
||||||
width : 150%;
|
|
||||||
margin-top : 25px;
|
|
||||||
margin-bottom : 15px;
|
|
||||||
margin-left : -100px;
|
|
||||||
background-color : darken(@red, 8%);
|
|
||||||
border : none;
|
|
||||||
}
|
|
||||||
small{
|
|
||||||
font-size: 0.6em;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
.protips{
|
|
||||||
margin-left : -80px;
|
|
||||||
font-size : 0.6em;
|
|
||||||
&>div{
|
|
||||||
margin-bottom : 10px;
|
|
||||||
line-height : 1.2em;
|
|
||||||
}
|
|
||||||
h4{
|
|
||||||
opacity : 0.8;
|
|
||||||
font-weight : 800;
|
|
||||||
line-height : 1.5em;
|
|
||||||
text-transform : uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
|
||||||
|
|
||||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
|
||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
|
||||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
|
||||||
|
|
||||||
const splice = function(str, index, inject){
|
|
||||||
return str.slice(0, index) + inject + str.slice(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SNIPPETBAR_HEIGHT = 25;
|
|
||||||
|
|
||||||
const Editor = React.createClass({
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
value : '',
|
|
||||||
onChange : ()=>{},
|
|
||||||
|
|
||||||
metadata : {},
|
|
||||||
onMetadataChange : ()=>{},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
getInitialState: function() {
|
|
||||||
return {
|
|
||||||
showMetadataEditor: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
cursorPosition : {
|
|
||||||
line : 0,
|
|
||||||
ch : 0
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.updateEditorSize();
|
|
||||||
this.highlightPageLines();
|
|
||||||
window.addEventListener("resize", this.updateEditorSize);
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
window.removeEventListener("resize", this.updateEditorSize);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateEditorSize : function() {
|
|
||||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
|
||||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
|
||||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
|
||||||
this.props.onChange(text);
|
|
||||||
},
|
|
||||||
handleCursorActivty : function(curpos){
|
|
||||||
this.cursorPosition = curpos;
|
|
||||||
},
|
|
||||||
handleInject : function(injectText){
|
|
||||||
const lines = this.props.value.split('\n');
|
|
||||||
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
|
|
||||||
|
|
||||||
this.handleTextChange(lines.join('\n'));
|
|
||||||
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
|
|
||||||
},
|
|
||||||
handgleToggle : function(){
|
|
||||||
this.setState({
|
|
||||||
showMetadataEditor : !this.state.showMetadataEditor
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
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,29 +0,0 @@
|
|||||||
|
|
||||||
.editor{
|
|
||||||
position : relative;
|
|
||||||
width : 100%;
|
|
||||||
|
|
||||||
.codeEditor{
|
|
||||||
height : 100%;
|
|
||||||
.pageLine{
|
|
||||||
background-color : fade(#333, 15%);
|
|
||||||
border-bottom : #333 solid 1px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.brewJump{
|
|
||||||
position: absolute;
|
|
||||||
background-color: @teal;
|
|
||||||
cursor: pointer;
|
|
||||||
width : 30px;
|
|
||||||
height : 30px;
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
bottom : 20px;
|
|
||||||
right : 20px;
|
|
||||||
z-index: 1000000;
|
|
||||||
justify-content:center;
|
|
||||||
.tooltipLeft("Jump to brew page");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
const Actions = require('homebrewery/brew.actions.js');
|
|
||||||
const Store = require('homebrewery/brew.store.js');
|
|
||||||
|
|
||||||
const Editor = require('./editor.jsx')
|
|
||||||
|
|
||||||
module.exports = Store.createSmartComponent(Editor, ()=>{
|
|
||||||
return {
|
|
||||||
value : Store.getBrewText(),
|
|
||||||
onChange : Actions.updateBrewText,
|
|
||||||
metadata : Store.getMetaData(),
|
|
||||||
onMetadataChange : Actions.updateMetaData,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const _ = require('lodash');
|
|
||||||
const cx = require('classnames');
|
|
||||||
const request = require("superagent");
|
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']
|
|
||||||
|
|
||||||
const MetadataEditor = React.createClass({
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {
|
|
||||||
metadata: {
|
|
||||||
editId : null,
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
tags : '',
|
|
||||||
published : false,
|
|
||||||
authors : [],
|
|
||||||
systems : []
|
|
||||||
},
|
|
||||||
onChange : ()=>{}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
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{
|
|
||||||
this.props.metadata.systems = _.without(this.props.metadata.systems, system);
|
|
||||||
}
|
|
||||||
this.props.onChange(this.props.metadata);
|
|
||||||
},
|
|
||||||
handlePublish : function(val){
|
|
||||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
|
||||||
published : val
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
request.get('/api/remove/' + this.props.metadata.editId)
|
|
||||||
.send()
|
|
||||||
.end(function(err, res){
|
|
||||||
window.location.href = '/';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getRedditLink : function(){
|
|
||||||
const meta = this.props.metadata;
|
|
||||||
const title = `${meta.title} [${meta.systems.join(' ')}]`;
|
|
||||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
|
||||||
|
|
||||||
**[Homebrewery Link](http://homebrewery.naturalcrit.com/share/${meta.shareId})**`;
|
|
||||||
|
|
||||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderSystems : function(){
|
|
||||||
return _.map(SYSTEMS, (val)=>{
|
|
||||||
return <label key={val}>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
checked={_.includes(this.props.metadata.systems, val)}
|
|
||||||
onChange={this.handleSystem.bind(null, val)} />
|
|
||||||
{val}
|
|
||||||
</label>
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPublish : function(){
|
|
||||||
if(this.props.metadata.published){
|
|
||||||
return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}>
|
|
||||||
<i className='fa fa-ban' /> unpublish
|
|
||||||
</button>
|
|
||||||
}else{
|
|
||||||
return <button className='publish' onClick={this.handlePublish.bind(null, true)}>
|
|
||||||
<i className='fa fa-globe' /> publish
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderDelete : function(){
|
|
||||||
if(!this.props.metadata.editId) return;
|
|
||||||
|
|
||||||
return <div className='field delete'>
|
|
||||||
<label>delete</label>
|
|
||||||
<div className='value'>
|
|
||||||
<button className='publish' onClick={this.handleDelete}>
|
|
||||||
<i className='fa fa-trash' /> delete brew
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
|
|
||||||
renderAuthors : function(){
|
|
||||||
let text = 'None.';
|
|
||||||
if(this.props.metadata.authors.length){
|
|
||||||
text = this.props.metadata.authors.join(', ');
|
|
||||||
}
|
|
||||||
return <div className='field authors'>
|
|
||||||
<label>authors</label>
|
|
||||||
<div className='value'>
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
|
|
||||||
renderShareToReddit : function(){
|
|
||||||
if(!this.props.metadata.shareId) return;
|
|
||||||
|
|
||||||
return <div className='field reddit'>
|
|
||||||
<label>reddit</label>
|
|
||||||
<div className='value'>
|
|
||||||
<a href={this.getRedditLink()} target='_blank'>
|
|
||||||
<button className='publish'>
|
|
||||||
<i className='fa fa-reddit-alien' /> share to reddit
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
|
||||||
return <div className='metadataEditor'>
|
|
||||||
<div className='field title'>
|
|
||||||
<label>title</label>
|
|
||||||
<input type='text' className='value'
|
|
||||||
value={this.props.metadata.title}
|
|
||||||
onChange={this.handleFieldChange.bind(null, 'title')} />
|
|
||||||
</div>
|
|
||||||
<div className='field description'>
|
|
||||||
<label>description</label>
|
|
||||||
<textarea value={this.props.metadata.description} className='value'
|
|
||||||
onChange={this.handleFieldChange.bind(null, 'description')} />
|
|
||||||
</div>
|
|
||||||
{/*}
|
|
||||||
<div className='field tags'>
|
|
||||||
<label>tags</label>
|
|
||||||
<textarea value={this.props.metadata.tags}
|
|
||||||
onChange={this.handleFieldChange.bind(null, 'tags')} />
|
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<div className='field systems'>
|
|
||||||
<label>systems</label>
|
|
||||||
<div className='value'>
|
|
||||||
{this.renderSystems()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.renderAuthors()}
|
|
||||||
|
|
||||||
<div className='field publish'>
|
|
||||||
<label>publish</label>
|
|
||||||
<div className='value'>
|
|
||||||
{this.renderPublish()}
|
|
||||||
<small>Published homebrews will be publicly viewable and searchable (eventually...)</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{this.renderShareToReddit()}
|
|
||||||
|
|
||||||
{this.renderDelete()}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = MetadataEditor;
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
|
|
||||||
.metadataEditor{
|
|
||||||
position : absolute;
|
|
||||||
z-index : 10000;
|
|
||||||
box-sizing : border-box;
|
|
||||||
width : 100%;
|
|
||||||
padding : 25px;
|
|
||||||
background-color : #999;
|
|
||||||
.field{
|
|
||||||
display : flex;
|
|
||||||
width : 100%;
|
|
||||||
margin-bottom : 10px;
|
|
||||||
&>label{
|
|
||||||
display : inline-block;
|
|
||||||
vertical-align : top;
|
|
||||||
width : 80px;
|
|
||||||
font-size : 0.7em;
|
|
||||||
font-weight : 800;
|
|
||||||
line-height : 1.8em;
|
|
||||||
text-transform : uppercase;
|
|
||||||
flex-grow : 0;
|
|
||||||
}
|
|
||||||
&>.value{
|
|
||||||
flex-grow : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.description.field textarea.value{
|
|
||||||
resize : none;
|
|
||||||
height : 5em;
|
|
||||||
font-family : 'Open Sans', sans-serif;
|
|
||||||
font-size : 0.8em;
|
|
||||||
}
|
|
||||||
.systems.field .value{
|
|
||||||
label{
|
|
||||||
vertical-align : middle;
|
|
||||||
margin-right : 15px;
|
|
||||||
cursor : pointer;
|
|
||||||
font-size : 0.7em;
|
|
||||||
font-weight : 800;
|
|
||||||
user-select : none;
|
|
||||||
}
|
|
||||||
input{
|
|
||||||
vertical-align : middle;
|
|
||||||
cursor : pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.publish.field .value{
|
|
||||||
position : relative;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
button.publish{
|
|
||||||
.button(@blueLight);
|
|
||||||
}
|
|
||||||
button.unpublish{
|
|
||||||
.button(@silver);
|
|
||||||
}
|
|
||||||
small{
|
|
||||||
position : absolute;
|
|
||||||
bottom : -15px;
|
|
||||||
left : 0px;
|
|
||||||
font-size : 0.6em;
|
|
||||||
font-style : italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete.field .value{
|
|
||||||
button{
|
|
||||||
.button(@red);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.reddit.field .value{
|
|
||||||
button{
|
|
||||||
.button(@purple);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.authors.field .value{
|
|
||||||
font-size: 0.8em;
|
|
||||||
line-height : 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
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 = React.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 = React.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.bind(this, 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>
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
module.exports = function(classname){
|
|
||||||
|
|
||||||
classname = classname || _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
|
|
||||||
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge'])
|
|
||||||
|
|
||||||
classname = classname.toLowerCase();
|
|
||||||
|
|
||||||
var hitDie = _.sample([4, 6, 8, 10, 12]);
|
|
||||||
|
|
||||||
var abilityList = ["Strength", "Dexerity", "Constitution", "Wisdom", "Charisma", "Intelligence"];
|
|
||||||
var skillList = ["Acrobatics ", "Animal Handling", "Arcana", "Athletics", "Deception", "History", "Insight", "Intimidation", "Investigation", "Medicine", "Nature", "Perception", "Performance", "Persuasion", "Religion", "Sleight of Hand", "Stealth", "Survival"];
|
|
||||||
|
|
||||||
|
|
||||||
return [
|
|
||||||
"## Class Features",
|
|
||||||
"As a " + classname + ", you gain the following class features",
|
|
||||||
"#### Hit Points",
|
|
||||||
"___",
|
|
||||||
"- **Hit Dice:** 1d" + hitDie + " per " + classname + " level",
|
|
||||||
"- **Hit Points at 1st Level:** " + hitDie + " + your Constitution modifier",
|
|
||||||
"- **Hit Points at Higher Levels:** 1d" + hitDie + " (or " + (hitDie/2 + 1) + ") + your Constitution modifier per " + classname + " level after 1st",
|
|
||||||
"",
|
|
||||||
"#### Proficiencies",
|
|
||||||
"___",
|
|
||||||
"- **Armor:** " + (_.sampleSize(["Light armor", "Medium armor", "Heavy armor", "Shields"], _.random(0,3)).join(', ') || "None"),
|
|
||||||
"- **Weapons:** " + (_.sampleSize(["Squeegee", "Rubber Chicken", "Simple weapons", "Martial weapons"], _.random(0,2)).join(', ') || "None"),
|
|
||||||
"- **Tools:** " + (_.sampleSize(["Artian's tools", "one musical instrument", "Thieve's tools"], _.random(0,2)).join(', ') || "None"),
|
|
||||||
"",
|
|
||||||
"___",
|
|
||||||
"- **Saving Throws:** " + (_.sampleSize(abilityList, 2).join(', ')),
|
|
||||||
"- **Skills:** Choose two from " + (_.sampleSize(skillList, _.random(4, 6)).join(', ')),
|
|
||||||
"",
|
|
||||||
"#### Equipment",
|
|
||||||
"You start with the following equipment, in addition to the equipment granted by your background:",
|
|
||||||
"- *(a)* a martial weapon and a shield or *(b)* two martial weapons",
|
|
||||||
"- *(a)* five javelins or *(b)* any simple melee weapon",
|
|
||||||
"- " + (_.sample(["10 lint fluffs", "1 button", "a cherished lost sock"])),
|
|
||||||
"\n\n\n"
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var features = [
|
|
||||||
"Astrological Botany",
|
|
||||||
"Astrological Chemistry",
|
|
||||||
"Biochemical Sorcery",
|
|
||||||
"Civil Alchemy",
|
|
||||||
"Consecrated Biochemistry",
|
|
||||||
"Demonic Anthropology",
|
|
||||||
"Divinatory Mineralogy",
|
|
||||||
"Genetic Banishing",
|
|
||||||
"Hermetic Geography",
|
|
||||||
"Immunological Incantations",
|
|
||||||
"Nuclear Illusionism",
|
|
||||||
"Ritual Astronomy",
|
|
||||||
"Seismological Divination",
|
|
||||||
"Spiritual Biochemistry",
|
|
||||||
"Statistical Occultism",
|
|
||||||
"Police Necromancer",
|
|
||||||
"Sixgun Poisoner",
|
|
||||||
"Pharmaceutical Gunslinger",
|
|
||||||
"Infernal Banker",
|
|
||||||
"Spell Analyst",
|
|
||||||
"Gunslinger Corruptor",
|
|
||||||
"Torque Interfacer",
|
|
||||||
"Exo Interfacer",
|
|
||||||
"Gunpowder Torturer",
|
|
||||||
"Orbital Gravedigger",
|
|
||||||
"Phased Linguist",
|
|
||||||
"Mathematical Pharmacist",
|
|
||||||
"Plasma Outlaw",
|
|
||||||
"Malefic Chemist",
|
|
||||||
"Police Cultist"
|
|
||||||
];
|
|
||||||
|
|
||||||
var classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
|
||||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
|
|
||||||
|
|
||||||
var levels = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", "20th"]
|
|
||||||
|
|
||||||
var profBonus = [2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6];
|
|
||||||
|
|
||||||
var getFeature = (level)=>{
|
|
||||||
var res = []
|
|
||||||
if(_.includes([4,6,8,12,14,16,19], level+1)){
|
|
||||||
res = ["Ability Score Improvement"]
|
|
||||||
}
|
|
||||||
res = _.union(res, _.sampleSize(features, _.sample([0,1,1,1,1,1])));
|
|
||||||
if(!res.length) return "─";
|
|
||||||
return res.join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
full : function(){
|
|
||||||
var classname = _.sample(classnames)
|
|
||||||
|
|
||||||
var maxes = [4,3,3,3,3,2,2,1,1]
|
|
||||||
var drawSlots = function(Slots){
|
|
||||||
var slots = Number(Slots);
|
|
||||||
return _.times(9, function(i){
|
|
||||||
var max = maxes[i];
|
|
||||||
if(slots < 1) return "—";
|
|
||||||
var res = _.min([max, slots]);
|
|
||||||
slots -= res;
|
|
||||||
return res;
|
|
||||||
}).join(' | ')
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var cantrips = 3;
|
|
||||||
var spells = 1;
|
|
||||||
var slots = 2;
|
|
||||||
return "<div class='classTable wide'>\n##### The " + classname + "\n" +
|
|
||||||
"| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n"+
|
|
||||||
"|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n" +
|
|
||||||
_.map(levels, function(levelName, level){
|
|
||||||
var res = [
|
|
||||||
levelName,
|
|
||||||
"+" + profBonus[level],
|
|
||||||
getFeature(level),
|
|
||||||
cantrips,
|
|
||||||
spells,
|
|
||||||
drawSlots(slots)
|
|
||||||
].join(' | ');
|
|
||||||
|
|
||||||
cantrips += _.random(0,1);
|
|
||||||
spells += _.random(0,1);
|
|
||||||
slots += _.random(0,2);
|
|
||||||
|
|
||||||
return "| " + res + " |";
|
|
||||||
}).join('\n') +'\n</div>\n\n';
|
|
||||||
},
|
|
||||||
|
|
||||||
half : function(){
|
|
||||||
var classname = _.sample(classnames)
|
|
||||||
|
|
||||||
var featureScore = 1
|
|
||||||
return "<div class='classTable'>\n##### The " + classname + "\n" +
|
|
||||||
"| Level | Proficiency Bonus | Features | " + _.sample(features) + "|\n" +
|
|
||||||
"|:---:|:---:|:---|:---:|\n" +
|
|
||||||
_.map(levels, function(levelName, level){
|
|
||||||
var res = [
|
|
||||||
levelName,
|
|
||||||
"+" + profBonus[level],
|
|
||||||
getFeature(level),
|
|
||||||
"+" + featureScore
|
|
||||||
].join(' | ');
|
|
||||||
|
|
||||||
featureScore += _.random(0,1);
|
|
||||||
|
|
||||||
return "| " + res + " |";
|
|
||||||
}).join('\n') +'\n</div>\n\n';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var titles = [
|
|
||||||
"The Burning Gallows",
|
|
||||||
"The Ring of Nenlast",
|
|
||||||
"Below the Blind Tavern",
|
|
||||||
"Below the Hungering River",
|
|
||||||
"Before Bahamut's Land",
|
|
||||||
"The Cruel Grave from Within",
|
|
||||||
"The Strength of Trade Road",
|
|
||||||
"Through The Raven Queen's Worlds",
|
|
||||||
"Within the Settlement",
|
|
||||||
"The Crown from Within",
|
|
||||||
"The Merchant Within the Battlefield",
|
|
||||||
"Ioun's Fading Traveler",
|
|
||||||
"The Legion Ingredient",
|
|
||||||
"The Explorer Lure",
|
|
||||||
"Before the Charming Badlands",
|
|
||||||
"The Living Dead Above the Fearful Cage",
|
|
||||||
"Vecna's Hidden Sage",
|
|
||||||
"Bahamut's Demonspawn",
|
|
||||||
"Across Gruumsh's Elemental Chaos",
|
|
||||||
"The Blade of Orcus",
|
|
||||||
"Beyond Revenge",
|
|
||||||
"Brain of Insanity",
|
|
||||||
"Breed Battle!, A New Beginning",
|
|
||||||
"Evil Lake, A New Beginning",
|
|
||||||
"Invasion of the Gigantic Cat, Part II",
|
|
||||||
"Kraken War 2020",
|
|
||||||
"The Body Whisperers",
|
|
||||||
"The Diabolical Tales of the Ape-Women",
|
|
||||||
"The Doctor Immortal",
|
|
||||||
"The Doctor from Heaven",
|
|
||||||
"The Graveyard",
|
|
||||||
"Azure Core",
|
|
||||||
"Core Battle",
|
|
||||||
"Core of Heaven: The Guardian of Amazement",
|
|
||||||
"Deadly Amazement III",
|
|
||||||
"Dry Chaos IX",
|
|
||||||
"Gate Thunder",
|
|
||||||
"Guardian: Skies of the Dark Wizard",
|
|
||||||
"Lute of Eternity",
|
|
||||||
"Mercury's Planet: Brave Evolution",
|
|
||||||
"Ruby of Atlantis: The Quake of Peace",
|
|
||||||
"Sky of Zelda: The Thunder of Force",
|
|
||||||
"Vyse's Skies",
|
|
||||||
"White Greatness III",
|
|
||||||
"Yellow Divinity",
|
|
||||||
"Zidane's Ghost"
|
|
||||||
];
|
|
||||||
|
|
||||||
var subtitles = [
|
|
||||||
"In an ominous universe, a botanist opposes terrorism.",
|
|
||||||
"In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.",
|
|
||||||
"In a land of corruption, two cyberneticists and a dungeon delver search for freedom.",
|
|
||||||
"In an evil empire of horror, two rangers battle the forces of hell.",
|
|
||||||
"In a lost city, in an age of sorcery, a librarian quests for revenge.",
|
|
||||||
"In a universe of illusions and danger, three time travellers and an adventurer search for justice.",
|
|
||||||
"In a forgotten universe of barbarism, in an era of terror and mysticism, a virtual reality programmer and a spy try to find vengance and battle crime.",
|
|
||||||
"In a universe of demons, in an era of insanity and ghosts, three bodyguards and a bodyguard try to find vengance.",
|
|
||||||
"In a kingdom of corruption and battle, seven artificial intelligences try to save the last living fertile woman.",
|
|
||||||
"In a universe of virutal reality and agony, in an age of ghosts and ghosts, a fortune-teller and a wanderer try to avert the apocalypse.",
|
|
||||||
"In a crime-infested kingdom, three martial artists quest for the truth and oppose evil.",
|
|
||||||
"In a terrifying universe of lost souls, in an era of lost souls, eight dancers fight evil.",
|
|
||||||
"In a galaxy of confusion and insanity, three martial artists and a duke battle a mob of psychics.",
|
|
||||||
"In an amazing kingdom, a wizard and a secretary hope to prevent the destruction of mankind.",
|
|
||||||
"In a kingdom of deception, a reporter searches for fame.",
|
|
||||||
"In a hellish empire, a swordswoman and a duke try to find the ultimate weapon and battle a conspiracy.",
|
|
||||||
"In an evil galaxy of illusion, in a time of technology and misery, seven psychiatrists battle crime.",
|
|
||||||
"In a dark city of confusion, three swordswomen and a singer battle lawlessness.",
|
|
||||||
"In an ominous empire, in an age of hate, two philosophers and a student try to find justice and battle a mob of mages intent on stealing the souls of the innocent.",
|
|
||||||
"In a kingdom of panic, six adventurers oppose lawlessness.",
|
|
||||||
"In a land of dreams and hopelessness, three hackers and a cyborg search for justice.",
|
|
||||||
"On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.",
|
|
||||||
"In a wicked universe, five seers fight lawlessness.",
|
|
||||||
"In a kingdom of death, in an era of illusion and blood, four colonists search for fame.",
|
|
||||||
"In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.",
|
|
||||||
"In a cursed empire, five inventors oppose terrorism.",
|
|
||||||
"On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.",
|
|
||||||
"In a forgotten land, a reporter and a spy try to stop the apocalypse.",
|
|
||||||
"In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.",
|
|
||||||
"On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.",
|
|
||||||
"In a galaxy of dark magic, four fighters seek freedom.",
|
|
||||||
"In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.",
|
|
||||||
"In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.",
|
|
||||||
"In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.",
|
|
||||||
"In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.",
|
|
||||||
"On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.",
|
|
||||||
"In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.",
|
|
||||||
"In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.",
|
|
||||||
"In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.",
|
|
||||||
"In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.",
|
|
||||||
"In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.",
|
|
||||||
"In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.",
|
|
||||||
"In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.",
|
|
||||||
"In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.",
|
|
||||||
"In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime."
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = () => {
|
|
||||||
return `<style>
|
|
||||||
.phb#p1{ text-align:center; }
|
|
||||||
.phb#p1:after{ display:none; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div style='margin-top:450px;'></div>
|
|
||||||
|
|
||||||
# ${_.sample(titles)}
|
|
||||||
|
|
||||||
<div style='margin-top:25px'></div>
|
|
||||||
<div class='wide'>
|
|
||||||
##### ${_.sample(subtitles)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
\\page`
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var ClassFeatureGen = require('./classfeature.gen.js');
|
|
||||||
|
|
||||||
var ClassTableGen = require('./classtable.gen.js');
|
|
||||||
|
|
||||||
module.exports = function(){
|
|
||||||
|
|
||||||
var classname = _.sample(['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
|
||||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'])
|
|
||||||
|
|
||||||
|
|
||||||
var image = _.sample(_.map([
|
|
||||||
"http://orig01.deviantart.net/4682/f/2007/099/f/c/bard_stick_figure_by_wrpigeek.png",
|
|
||||||
"http://img07.deviantart.net/a3c9/i/2007/099/3/a/archer_stick_figure_by_wrpigeek.png",
|
|
||||||
"http://pre04.deviantart.net/d596/th/pre/f/2007/099/5/2/adventurer_stick_figure_by_wrpigeek.png",
|
|
||||||
"http://img13.deviantart.net/d501/i/2007/099/d/4/black_mage_stick_figure_by_wrpigeek.png",
|
|
||||||
"http://img09.deviantart.net/5cf3/i/2007/099/d/d/dark_knight_stick_figure_by_wrpigeek.png",
|
|
||||||
"http://pre01.deviantart.net/7a34/th/pre/f/2007/099/6/3/monk_stick_figure_by_wrpigeek.png",
|
|
||||||
"http://img11.deviantart.net/5dcc/i/2007/099/d/1/mystic_knight_stick_figure_by_wrpigeek.png",
|
|
||||||
"http://pre08.deviantart.net/ad45/th/pre/f/2007/099/a/0/thief_stick_figure_by_wrpigeek.png",
|
|
||||||
], function(url){
|
|
||||||
return "<img src = '" + url + "' style='max-width:8cm;max-height:25cm' />"
|
|
||||||
}))
|
|
||||||
|
|
||||||
|
|
||||||
return [
|
|
||||||
image,
|
|
||||||
"",
|
|
||||||
"```",
|
|
||||||
"```",
|
|
||||||
"<div style='margin-top:240px'></div>\n\n",
|
|
||||||
"## " + classname,
|
|
||||||
"Cool intro stuff will go here",
|
|
||||||
|
|
||||||
"\\page",
|
|
||||||
ClassTableGen(classname),
|
|
||||||
ClassFeatureGen(classname),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
].join('\n') + '\n\n\n';
|
|
||||||
};
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var spellNames = [
|
|
||||||
"Astral Rite of Acne",
|
|
||||||
"Create Acne",
|
|
||||||
"Cursed Ramen Erruption",
|
|
||||||
"Dark Chant of the Dentists",
|
|
||||||
"Erruption of Immaturity",
|
|
||||||
"Flaming Disc of Inconvenience",
|
|
||||||
"Heal Bad Hygene",
|
|
||||||
"Heavenly Transfiguration of the Cream Devil",
|
|
||||||
"Hellish Cage of Mucus",
|
|
||||||
"Irritate Peanut Butter Fairy",
|
|
||||||
"Luminous Erruption of Tea",
|
|
||||||
"Mystic Spell of the Poser",
|
|
||||||
"Sorcerous Enchantment of the Chimneysweep",
|
|
||||||
"Steak Sauce Ray",
|
|
||||||
"Talk to Groupie",
|
|
||||||
"Astonishing Chant of Chocolate",
|
|
||||||
"Astounding Pasta Puddle",
|
|
||||||
"Ball of Annoyance",
|
|
||||||
"Cage of Yarn",
|
|
||||||
"Control Noodles Elemental",
|
|
||||||
"Create Nervousness",
|
|
||||||
"Cure Baldness",
|
|
||||||
"Cursed Ritual of Bad Hair",
|
|
||||||
"Dispell Piles in Dentist",
|
|
||||||
"Eliminate Florists",
|
|
||||||
"Illusionary Transfiguration of the Babysitter",
|
|
||||||
"Necromantic Armor of Salad Dressing",
|
|
||||||
"Occult Transfiguration of Foot Fetish",
|
|
||||||
"Protection from Mucus Giant",
|
|
||||||
"Tinsel Blast",
|
|
||||||
"Alchemical Evocation of the Goths",
|
|
||||||
"Call Fangirl",
|
|
||||||
"Divine Spell of Crossdressing",
|
|
||||||
"Dominate Ramen Giant",
|
|
||||||
"Eliminate Vindictiveness in Gym Teacher",
|
|
||||||
"Extra-Planar Spell of Irritation",
|
|
||||||
"Induce Whining in Babysitter",
|
|
||||||
"Invoke Complaining",
|
|
||||||
"Magical Enchantment of Arrogance",
|
|
||||||
"Occult Globe of Salad Dressing",
|
|
||||||
"Overwhelming Enchantment of the Chocolate Fairy",
|
|
||||||
"Sorcerous Dandruff Globe",
|
|
||||||
"Spiritual Invocation of the Costumers",
|
|
||||||
"Ultimate Rite of the Confetti Angel",
|
|
||||||
"Ultimate Ritual of Mouthwash",
|
|
||||||
];
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
spellList : function(){
|
|
||||||
var levels = ['Cantrips (0 Level)', '2nd Level', '3rd Level', '4th Level', '5th Level', '6th Level', '7th Level', '8th Level', '9th Level'];
|
|
||||||
|
|
||||||
var content = _.map(levels, (level)=>{
|
|
||||||
var spells = _.map(_.sampleSize(spellNames, _.random(5, 15)), (spell)=>{
|
|
||||||
return `- ${spell}`;
|
|
||||||
}).join('\n');
|
|
||||||
return `##### ${level} \n${spells} \n`;
|
|
||||||
}).join('\n');
|
|
||||||
|
|
||||||
return `<div class='spellList'>\n${content}\n</div>`;
|
|
||||||
},
|
|
||||||
|
|
||||||
spell : function(){
|
|
||||||
var level = ["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th"];
|
|
||||||
var spellSchools = ["abjuration", "conjuration", "divination", "enchantment", "evocation", "illusion", "necromancy", "transmutation"];
|
|
||||||
|
|
||||||
|
|
||||||
var components = _.sampleSize(["V", "S", "M"], _.random(1,3)).join(', ');
|
|
||||||
if(components.indexOf("M") !== -1){
|
|
||||||
components += " (" + _.sampleSize(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1,3)).join(', ') + ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
"#### " + _.sample(spellNames),
|
|
||||||
"*" + _.sample(level) + "-level " + _.sample(spellSchools) + "*",
|
|
||||||
"___",
|
|
||||||
"- **Casting Time:** 1 action",
|
|
||||||
"- **Range:** " + _.sample(["Self", "Touch", "30 feet", "60 feet"]),
|
|
||||||
"- **Components:** " + components,
|
|
||||||
"- **Duration:** " + _.sample(["Until dispelled", "1 round", "Instantaneous", "Concentration, up to 10 minutes", "1 hour"]),
|
|
||||||
"",
|
|
||||||
"A flame, equivalent in brightness to a torch, springs from from an object that you touch. ",
|
|
||||||
"The effect look like a regular flame, but it creates no heat and doesn't use oxygen. ",
|
|
||||||
"A *continual flame* can be covered or hidden but not smothered or quenched.",
|
|
||||||
"\n\n\n"
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
var genList = function(list, max){
|
|
||||||
return _.sampleSize(list, _.random(0,max)).join(', ') || "None";
|
|
||||||
}
|
|
||||||
|
|
||||||
var getMonsterName = function(){
|
|
||||||
return _.sample([
|
|
||||||
"All-devouring Baseball Imp",
|
|
||||||
"All-devouring Gumdrop Wraith",
|
|
||||||
"Chocolate Hydra",
|
|
||||||
"Devouring Peacock",
|
|
||||||
"Economy-sized Colossus of the Lemonade Stand",
|
|
||||||
"Ghost Pigeon",
|
|
||||||
"Gibbering Duck",
|
|
||||||
"Sparklemuffin Peacock Spider",
|
|
||||||
"Gum Elemental",
|
|
||||||
"Illiterate Construct of the Candy Store",
|
|
||||||
"Ineffable Chihuahua",
|
|
||||||
"Irritating Death Hamster",
|
|
||||||
"Irritating Gold Mouse",
|
|
||||||
"Juggernaut Snail",
|
|
||||||
"Juggernaut of the Sock Drawer",
|
|
||||||
"Koala of the Cosmos",
|
|
||||||
"Mad Koala of the West",
|
|
||||||
"Milk Djinni of the Lemonade Stand",
|
|
||||||
"Mind Ferret",
|
|
||||||
"Mystic Salt Spider",
|
|
||||||
"Necrotic Halitosis Angel",
|
|
||||||
"Pinstriped Famine Sheep",
|
|
||||||
"Ritalin Leech",
|
|
||||||
"Shocker Kangaroo",
|
|
||||||
"Stellar Tennis Juggernaut",
|
|
||||||
"Wailing Quail of the Sun",
|
|
||||||
"Angel Pigeon",
|
|
||||||
"Anime Sphinx",
|
|
||||||
"Bored Avalanche Sheep of the Wasteland",
|
|
||||||
"Devouring Nougat Sphinx of the Sock Drawer",
|
|
||||||
"Djinni of the Footlocker",
|
|
||||||
"Ectoplasmic Jazz Devil",
|
|
||||||
"Flatuent Angel",
|
|
||||||
"Gelatinous Duck of the Dream-Lands",
|
|
||||||
"Gelatinous Mouse",
|
|
||||||
"Golem of the Footlocker",
|
|
||||||
"Lich Wombat",
|
|
||||||
"Mechanical Sloth of the Past",
|
|
||||||
"Milkshake Succubus",
|
|
||||||
"Puffy Bone Peacock of the East",
|
|
||||||
"Rainbow Manatee",
|
|
||||||
"Rune Parrot",
|
|
||||||
"Sand Cow",
|
|
||||||
"Sinister Vanilla Dragon",
|
|
||||||
"Snail of the North",
|
|
||||||
"Spider of the Sewer",
|
|
||||||
"Stellar Sawdust Leech",
|
|
||||||
"Storm Anteater of Hell",
|
|
||||||
"Stupid Spirit of the Brewery",
|
|
||||||
"Time Kangaroo",
|
|
||||||
"Tomb Poodle",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var getType = function(){
|
|
||||||
return _.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast']) + " " + _.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])
|
|
||||||
}
|
|
||||||
|
|
||||||
var getAlignment = function(){
|
|
||||||
return _.sample([
|
|
||||||
"annoying evil",
|
|
||||||
"chaotic gossipy",
|
|
||||||
"chaotic sloppy",
|
|
||||||
"depressed neutral",
|
|
||||||
"lawful bogus",
|
|
||||||
"lawful coy",
|
|
||||||
"manic-depressive evil",
|
|
||||||
"narrow-minded neutral",
|
|
||||||
"neutral annoying",
|
|
||||||
"neutral ignorant",
|
|
||||||
"oedpipal neutral",
|
|
||||||
"silly neutral",
|
|
||||||
"unoriginal neutral",
|
|
||||||
"weird neutral",
|
|
||||||
"wordy evil",
|
|
||||||
"unaligned"
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
var getStats = function(){
|
|
||||||
return '>|' + _.times(6, function(){
|
|
||||||
var num = _.random(1,20);
|
|
||||||
var mod = Math.ceil(num/2 - 5)
|
|
||||||
return num + " (" + (mod >= 0 ? '+'+mod : mod ) + ")"
|
|
||||||
}).join('|') + '|';
|
|
||||||
}
|
|
||||||
|
|
||||||
var genAbilities = function(){
|
|
||||||
return _.sample([
|
|
||||||
"> ***Pack Tactics.*** These guys work together. Like super well, you don't even know.",
|
|
||||||
"> ***False Appearance. *** While the armor reamin motionless, it is indistinguishable from a normal suit of armor.",
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var genAction = function(){
|
|
||||||
var name = _.sample([
|
|
||||||
"Abdominal Drop",
|
|
||||||
"Airplane Hammer",
|
|
||||||
"Atomic Death Throw",
|
|
||||||
"Bulldog Rake",
|
|
||||||
"Corkscrew Strike",
|
|
||||||
"Crossed Splash",
|
|
||||||
"Crossface Suplex",
|
|
||||||
"DDT Powerbomb",
|
|
||||||
"Dual Cobra Wristlock",
|
|
||||||
"Dual Throw",
|
|
||||||
"Elbow Hold",
|
|
||||||
"Gory Body Sweep",
|
|
||||||
"Heel Jawbreaker",
|
|
||||||
"Jumping Driver",
|
|
||||||
"Open Chin Choke",
|
|
||||||
"Scorpion Flurry",
|
|
||||||
"Somersault Stump Fists",
|
|
||||||
"Suffering Wringer",
|
|
||||||
"Super Hip Submission",
|
|
||||||
"Super Spin",
|
|
||||||
"Team Elbow",
|
|
||||||
"Team Foot",
|
|
||||||
"Tilt-a-whirl Chin Sleeper",
|
|
||||||
"Tilt-a-whirl Eye Takedown",
|
|
||||||
"Turnbuckle Roll"
|
|
||||||
])
|
|
||||||
|
|
||||||
return "> ***" + name + ".*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) ";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
|
|
||||||
full : function(){
|
|
||||||
return [
|
|
||||||
"___",
|
|
||||||
"___",
|
|
||||||
"> ## " + getMonsterName(),
|
|
||||||
">*" + getType() + ", " + getAlignment() + "*",
|
|
||||||
"> ___",
|
|
||||||
"> - **Armor Class** " + _.random(10,20),
|
|
||||||
"> - **Hit Points** " + _.random(1, 150) + "(1d4 + 5)",
|
|
||||||
"> - **Speed** " + _.random(0,50) + "ft.",
|
|
||||||
">___",
|
|
||||||
">|STR|DEX|CON|INT|WIS|CHA|",
|
|
||||||
">|:---:|:---:|:---:|:---:|:---:|:---:|",
|
|
||||||
getStats(),
|
|
||||||
">___",
|
|
||||||
"> - **Condition Immunities** " + genList(["groggy", "swagged", "weak-kneed", "buzzed", "groovy", "melancholy", "drunk"], 3),
|
|
||||||
"> - **Senses** passive Perception " + _.random(3, 20),
|
|
||||||
"> - **Languages** " + genList(["Common", "Pottymouth", "Gibberish", "Latin", "Jive"], 2),
|
|
||||||
"> - **Challenge** " + _.random(0, 15) + " (" + _.random(10,10000)+ " XP)",
|
|
||||||
"> ___",
|
|
||||||
_.times(_.random(3,6), function(){
|
|
||||||
return genAbilities()
|
|
||||||
}).join('\n>\n'),
|
|
||||||
"> ### Actions",
|
|
||||||
_.times(_.random(4,6), function(){
|
|
||||||
return genAction()
|
|
||||||
}).join('\n>\n'),
|
|
||||||
].join('\n') + '\n\n\n';
|
|
||||||
},
|
|
||||||
|
|
||||||
half : function(){
|
|
||||||
return [
|
|
||||||
"___",
|
|
||||||
"> ## " + getMonsterName(),
|
|
||||||
">*" + getType() + ", " + getAlignment() + "*",
|
|
||||||
"> ___",
|
|
||||||
"> - **Armor Class** " + _.random(10,20),
|
|
||||||
"> - **Hit Points** " + _.random(1, 150) + "(1d4 + 5)",
|
|
||||||
"> - **Speed** " + _.random(0,50) + "ft.",
|
|
||||||
">___",
|
|
||||||
">|STR|DEX|CON|INT|WIS|CHA|",
|
|
||||||
">|:---:|:---:|:---:|:---:|:---:|:---:|",
|
|
||||||
getStats(),
|
|
||||||
">___",
|
|
||||||
"> - **Condition Immunities** " + genList(["groggy", "swagged", "weak-kneed", "buzzed", "groovy", "melancholy", "drunk"], 3),
|
|
||||||
"> - **Senses** passive Perception " + _.random(3, 20),
|
|
||||||
"> - **Languages** " + genList(["Common", "Pottymouth", "Gibberish", "Latin", "Jive"], 2),
|
|
||||||
"> - **Challenge** " + _.random(0, 15) + " (" + _.random(10,10000)+ " XP)",
|
|
||||||
"> ___",
|
|
||||||
_.times(_.random(0,2), function(){
|
|
||||||
return genAbilities()
|
|
||||||
}).join('\n>\n'),
|
|
||||||
"> ### Actions",
|
|
||||||
_.times(_.random(1,2), function(){
|
|
||||||
return genAction()
|
|
||||||
}).join('\n>\n'),
|
|
||||||
].join('\n') + '\n\n\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
var MagicGen = require('./magic.gen.js');
|
|
||||||
var ClassTableGen = require('./classtable.gen.js');
|
|
||||||
var MonsterBlockGen = require('./monsterblock.gen.js');
|
|
||||||
var ClassFeatureGen = require('./classfeature.gen.js');
|
|
||||||
var FullClassGen = require('./fullclass.gen.js');
|
|
||||||
var CoverPageGen = require('./coverpage.gen.js');
|
|
||||||
var TableOfContentsGen = require('./tableOfContents.gen.js');
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = [
|
|
||||||
|
|
||||||
{
|
|
||||||
groupName : 'Editor',
|
|
||||||
icon : 'fa-pencil',
|
|
||||||
snippets : [
|
|
||||||
{
|
|
||||||
name : "Column Break",
|
|
||||||
icon : 'fa-columns',
|
|
||||||
gen : "```\n```\n\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : "New Page",
|
|
||||||
icon : 'fa-file-text',
|
|
||||||
gen : "\\page\n\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : "Vertical Spacing",
|
|
||||||
icon : 'fa-arrows-v',
|
|
||||||
gen : "<div style='margin-top:140px'></div>\n\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : "Wide Block",
|
|
||||||
icon : 'fa-arrows-h',
|
|
||||||
gen : "<div class='wide'>\nEverything in here will be extra wide. Tables, text, everything! Beware though, CSS columns can behave a bit weird sometimes.\n</div>\n"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : "Image",
|
|
||||||
icon : 'fa-image',
|
|
||||||
gen : [
|
|
||||||
"<img ",
|
|
||||||
" src='https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg' ",
|
|
||||||
" style='width:325px' />",
|
|
||||||
"Credit: Kyounghwan Kim"
|
|
||||||
].join('\n')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : "Background Image",
|
|
||||||
icon : 'fa-tree',
|
|
||||||
gen : [
|
|
||||||
"<img ",
|
|
||||||
" src='http://i.imgur.com/hMna6G0.png' ",
|
|
||||||
" style='position:absolute; top:50px; right:30px; width:280px' />"
|
|
||||||
].join('\n')
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name : "Page Number",
|
|
||||||
icon : 'fa-bookmark',
|
|
||||||
gen : "<div class='pageNumber'>1</div>\n<div class='footnote'>PART 1 | FANCINESS</div>\n\n"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name : "Auto-incrementing Page Number",
|
|
||||||
icon : 'fa-sort-numeric-asc',
|
|
||||||
gen : "<div class='pageNumber auto'></div>\n"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name : "Link to page",
|
|
||||||
icon : 'fa-link',
|
|
||||||
gen : "[Click here](#p3) to go to page 3\n"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name : "Table of Contents",
|
|
||||||
icon : 'fa-book',
|
|
||||||
gen : TableOfContentsGen
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
/************************* PHB ********************/
|
|
||||||
|
|
||||||
{
|
|
||||||
groupName : 'PHB',
|
|
||||||
icon : 'fa-book',
|
|
||||||
snippets : [
|
|
||||||
{
|
|
||||||
name : 'Spell',
|
|
||||||
icon : 'fa-magic',
|
|
||||||
gen : MagicGen.spell,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Spell List',
|
|
||||||
icon : 'fa-list',
|
|
||||||
gen : MagicGen.spellList,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Class Feature',
|
|
||||||
icon : 'fa-trophy',
|
|
||||||
gen : ClassFeatureGen,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Note',
|
|
||||||
icon : 'fa-sticky-note',
|
|
||||||
gen : function(){
|
|
||||||
return [
|
|
||||||
"> ##### Time to Drop Knowledge",
|
|
||||||
"> Use notes to point out some interesting information. ",
|
|
||||||
"> ",
|
|
||||||
"> **Tables and lists** both work within a note."
|
|
||||||
].join('\n');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Descriptive Text Box',
|
|
||||||
icon : 'fa-sticky-note-o',
|
|
||||||
gen : function(){
|
|
||||||
return [
|
|
||||||
"<div class='descriptive'>",
|
|
||||||
"##### Time to Drop Knowledge",
|
|
||||||
"Use notes to point out some interesting information. ",
|
|
||||||
"",
|
|
||||||
"**Tables and lists** both work within a note.",
|
|
||||||
"</div>"
|
|
||||||
].join('\n');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Monster Stat Block',
|
|
||||||
icon : 'fa-bug',
|
|
||||||
gen : MonsterBlockGen.half,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Wide Monster Stat Block',
|
|
||||||
icon : 'fa-paw',
|
|
||||||
gen : MonsterBlockGen.full,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Cover Page',
|
|
||||||
icon : 'fa-file-word-o',
|
|
||||||
gen : CoverPageGen,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/********************* TABLES *********************/
|
|
||||||
|
|
||||||
{
|
|
||||||
groupName : 'Tables',
|
|
||||||
icon : 'fa-table',
|
|
||||||
snippets : [
|
|
||||||
{
|
|
||||||
name : "Class Table",
|
|
||||||
icon : 'fa-table',
|
|
||||||
gen : ClassTableGen.full,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : "Half Class Table",
|
|
||||||
icon : 'fa-list-alt',
|
|
||||||
gen : ClassTableGen.half,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Table',
|
|
||||||
icon : 'fa-th-list',
|
|
||||||
gen : function(){
|
|
||||||
return [
|
|
||||||
"##### Cookie Tastiness",
|
|
||||||
"| Tastiness | Cookie Type |",
|
|
||||||
"|:----:|:-------------|",
|
|
||||||
"| -5 | Raisin |",
|
|
||||||
"| 8th | Chocolate Chip |",
|
|
||||||
"| 11th | 2 or lower |",
|
|
||||||
"| 14th | 3 or lower |",
|
|
||||||
"| 17th | 4 or lower |\n\n",
|
|
||||||
].join('\n');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Wide Table',
|
|
||||||
icon : 'fa-list',
|
|
||||||
gen : function(){
|
|
||||||
return [
|
|
||||||
"<div class='wide'>",
|
|
||||||
"##### Cookie Tastiness",
|
|
||||||
"| Tastiness | Cookie Type |",
|
|
||||||
"|:----:|:-------------|",
|
|
||||||
"| -5 | Raisin |",
|
|
||||||
"| 8th | Chocolate Chip |",
|
|
||||||
"| 11th | 2 or lower |",
|
|
||||||
"| 14th | 3 or lower |",
|
|
||||||
"| 17th | 4 or lower |",
|
|
||||||
"</div>\n\n"
|
|
||||||
].join('\n');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : 'Split Table',
|
|
||||||
icon : 'fa-th-large',
|
|
||||||
gen : function(){
|
|
||||||
return [
|
|
||||||
"<div style='column-count:2'>",
|
|
||||||
"| d10 | Damage Type |",
|
|
||||||
"|:---:|:------------|",
|
|
||||||
"| 1 | Acid |",
|
|
||||||
"| 2 | Cold |",
|
|
||||||
"| 3 | Fire |",
|
|
||||||
"| 4 | Force |",
|
|
||||||
"| 5 | Lightning |",
|
|
||||||
"",
|
|
||||||
"```",
|
|
||||||
"```",
|
|
||||||
"",
|
|
||||||
"| d10 | Damage Type |",
|
|
||||||
"|:---:|:------------|",
|
|
||||||
"| 6 | Necrotic |",
|
|
||||||
"| 7 | Poison |",
|
|
||||||
"| 8 | Psychic |",
|
|
||||||
"| 9 | Radiant |",
|
|
||||||
"| 10 | Thunder |",
|
|
||||||
"</div>\n\n",
|
|
||||||
].join('\n');
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**************** PRINT *************/
|
|
||||||
|
|
||||||
{
|
|
||||||
groupName : 'Print',
|
|
||||||
icon : 'fa-print',
|
|
||||||
snippets : [
|
|
||||||
{
|
|
||||||
name : "A4 PageSize",
|
|
||||||
icon : 'fa-file-o',
|
|
||||||
gen : ['<style>',
|
|
||||||
' .phb{',
|
|
||||||
' width : 210mm;',
|
|
||||||
' height : 296.8mm;',
|
|
||||||
' }',
|
|
||||||
'</style>'
|
|
||||||
].join('\n')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name : "Ink Friendly",
|
|
||||||
icon : 'fa-tint',
|
|
||||||
gen : ['<style>',
|
|
||||||
' .phb{ background : white;}',
|
|
||||||
' .phb img{ display : none;}',
|
|
||||||
' .phb hr+blockquote{background : white;}',
|
|
||||||
'</style>',
|
|
||||||
''
|
|
||||||
].join('\n')
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const getTOC = (pages) => {
|
|
||||||
const add1 = (title, page)=>{
|
|
||||||
res.push({
|
|
||||||
title : title,
|
|
||||||
page : page + 1,
|
|
||||||
children : []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const add2 = (title, page)=>{
|
|
||||||
if(!_.last(res)) add1('', page);
|
|
||||||
_.last(res).children.push({
|
|
||||||
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,
|
|
||||||
children : []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = [];
|
|
||||||
_.each(pages, (page, pageNum)=>{
|
|
||||||
const lines = page.split('\n');
|
|
||||||
_.each(lines, (line) => {
|
|
||||||
if(_.startsWith(line, '# ')){
|
|
||||||
const title = line.replace('# ', '');
|
|
||||||
add1(title, pageNum)
|
|
||||||
}
|
|
||||||
if(_.startsWith(line, '## ')){
|
|
||||||
const title = line.replace('## ', '');
|
|
||||||
add2(title, pageNum);
|
|
||||||
}
|
|
||||||
if(_.startsWith(line, '### ')){
|
|
||||||
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})**`)
|
|
||||||
if(g1.children.length){
|
|
||||||
_.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) => {
|
|
||||||
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}, []).join('\n');
|
|
||||||
|
|
||||||
return `<div class='toc'>
|
|
||||||
##### Table Of Contents
|
|
||||||
${markdown}
|
|
||||||
</div>\n`;
|
|
||||||
}
|
|
||||||
@@ -7,10 +7,10 @@ const Actions = require('homebrewery/brew.actions.js');
|
|||||||
|
|
||||||
const HomePage = require('./pages/homePage/homePage.jsx');
|
const HomePage = require('./pages/homePage/homePage.jsx');
|
||||||
const EditPage = require('./pages/editPage/editPage.jsx');
|
const EditPage = require('./pages/editPage/editPage.jsx');
|
||||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
//const UserPage = require('./pages/userPage/userPage.jsx');
|
||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
//const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||||
|
|
||||||
let Router;
|
let Router;
|
||||||
@@ -47,6 +47,7 @@ const Homebrew = React.createClass({
|
|||||||
id={args.id}
|
id={args.id}
|
||||||
brew={this.props.brew} />
|
brew={this.props.brew} />
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
|
||||||
'/share/:id' : (args) => {
|
'/share/:id' : (args) => {
|
||||||
if(!this.props.brew.shareId){
|
if(!this.props.brew.shareId){
|
||||||
@@ -62,7 +63,7 @@ const Homebrew = React.createClass({
|
|||||||
username={args.username}
|
username={args.username}
|
||||||
brews={this.props.brews}
|
brews={this.props.brews}
|
||||||
/>
|
/>
|
||||||
},
|
},*/
|
||||||
'/print/:id' : (args, query) => {
|
'/print/:id' : (args, query) => {
|
||||||
return <PrintPage brew={this.props.brew} query={query}/>;
|
return <PrintPage brew={this.props.brew} query={query}/>;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
const flux = require('pico-flux')
|
||||||
|
const React = require('react');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
const Store = require('homebrewery/brew.store.js');
|
||||||
|
const Actions = require('homebrewery/brew.actions.js');
|
||||||
|
|
||||||
|
const onStoreChange = () => {
|
||||||
|
return {
|
||||||
|
status : Store.getStatus(),
|
||||||
|
errors : Store.getErrors()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ContinousSave = React.createClass({
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
status : 'ready',
|
||||||
|
errors : undefined
|
||||||
|
};
|
||||||
|
},
|
||||||
|
componentDidMount: function() {
|
||||||
|
flux.actionEmitter.on('dispatch', this.actionHandler);
|
||||||
|
window.onbeforeunload = ()=>{
|
||||||
|
if(this.props.status !== 'ready') return 'You have unsaved changes!';
|
||||||
|
};
|
||||||
|
},
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
flux.actionEmitter.removeListener('dispatch', this.actionHandler);
|
||||||
|
window.onbeforeunload = function(){};
|
||||||
|
},
|
||||||
|
actionHandler : function(actionType){
|
||||||
|
if(actionType == 'UPDATE_BREW_TEXT' || actionType == 'UPDATE_META'){
|
||||||
|
Actions.pendingSave();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClick : function(){
|
||||||
|
Actions.save();
|
||||||
|
},
|
||||||
|
renderError : function(){
|
||||||
|
let errMsg = '';
|
||||||
|
try{
|
||||||
|
errMsg += this.state.errors.toString() + '\n\n';
|
||||||
|
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
|
||||||
|
}catch(e){}
|
||||||
|
|
||||||
|
return <Nav.item className='continousSave error' icon="fa-warning">
|
||||||
|
Oops!
|
||||||
|
<div className='errorContainer'>
|
||||||
|
Looks like there was a problem saving. <br />
|
||||||
|
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}>
|
||||||
|
here
|
||||||
|
</a>.
|
||||||
|
</div>
|
||||||
|
</Nav.item>
|
||||||
|
},
|
||||||
|
render : function(){
|
||||||
|
if(this.props.status == 'error') return this.renderError();
|
||||||
|
|
||||||
|
if(this.props.status == 'saving'){
|
||||||
|
return <Nav.item className='continousSave' icon="fa-spinner fa-spin">saving...</Nav.item>
|
||||||
|
}
|
||||||
|
if(this.props.status == 'pending'){
|
||||||
|
return <Nav.item className='continousSave' onClick={this.handleClick} color='blue' icon='fa-save'>Save Now</Nav.item>
|
||||||
|
}
|
||||||
|
if(this.props.status == 'ready'){
|
||||||
|
return <Nav.item className='continousSave saved'>saved.</Nav.item>
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Store.createSmartComponent(ContinousSave, onStoreChange);
|
||||||
@@ -125,4 +125,11 @@
|
|||||||
text-align : center;
|
text-align : center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.staticSave.navItem{
|
||||||
|
background-color: @orange;
|
||||||
|
&:hover{
|
||||||
|
background-color: @green;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
37
client/homebrew/navbar/staticSave.navitem.jsx
Normal file
37
client/homebrew/navbar/staticSave.navitem.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const cx = require('classnames');
|
||||||
|
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
const Store = require('homebrewery/brew.store.js');
|
||||||
|
const Actions = require('homebrewery/brew.actions.js');
|
||||||
|
|
||||||
|
const StaticSave = React.createClass({
|
||||||
|
getDefaultProps: function() {
|
||||||
|
return {
|
||||||
|
status : 'ready'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleClick : function(){
|
||||||
|
Actions.saveNew();
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -10,35 +10,31 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
|
const Save = require('../../navbar/continousSave.navitem.jsx');
|
||||||
//const RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
|
//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('homebrewery/markdown.js');
|
const Markdown = require('homebrewery/markdown.js');
|
||||||
|
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 3000;
|
const SAVE_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
|
||||||
|
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({
|
const EditPage = React.createClass({
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {}
|
||||||
text : '',
|
|
||||||
shareId : null,
|
|
||||||
editId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
tags : '',
|
|
||||||
published : false,
|
|
||||||
authors : [],
|
|
||||||
systems : []
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -56,17 +52,11 @@ const EditPage = React.createClass({
|
|||||||
savedBrew : null,
|
savedBrew : null,
|
||||||
|
|
||||||
componentDidMount: function(){
|
componentDidMount: function(){
|
||||||
this.trySave();
|
//this.trySave();
|
||||||
window.onbeforeunload = ()=>{
|
window.onbeforeunload = ()=>{
|
||||||
if(this.state.isSaving || this.state.isPending){
|
if(Store.getStatus() !== 'ready') return 'You have unsaved changes!';
|
||||||
return 'You have unsaved changes!';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setState({
|
|
||||||
htmlErrors : Markdown.validate(this.state.brew.text)
|
|
||||||
})
|
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
@@ -75,12 +65,20 @@ const EditPage = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
handleControlKeys : Utils.controlKeys({
|
||||||
|
s : Actions.pendingSave,
|
||||||
|
p : ()=>{
|
||||||
|
window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
/*
|
||||||
handleControlKeys : function(e){
|
handleControlKeys : function(e){
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
if(e.keyCode == S_KEY) this.save();
|
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)
|
||||||
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -115,7 +113,7 @@ const EditPage = React.createClass({
|
|||||||
|
|
||||||
this.trySave();
|
this.trySave();
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
hasChanges : function(){
|
hasChanges : function(){
|
||||||
if(this.savedBrew){
|
if(this.savedBrew){
|
||||||
return !_.isEqual(this.state.brew, this.savedBrew)
|
return !_.isEqual(this.state.brew, this.savedBrew)
|
||||||
@@ -214,16 +212,7 @@ const EditPage = React.createClass({
|
|||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<BrewInterface />
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx');
|
|||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|
||||||
const Editor = require('../../editor/editor.smart.jsx');
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.smart.jsx');
|
|
||||||
|
|
||||||
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
|
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +36,7 @@ const HomePage = React.createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar ver={this.props.ver}>
|
return <Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
<PatreonNavItem />
|
<PatreonNavItem />
|
||||||
<IssueNavItem />
|
<IssueNavItem />
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
|
||||||
const request = require("superagent");
|
|
||||||
|
|
||||||
const Markdown = require('homebrewery/markdown.js');
|
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
const Issue = require('../../navbar/issue.navitem.jsx');
|
||||||
|
const Save = require('../../navbar/staticSave.navitem.jsx');
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Store = require('homebrewery/brew.store.js');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
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 KEY = 'homebrewery-new';
|
||||||
|
|
||||||
const NewPage = React.createClass({
|
const NewPage = React.createClass({
|
||||||
|
/*
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
metadata : {
|
metadata : {
|
||||||
@@ -34,8 +37,13 @@ const NewPage = React.createClass({
|
|||||||
errors : []
|
errors : []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
const storage = localStorage.getItem(KEY);
|
const storage = localStorage.getItem(KEY);
|
||||||
|
|
||||||
|
//TODO: Add aciton to load from local?
|
||||||
|
|
||||||
|
|
||||||
if(storage){
|
if(storage){
|
||||||
this.setState({
|
this.setState({
|
||||||
text : storage
|
text : storage
|
||||||
@@ -46,8 +54,15 @@ const NewPage = React.createClass({
|
|||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
|
handleControlKeys : Utils.controlKeys({
|
||||||
|
s : Actions.saveNew,
|
||||||
|
p : Actions.localPrint
|
||||||
|
}),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
handleControlKeys : function(e){
|
||||||
|
console.log(e);
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const S_KEY = 83;
|
const S_KEY = 83;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
@@ -59,24 +74,7 @@ const NewPage = React.createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
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(){
|
save : function(){
|
||||||
this.setState({
|
this.setState({
|
||||||
isSaving : true
|
isSaving : true
|
||||||
@@ -99,7 +97,8 @@ const NewPage = React.createClass({
|
|||||||
window.location = '/edit/' + brew.editId;
|
window.location = '/edit/' + brew.editId;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
|
/*
|
||||||
renderSaveButton : function(){
|
renderSaveButton : function(){
|
||||||
if(this.state.isSaving){
|
if(this.state.isSaving){
|
||||||
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
|
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
|
||||||
@@ -111,48 +110,45 @@ const NewPage = React.createClass({
|
|||||||
</Nav.item>
|
</Nav.item>
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
|
/*
|
||||||
print : function(){
|
print : function(){
|
||||||
localStorage.setItem('print', this.state.text);
|
localStorage.setItem('print', this.state.text);
|
||||||
window.open('/print?dialog=true&local=print','_blank');
|
window.open('/print?dialog=true&local=print','_blank');
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
renderLocalPrintButton : function(){
|
renderLocalPrintButton : function(){
|
||||||
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}>
|
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={Actions.localPrint}>
|
||||||
get PDF
|
get PDF
|
||||||
</Nav.item>
|
</Nav.item>
|
||||||
},
|
},
|
||||||
|
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar>
|
return
|
||||||
|
|
||||||
<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(){
|
render : function(){
|
||||||
return <div className='newPage page'>
|
return <div className='newPage page'>
|
||||||
{this.renderNavbar()}
|
<Navbar>
|
||||||
|
<Nav.section>
|
||||||
|
<Nav.item className='brewTitle'>{Store.getMetaData().title}</Nav.item>
|
||||||
|
</Nav.section>
|
||||||
|
|
||||||
|
<Nav.section>
|
||||||
|
<Save />
|
||||||
|
<Nav.item color='purple' icon='fa-file-pdf-o' onClick={Actions.localPrint}>
|
||||||
|
get PDF
|
||||||
|
</Nav.item>
|
||||||
|
<Issue />
|
||||||
|
<Account />
|
||||||
|
</Nav.section>
|
||||||
|
</Navbar>
|
||||||
|
|
||||||
<div className='content'>
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
<BrewInterface />
|
||||||
<Editor
|
|
||||||
ref='editor'
|
|
||||||
value={this.state.text}
|
|
||||||
onChange={this.handleTextChange}
|
|
||||||
metadata={this.state.metadata}
|
|
||||||
onMetadataChange={this.handleMetadataChange}
|
|
||||||
/>
|
|
||||||
<BrewRenderer text={this.state.text} errors={this.state.errors} />
|
|
||||||
</SplitPane>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
.newPage{
|
.newPage{
|
||||||
|
|
||||||
.saveButton{
|
|
||||||
background-color: @orange;
|
|
||||||
&:hover{
|
|
||||||
background-color: @green;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -12,21 +12,17 @@ const PrintPage = React.createClass({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
brewText: this.props.brew.text
|
brewText: this.props.brew.text
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
if(this.props.query.local){
|
if(this.props.query.local){
|
||||||
this.setState({ brewText : localStorage.getItem(this.props.query.local)});
|
this.setState({ brewText : localStorage.getItem(this.props.query.local)});
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.props.query.dialog) window.print();
|
if(this.props.query.dialog) window.print();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPages : function(){
|
renderPages : function(){
|
||||||
return _.map(this.state.brewText.split('\\page'), (page, index) => {
|
return _.map(this.state.brewText.split('\\page'), (page, index) => {
|
||||||
return <div
|
return <div
|
||||||
|
|||||||
@@ -19,15 +19,32 @@ const Actions = {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
save : () => {
|
||||||
|
const brew = Store.getBrew();
|
||||||
|
dispatch('SET_STATUS', 'saving');
|
||||||
|
request
|
||||||
|
.put('/api/update/' + brew.editId)
|
||||||
|
.send(brew)
|
||||||
|
.end((err, res) => {
|
||||||
|
if(err) return dispatch('SET_STATUS', 'error', err);
|
||||||
|
dispatch('SET_STATUS', 'ready');
|
||||||
|
dispatch('SET_BREW', res.body);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
saveNew : () => {
|
saveNew : () => {
|
||||||
//TODO: Maybe set the status?
|
dispatch('SET_STATUS', 'saving');
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(Store.getBrew())
|
.send(Store.getBrew())
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err) return;
|
if(err) return dispatch('SET_STATUS', 'error', err);
|
||||||
const brew = res.body;
|
const brew = res.body;
|
||||||
window.location = '/edit/' + brew.editId;
|
window.location = '/edit/' + brew.editId;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
localPrint : ()=>{
|
||||||
|
localStorage.setItem('print', Store.getBrewText());
|
||||||
|
window.open('/print?dialog=true&local=print','_blank');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ let State = {
|
|||||||
systems : []
|
systems : []
|
||||||
},
|
},
|
||||||
|
|
||||||
errors : []
|
errors : [],
|
||||||
|
status : 'ready', //ready, pending, saving, error
|
||||||
};
|
};
|
||||||
|
|
||||||
const Store = flux.createStore({
|
const Store = flux.createStore({
|
||||||
@@ -34,6 +35,10 @@ const Store = flux.createStore({
|
|||||||
},
|
},
|
||||||
UPDATE_META : (meta) => {
|
UPDATE_META : (meta) => {
|
||||||
State.brew = _.merge({}, State.brew, meta);
|
State.brew = _.merge({}, State.brew, meta);
|
||||||
|
},
|
||||||
|
SET_STATUS : (status, error) => {
|
||||||
|
State.status = status;
|
||||||
|
if(error) State.errors = error;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,9 +58,12 @@ Store.getMetaData = ()=>{
|
|||||||
Store.getErrors = ()=>{
|
Store.getErrors = ()=>{
|
||||||
return State.errors;
|
return State.errors;
|
||||||
};
|
};
|
||||||
|
|
||||||
Store.getVersion = ()=>{
|
Store.getVersion = ()=>{
|
||||||
return State.version;
|
return State.version;
|
||||||
};
|
};
|
||||||
|
Store.getStatus = ()=>{
|
||||||
|
return State.status;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
module.exports = Store;
|
module.exports = Store;
|
||||||
@@ -66,6 +66,11 @@ const BrewEditor = React.createClass({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
brewJump : function(){
|
||||||
|
const currentPage = this.getCurrentPage();
|
||||||
|
window.location.hash = 'p' + currentPage;
|
||||||
|
},
|
||||||
|
|
||||||
//Called when there are changes to the editor's dimensions
|
//Called when there are changes to the editor's dimensions
|
||||||
update : function(){
|
update : function(){
|
||||||
this.refs.codeEditor.updateSize();
|
this.refs.codeEditor.updateSize();
|
||||||
@@ -113,6 +118,12 @@ const BrewEditor = React.createClass({
|
|||||||
onChange={this.handleTextChange}
|
onChange={this.handleTextChange}
|
||||||
onCursorActivity={this.handleCursorActivty} />
|
onCursorActivity={this.handleCursorActivty} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<div className='brewJump' onClick={this.brewJump}>
|
||||||
|
<i className='fa fa-arrow-right' />
|
||||||
|
</div>
|
||||||
|
*/}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,8 +5,23 @@
|
|||||||
.codeEditor{
|
.codeEditor{
|
||||||
height : 100%;
|
height : 100%;
|
||||||
.pageLine{
|
.pageLine{
|
||||||
background-color : fade(#333, 30%);
|
background-color : fade(#333, 15%);
|
||||||
border-bottom : #333 solid 1px;
|
border-bottom : #333 solid 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brewJump{
|
||||||
|
position: absolute;
|
||||||
|
background-color: @teal;
|
||||||
|
cursor: pointer;
|
||||||
|
width : 30px;
|
||||||
|
height : 30px;
|
||||||
|
display : flex;
|
||||||
|
align-items : center;
|
||||||
|
bottom : 20px;
|
||||||
|
right : 20px;
|
||||||
|
z-index: 1000000;
|
||||||
|
justify-content:center;
|
||||||
|
.tooltipLeft("Jump to brew page");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ const cx = require('classnames');
|
|||||||
const Markdown = require('homebrewery/markdown.js');
|
const Markdown = require('homebrewery/markdown.js');
|
||||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||||
|
|
||||||
|
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx')
|
||||||
const Store = require('homebrewery/brew.store.js');
|
const Store = require('homebrewery/brew.store.js');
|
||||||
|
|
||||||
|
|
||||||
@@ -130,6 +131,7 @@ const BrewRenderer = React.createClass({
|
|||||||
style={{height : this.state.height}}>
|
style={{height : this.state.height}}>
|
||||||
|
|
||||||
<ErrorBar errors={this.props.errors} />
|
<ErrorBar errors={this.props.errors} />
|
||||||
|
<RenderWarnings />
|
||||||
|
|
||||||
<div className='pages' ref='pages'>
|
<div className='pages' ref='pages'>
|
||||||
{this.renderPages()}
|
{this.renderPages()}
|
||||||
|
|||||||
20
shared/homebrewery/utils.js
Normal file
20
shared/homebrewery/utils.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
|
||||||
|
const Utils = {
|
||||||
|
controlKeys : (mapping) => {
|
||||||
|
return (e) => {
|
||||||
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
|
if(typeof mapping[e.key] === 'function'){
|
||||||
|
mapping[e.key]();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = Utils;
|
||||||
Reference in New Issue
Block a user