mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-24 12:03:00 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c9ae099b0 | ||
|
|
ff8869f116 | ||
|
|
8e644b23dc | ||
|
|
3d53f08276 | ||
|
|
ba77908a50 | ||
|
|
9cffc94d90 | ||
|
|
8ab925398a |
14
.github/issue_template.md
vendored
14
.github/issue_template.md
vendored
@@ -1,14 +0,0 @@
|
||||
**Browser Type/Version**: [Google Ultron v90.01]
|
||||
|
||||
**Operating System**: [GLaDOS v34.5.8]
|
||||
|
||||
**Issue Description**: [The thing won't thing]
|
||||
|
||||
**Markdown code to reproduce**:
|
||||
|
||||
```
|
||||
# thing
|
||||
> thing 2
|
||||
```
|
||||
|
||||
**Related Images** :
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,6 +11,4 @@ architecture.json
|
||||
!/config/default.json
|
||||
|
||||
node_modules
|
||||
storage
|
||||
.idea
|
||||
*.swp
|
||||
storage
|
||||
29
Dockerfile
29
Dockerfile
@@ -1,29 +0,0 @@
|
||||
FROM node:latest
|
||||
|
||||
MAINTAINER David Hudson <jendave@yahoo.com>
|
||||
|
||||
# System update
|
||||
RUN apt-get -q -y update
|
||||
|
||||
RUN apt-get -q -y install npm
|
||||
RUN apt-get -q -y install mongodb
|
||||
|
||||
RUN apt-get clean && rm -r /var/lib/apt/lists/*
|
||||
|
||||
EXPOSE 22
|
||||
EXPOSE 8000
|
||||
|
||||
ADD start.sh /start.sh
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
VOLUME ["/opt/apps"]
|
||||
COPY . /opt/apps/naturalcrit/
|
||||
WORKDIR /opt/apps/naturalcrit/
|
||||
|
||||
RUN npm install
|
||||
RUN npm install -g gulp-cli
|
||||
RUN npm install gulp
|
||||
RUN gulp fresh
|
||||
|
||||
CMD ["/start.sh"]
|
||||
|
||||
15
README.md
15
README.md
@@ -1,5 +1,5 @@
|
||||
# NaturalCrit
|
||||
A tool suite for DMs to use for D&D. Check it out [here](http://www.naturalcrit.com).
|
||||
A tool suite for DMs to use for D&D
|
||||
|
||||
|
||||
### Getting started
|
||||
@@ -16,18 +16,7 @@ A tool suite for DMs to use for D&D. Check it out [here](http://www.naturalcrit.
|
||||
|
||||
Have fun!
|
||||
|
||||
### Docker Image
|
||||
You can use [Docker](https://docs.docker.com) to get up and running with NaturalCrit.
|
||||
|
||||
1. Install Docker
|
||||
1. Clone the repo
|
||||
1. In the terminal, go to the repo
|
||||
1. Build the docker image `docker build -t naturalcrit .`
|
||||
1. Run the docker container `docker run -dit -p 8000:8000 naturalcrit`
|
||||
1. You can check out the website on your computer on port 8000
|
||||
1. You may have to use `docker-machine env` to get the IP address of your docker instance
|
||||
|
||||
|
||||
### changelog
|
||||
|
||||
You can check out the changelog [here](https://github.com/stolksdorf/NaturalCrit/blob/master/changelog.md)
|
||||
You can check out the changelog [here](https://github.com/stolksdorf/NaturalCrit/blob/master/changelog.md)
|
||||
98
changelog.md
98
changelog.md
@@ -1,109 +1,19 @@
|
||||
# changelog
|
||||
## changelog
|
||||
|
||||
### Saturday, 14/05/2016 - v2.0.0 (finally!)
|
||||
|
||||
I've been working on v2 for a *very* long time. I want to thank you guys for being paitent.
|
||||
It started rather small, but as more and more features were added, I decided to just wait until everything was polished.
|
||||
|
||||
Massive changelog incoming:
|
||||
|
||||
#### Brews
|
||||
- New flow for creating new brews. Before creating a new brew would immedaitely create a brew in the database and let you edit it. Many people would create a new brew just to experiment and close it, which lead to many "abandoned brews" (see the Under the hood section below). This started eating up a ton of database space. You now have to manually save a new brew for the first time, however Homebrewery will store anything you don't have saved in local storage, just in case your browser crashes or whatever, it will load that up when you go back to `/homebrew/new`
|
||||
- Black blocking edges around notes and stat blocks when printing to PDFs have been fixed
|
||||
- Borders sometimes not showing up in the second column have been fixed
|
||||
- All pseudo-element borders have been replaced with reliable border images.
|
||||
- Chrome can finally print to PDF as good as Chrome Canary! Updating instructions.
|
||||
- Added a little page number box.
|
||||
- Added in a new editable Brew Title. This will be shown in the navbar on share pages, and will default to the file name when you save as PDF. All exsisting brews will be defaulted with an empty title.
|
||||
- Mutliline lists render better now
|
||||
- Firefox rendering has been slithgly improved. Firefox and Chrome render things **slightly** differently, over the course of a brew, these little changes add up and lead to very noticable rendering differences between the browsers. I'm trying my best to get Firefox rendering better, but it's a difficult problem.
|
||||
- A bunch of you have wanted to donate to me. I am super flattered by that. I created a [patreon page](https://www.patreon.com/stolksdorf). If you feel like helping out, head here :)
|
||||
|
||||
#### Under the Hood Stuff
|
||||
- Setup a proper staging environment. Will be using this for tests, and hopefully getting the community to help me test future versions
|
||||
- Server-side prerendering now much faster
|
||||
- Regular weekly database back-ups. Your brews are safe!
|
||||
- Database is now uniquely indexed on both editId and shareId, page loads/saving should be much faster
|
||||
- Improved Admin console. This helps me answer people's questions about issues with their brews
|
||||
- Added a whole querying/pagniation API that I can use for stats and answering questions
|
||||
- Clearing out "Abandoned" brews (smaller than a tweet and haven't been viewed for a week). These account for nearly a third of all stored brews.
|
||||
|
||||
#### Interface
|
||||
- Added in a whole new editor with syntax highlighting for markdown
|
||||
- Built a splitpane! Remembers where you left the split in between sessions
|
||||
- Re-organized the snippets into a hierarchical groups. Should be much easier to find what you need
|
||||
- Partial page rendering is working. The Homebrewery will now only load the viewable pages, and any page with `<style>` tags on them. If you are working on a large brew you should notice *significant* performance improvements
|
||||
- Edit page saving interface has been improved significantly. Auto-saves after 3 seconds on inactivity, now allows user to save at anytime. Will stop the tab from closing witha pop-up if there are unsaved changes.
|
||||
- Navbar and overall style has been improved and spacing made more consistent
|
||||
- Elements under the hood are way more organized and should behaviour much more reliably in many sizes.
|
||||
- Source now opens to it's own route `/source/:sharedId` instead of just a window. Now easier to share, and won't be blocked by some browsers.
|
||||
- Print page now auto-opens print dialog. If you want to share your print page link, just remove the `?dialog=true` parameter and it won't open the dialog.
|
||||
|
||||
|
||||
|
||||
\page
|
||||
|
||||
### Wednesday, 20/04/2016
|
||||
- A lot of admin improvements. Pagninated brew table
|
||||
- Added a searching and removing abandoned brew api endpoints (turns out about 40% of brews are shorter that a tweet!).
|
||||
- Fixed the require cache being cleared. Pages should render a bit faster now.
|
||||
- Pulled in `kkragenbrink`s fix for nested lists, Thanks!
|
||||
|
||||
|
||||
### Wednesday, 06/04/2016 - v1.4
|
||||
* Pages will now partially render. This should greatly speed up *very* large homebrews. The Homebreery will figure out which page you should be looking at and render that page, the page before, and the page after.
|
||||
* Zooming should be fixed. I've changed the font size units to be cm, which match the units of the page. Zooming in and out now look much better.
|
||||
|
||||
|
||||
### Monday, 29/02/2016 - v1.3.1
|
||||
* Removng the changelog button from the Share page
|
||||
* Added a A4 page size snippet (thanks guppy42!)
|
||||
* Added support for `<sup>` and `<sub>` tags (thanks crashinworld14!)
|
||||
|
||||
### Saturday, 20/02/2016
|
||||
* Fixed h1 headers not going full width (thanks McToomin27)
|
||||
* Added a github issue template
|
||||
|
||||
## v1.3.0
|
||||
|
||||
### Friday, 19/02/2016
|
||||
* Improved the admin panel
|
||||
* Added ability to clear away old empty brews
|
||||
* Added delete button to the edit page
|
||||
* Added a dynamically updating changelog page! Nifty!
|
||||
* Added stlying for wide monster stat blocks and single column class tables
|
||||
* Added snippets for wide monster stat blocks and single column class tables
|
||||
|
||||
### Tuesday, 16/02/2016
|
||||
* Paragraphs right after tables now indent (thanks LikeAJi6!)
|
||||
* Added a `@page` css rule to auto turn off margins when printing
|
||||
* Added a `page-break` property on each `.phb` page to properly page the pages up when exporting (thanks Jokefury!)
|
||||
* Improved first character rendering on Firefox
|
||||
* Improved table spacing a bit
|
||||
* Changed padding at page bottom for better fit and clipping of elements
|
||||
* Improved spacing for bold text (thanks nickpunt!)
|
||||
|
||||
|
||||
## v1.2.0
|
||||
|
||||
### Sunday, 17/01/2016
|
||||
#### Sunday, 17/01/2016
|
||||
* Added a printer friendly snippet that injects some CSS to remove backbrounds and images
|
||||
* Adjusted the styling specific to spell blocks to give it tighter spacing
|
||||
* Added a changelog! How meta!
|
||||
|
||||
## v1.1.0
|
||||
#### Thursday, 14/01/2016
|
||||
|
||||
### Thursday, 14/01/2016
|
||||
* Added view source to see the markdown that made the page
|
||||
* Added print view
|
||||
* Fixed API issues that were causing the server to crash
|
||||
* Increased padding on table cells
|
||||
* Raw html now shows in view source
|
||||
|
||||
|
||||
## v1.0.0 - Release
|
||||
|
||||
### Wednesday, 3/01/2016
|
||||
#### Wednesday, 3/01/2016
|
||||
|
||||
* Added `phb.standalone.css` plus a build system for creating it
|
||||
* Added page numbers and footer text
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@import 'naturalcrit/styles/reset.less';
|
||||
@import 'naturalcrit/styles/elements.less';
|
||||
@import 'naturalcrit/styles/animations.less';
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@import 'naturalcrit/styles/tooltip.less';
|
||||
@import 'naturalCrit/styles/reset.less';
|
||||
@import 'naturalCrit/styles/elements.less';
|
||||
@import 'naturalCrit/styles/animations.less';
|
||||
@import 'naturalCrit/styles/colors.less';
|
||||
@import 'naturalCrit/styles/tooltip.less';
|
||||
|
||||
@import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
|
||||
@@ -1,152 +1,64 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
var request = require('superagent');
|
||||
|
||||
var Moment = require('moment');
|
||||
var Moment = require('moment')
|
||||
|
||||
var HomebrewAdmin = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
homebrews : [],
|
||||
admin_key : ''
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
page: 0,
|
||||
count : 20,
|
||||
brewCache : {},
|
||||
total : 0,
|
||||
|
||||
processingOldBrews : false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
fetchBrews : function(page){
|
||||
request.get('/homebrew/api/search')
|
||||
.query({
|
||||
admin_key : this.props.admin_key,
|
||||
count : this.state.count,
|
||||
page : page
|
||||
})
|
||||
.end((err, res)=>{
|
||||
this.state.brewCache[page] = res.body.brews;
|
||||
this.setState({
|
||||
brewCache : this.state.brewCache,
|
||||
total : res.body.total,
|
||||
count : res.body.count
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.fetchBrews(this.state.page);
|
||||
},
|
||||
|
||||
changePageTo : function(page){
|
||||
if(!this.state.brewCache[page]){
|
||||
this.fetchBrews(page);
|
||||
}
|
||||
this.setState({
|
||||
page : page
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
clearInvalidBrews : function(){
|
||||
request.get('/homebrew/api/invalid')
|
||||
.query({admin_key : this.props.admin_key})
|
||||
.end((err, res)=>{
|
||||
if(!confirm("This will remove " + res.body.count + " brews. Are you sure?")) return;
|
||||
request.get('/homebrew/api/invalid')
|
||||
.query({admin_key : this.props.admin_key, do_it : true})
|
||||
.end((err, res)=>{
|
||||
alert("Done!")
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
deleteBrew : function(brewId){
|
||||
if(!confirm("Are you sure you want to delete '" + brewId + "'?")) return;
|
||||
request.get('/homebrew/api/remove/' + brewId)
|
||||
.query({admin_key : this.props.admin_key})
|
||||
.end(function(err, res){
|
||||
window.location.reload();
|
||||
})
|
||||
},
|
||||
|
||||
handlePageChange : function(dir){
|
||||
this.changePageTo(this.state.page + dir);
|
||||
},
|
||||
|
||||
renderPagnination : function(){
|
||||
var outOf;
|
||||
if(this.state.total){
|
||||
outOf = this.state.page + ' / ' + Math.round(this.state.total/this.state.count);
|
||||
}
|
||||
return <div className='pagnination'>
|
||||
<i className='fa fa-chevron-left' onClick={this.handlePageChange.bind(this, -1)}/>
|
||||
{outOf}
|
||||
<i className='fa fa-chevron-right' onClick={this.handlePageChange.bind(this, 1)}/>
|
||||
</div>
|
||||
},
|
||||
|
||||
|
||||
renderBrews : function(){
|
||||
var brews = this.state.brewCache[this.state.page] || _.times(this.state.count);
|
||||
return _.map(brews, (brew)=>{
|
||||
return <tr className={cx('brewRow', {'isEmpty' : brew.text == "false"})} key={brew.sharedId}>
|
||||
<td><a href={'/homebrew/edit/' + brew.editId} target='_blank'>{brew.editId}</a></td>
|
||||
<td><a href={'/homebrew/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></td>
|
||||
return _.map(this.props.homebrews, (brew)=>{
|
||||
return <tr className={cx('brewRow', {'isEmpty' : brew.text == ""})} key={brew.sharedId}>
|
||||
|
||||
<td>{brew.editId}</td>
|
||||
<td>{brew.shareId}</td>
|
||||
<td>{Moment(brew.createdAt).fromNow()}</td>
|
||||
<td>{Moment(brew.updatedAt).fromNow()}</td>
|
||||
<td>{Moment(brew.lastViewed).fromNow()}</td>
|
||||
<td>{brew.views}</td>
|
||||
<td>
|
||||
<div className='deleteButton' onClick={this.deleteBrew.bind(this, brew.editId)}>
|
||||
<i className='fa fa-trash' />
|
||||
|
||||
<td className='preview'>
|
||||
<a target="_blank" href={'/homebrew/share/' + brew.shareId}>view</a>
|
||||
<div className='content'>
|
||||
{brew.text.slice(0, 500)}
|
||||
</div>
|
||||
</td>
|
||||
<td><a href={'/homebrew/remove/' + brew.editId +'?admin_key=' + this.props.admin_key}><i className='fa fa-trash' /></a></td>
|
||||
</tr>
|
||||
});
|
||||
},
|
||||
|
||||
renderBrewTable : function(){
|
||||
return <div className='brewTable'>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Edit Id</th>
|
||||
<th>Share Id</th>
|
||||
<th>Created At</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Last Viewed</th>
|
||||
<th>Views</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderBrews()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return <div className='homebrewAdmin'>
|
||||
<h2>
|
||||
Homebrews - {this.state.total}
|
||||
</h2>
|
||||
{this.renderPagnination()}
|
||||
{this.renderBrewTable()}
|
||||
|
||||
<button className='clearOldButton' onClick={this.clearInvalidBrews}>
|
||||
Clear Old
|
||||
</button>
|
||||
</div>
|
||||
return(
|
||||
<div className='homebrewAdmin'>
|
||||
<h2>Homebrews - {this.props.homebrews.length}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Edit Id</th>
|
||||
<th>Share Id</th>
|
||||
<th>Created At</th>
|
||||
<th>Last Updated</th>
|
||||
<th>Last Viewed</th>
|
||||
<th>Number of Views</th>
|
||||
<th>Preview</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderBrews()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,53 +1,44 @@
|
||||
|
||||
.homebrewAdmin{
|
||||
margin-bottom: 80px;
|
||||
.brewTable{
|
||||
table{
|
||||
|
||||
th{
|
||||
padding : 10px;
|
||||
font-weight : 800;
|
||||
}
|
||||
tr:nth-child(even){
|
||||
background-color : fade(@green, 10%);
|
||||
}
|
||||
tr.isEmpty{
|
||||
background-color : fade(@red, 30%);
|
||||
}
|
||||
td{
|
||||
min-width : 100px;
|
||||
padding : 10px;
|
||||
text-align : center;
|
||||
|
||||
&.preview{
|
||||
position : relative;
|
||||
&:hover{
|
||||
.content{
|
||||
display : block;
|
||||
}
|
||||
}
|
||||
table{
|
||||
overflow-y : scroll;
|
||||
max-height : 800px;
|
||||
th{
|
||||
padding : 10px;
|
||||
font-weight : 800;
|
||||
}
|
||||
tr:nth-child(even){
|
||||
background-color : fade(@green, 10%);
|
||||
}
|
||||
tr.isEmpty{
|
||||
background-color : fade(@red, 30%);
|
||||
}
|
||||
td{
|
||||
min-width : 100px;
|
||||
padding : 10px;
|
||||
text-align : center;
|
||||
&.preview{
|
||||
position : relative;
|
||||
&:hover{
|
||||
.content{
|
||||
position : absolute;
|
||||
display : none;
|
||||
top : 100%;
|
||||
left : 0px;
|
||||
z-index : 1000;
|
||||
max-height : 500px;
|
||||
width : 300px;
|
||||
padding : 30px;
|
||||
background-color : white;
|
||||
font-family : monospace;
|
||||
text-align : left;
|
||||
pointer-events : none;
|
||||
display : block;
|
||||
}
|
||||
}
|
||||
.content{
|
||||
position : absolute;
|
||||
display : none;
|
||||
top : 100%;
|
||||
left : 0px;
|
||||
z-index : 1000;
|
||||
max-height : 500px;
|
||||
width : 300px;
|
||||
padding : 30px;
|
||||
background-color : white;
|
||||
font-family : monospace;
|
||||
text-align : left;
|
||||
pointer-events : none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.deleteButton{
|
||||
cursor: pointer;
|
||||
}
|
||||
button.clearOldButton{
|
||||
float : right;
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Markdown = require('marked');
|
||||
|
||||
var PAGE_HEIGHT = 1056 + 30;
|
||||
|
||||
var BrewRenderer = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
text : ''
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
viewablePageNumber: 0,
|
||||
height : 0
|
||||
};
|
||||
},
|
||||
totalPages : 0,
|
||||
height : 0,
|
||||
|
||||
componentDidMount: function() {
|
||||
this.setState({
|
||||
height : this.refs.main.parentNode.clientHeight
|
||||
});
|
||||
},
|
||||
handleScroll : function(e){
|
||||
this.setState({
|
||||
viewablePageNumber : Math.floor(e.target.scrollTop / PAGE_HEIGHT)
|
||||
});
|
||||
},
|
||||
//Implement later
|
||||
scrollToPage : function(pageNumber){
|
||||
},
|
||||
|
||||
shouldRender : function(pageText, index){
|
||||
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.totalPages}
|
||||
</div>
|
||||
},
|
||||
|
||||
renderDummyPage : function(key){
|
||||
return <div className='phb' key={key}>
|
||||
<i className='fa fa-spinner fa-spin' />
|
||||
</div>
|
||||
},
|
||||
|
||||
renderPage : function(pageText, index){
|
||||
return <div className='phb' dangerouslySetInnerHTML={{__html:Markdown(pageText)}} key={index} />
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
var pages = this.props.text.split('\\page');
|
||||
this.totalPages = pages.length;
|
||||
|
||||
return _.map(pages, (page, index)=>{
|
||||
if(this.shouldRender(page, index)){
|
||||
return this.renderPage(page, index);
|
||||
}else{
|
||||
return this.renderDummyPage(index);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='brewRenderer'
|
||||
onScroll={this.handleScroll}
|
||||
ref='main'
|
||||
style={{height : this.state.height}}>
|
||||
|
||||
<div className='pages'>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
{this.renderPageInfo()}
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = BrewRenderer;
|
||||
@@ -1,28 +0,0 @@
|
||||
|
||||
@import (less) 'naturalcrit/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;
|
||||
}
|
||||
.pages{
|
||||
margin : 30px 0px;
|
||||
&>.phb{
|
||||
margin-right : auto;
|
||||
margin-bottom : 30px;
|
||||
margin-left : auto;
|
||||
box-shadow : 1px 4px 14px #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
client/homebrew/editPage/editPage.jsx
Normal file
86
client/homebrew/editPage/editPage.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Statusbar = require('../statusbar/statusbar.jsx');
|
||||
var PageContainer = require('../pageContainer/pageContainer.jsx');
|
||||
var Editor = require('../editor/editor.jsx');
|
||||
|
||||
var FullClassGen = require('../editor/snippets/fullclass.gen.js');
|
||||
|
||||
var request = require("superagent");
|
||||
|
||||
var SAVE_TIMEOUT = 3000;
|
||||
|
||||
var EditPage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
id : null,
|
||||
entry : {
|
||||
text : "",
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
text: this.props.entry.text,
|
||||
pending : false,
|
||||
lastUpdated : this.props.entry.updatedAt
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var self = this;
|
||||
window.onbeforeunload = function(){
|
||||
if(!self.state.pending) return;
|
||||
return "You have unsaved changes!";
|
||||
}
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text,
|
||||
pending : true
|
||||
});
|
||||
this.save();
|
||||
},
|
||||
|
||||
save : _.debounce(function(){
|
||||
request
|
||||
.put('/homebrew/update/' + this.props.id)
|
||||
.send({text : this.state.text})
|
||||
.end((err, res) => {
|
||||
this.setState({
|
||||
pending : false,
|
||||
lastUpdated : res.body.updatedAt
|
||||
})
|
||||
})
|
||||
}, SAVE_TIMEOUT),
|
||||
|
||||
render : function(){
|
||||
return <div className='editPage'>
|
||||
<Statusbar
|
||||
editId={this.props.entry.editId}
|
||||
shareId={this.props.entry.shareId}
|
||||
printId={this.props.entry.shareId}
|
||||
lastUpdated={this.state.lastUpdated}
|
||||
isPending={this.state.pending} />
|
||||
|
||||
<div className='paneSplit'>
|
||||
<div className='leftPane'>
|
||||
<Editor text={this.state.text} onChange={this.handleTextChange} />
|
||||
</div>
|
||||
<div className='rightPane'>
|
||||
<PageContainer text={this.state.text} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = EditPage;
|
||||
5
client/homebrew/editPage/editPage.less
Normal file
5
client/homebrew/editPage/editPage.less
Normal file
@@ -0,0 +1,5 @@
|
||||
.editPage{
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,83 +1,53 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
var Snippets = require('./snippets/snippets.js');
|
||||
|
||||
|
||||
var splice = function(str, index, inject){
|
||||
return str.slice(0, index) + inject + str.slice(index);
|
||||
};
|
||||
var execute = function(val){
|
||||
if(_.isFunction(val)) return val();
|
||||
return val;
|
||||
}
|
||||
var SnippetIcons = require('./snippets/snippets.js');
|
||||
|
||||
|
||||
var Editor = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
value : "",
|
||||
text : "",
|
||||
onChange : function(){}
|
||||
};
|
||||
},
|
||||
cursorPosition : {
|
||||
line : 0,
|
||||
ch : 0
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
var paneHeight = this.refs.main.parentNode.clientHeight;
|
||||
paneHeight -= this.refs.snippetBar.clientHeight + 1;
|
||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||
this.refs.textarea.focus();
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.props.onChange(text);
|
||||
},
|
||||
handleCursorActivty : function(curpos){
|
||||
this.cursorPosition = curpos;
|
||||
handleTextChange : function(e){
|
||||
this.props.onChange(e.target.value);
|
||||
},
|
||||
|
||||
handleSnippetClick : function(injectText){
|
||||
var 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);
|
||||
iconClick : function(snippetFn){
|
||||
var curPos = this.refs.textarea.selectionStart;
|
||||
this.props.onChange(this.props.text.slice(0, curPos) +
|
||||
snippetFn() +
|
||||
this.props.text.slice(curPos + 1));
|
||||
},
|
||||
|
||||
//Called when there are changes to the editor's dimensions
|
||||
update : function(){
|
||||
this.refs.codeEditor.updateSize();
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
return _.map(Snippets, (snippetGroup)=>{
|
||||
return <SnippetGroup
|
||||
groupName={snippetGroup.groupName}
|
||||
icon={snippetGroup.icon}
|
||||
snippets={snippetGroup.snippets}
|
||||
key={snippetGroup.groupName}
|
||||
onSnippetClick={this.handleSnippetClick}
|
||||
/>
|
||||
renderTemplateIcons : function(){
|
||||
return _.map(SnippetIcons, (t) => {
|
||||
return <div className='icon' key={t.icon}
|
||||
onClick={this.iconClick.bind(this, t.snippet)}
|
||||
data-tooltip={t.tooltip}>
|
||||
<i className={'fa ' + t.icon} />
|
||||
</div>;
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='editor' ref='main'>
|
||||
<div className='snippetBar' ref='snippetBar'>
|
||||
{this.renderSnippetGroups()}
|
||||
<div className='editor'>
|
||||
<div className='textIcons'>
|
||||
{this.renderTemplateIcons()}
|
||||
</div>
|
||||
<CodeEditor
|
||||
ref='codeEditor'
|
||||
wrap={true}
|
||||
language='gfm'
|
||||
value={this.props.value}
|
||||
onChange={this.handleTextChange}
|
||||
onCursorActivity={this.handleCursorActivty} />
|
||||
<textarea
|
||||
ref='textarea'
|
||||
value={this.props.text}
|
||||
onChange={this.handleTextChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -85,45 +55,3 @@ var Editor = React.createClass({
|
||||
|
||||
module.exports = Editor;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
var SnippetGroup = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
groupName : '',
|
||||
icon : 'fa-rocket',
|
||||
snippets : [],
|
||||
onSnippetClick : function(){},
|
||||
};
|
||||
},
|
||||
handleSnippetClick : function(snippet){
|
||||
this.props.onSnippetClick(execute(snippet.gen));
|
||||
},
|
||||
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,56 +1,40 @@
|
||||
|
||||
.editor{
|
||||
position : relative;
|
||||
width : 100%;
|
||||
.snippetBar{
|
||||
display : flex;
|
||||
padding : 5px;
|
||||
background-color : #ddd;
|
||||
align-items : center;
|
||||
.snippetGroup{
|
||||
.animate(background-color);
|
||||
margin : 0px 8px;
|
||||
padding : 3px;
|
||||
font-size : 13px;
|
||||
border-radius : 5px;
|
||||
&:hover, &.selected{
|
||||
background-color : #999;
|
||||
}
|
||||
.text{
|
||||
line-height : 20px;
|
||||
.groupName{
|
||||
margin-left : 6px;
|
||||
font-size : 10px;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
.dropdown{
|
||||
visibility : visible;
|
||||
}
|
||||
}
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
visibility : hidden;
|
||||
z-index : 1000;
|
||||
padding : 5px;
|
||||
background-color : #ddd;
|
||||
.snippet{
|
||||
.animate(background-color);
|
||||
padding : 10px;
|
||||
cursor : pointer;
|
||||
font-size : 10px;
|
||||
i{
|
||||
margin-right: 8px;
|
||||
font-size : 13px;
|
||||
}
|
||||
&:hover{
|
||||
background-color : #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
position : relative;
|
||||
height : 100%;
|
||||
min-height : 100%;
|
||||
width : 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.textIcons{
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
.icon{
|
||||
display : inline-block;
|
||||
height : 30px;
|
||||
width : 30px;
|
||||
cursor : pointer;
|
||||
font-size : 1.5em;
|
||||
line-height : 30px;
|
||||
text-align : center;
|
||||
&:nth-child(8n + 1){ background-color: @blue; }
|
||||
&:nth-child(8n + 2){ background-color: @orange; }
|
||||
&:nth-child(8n + 3){ background-color: @teal; }
|
||||
&:nth-child(8n + 4){ background-color: @red; }
|
||||
&:nth-child(8n + 5){ background-color: @purple; }
|
||||
&:nth-child(8n + 6){ background-color: @silver; }
|
||||
&:nth-child(8n + 7){ background-color: @yellow; }
|
||||
&:nth-child(8n + 8){ background-color: @green; }
|
||||
}
|
||||
}
|
||||
.codeEditor{
|
||||
height : 100%;
|
||||
textarea{
|
||||
box-sizing : border-box;
|
||||
resize : none;
|
||||
overflow-y : scroll;
|
||||
height : 100%;
|
||||
width : 100%;
|
||||
padding : 10px;
|
||||
border : none;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,13 @@ module.exports = function(classname){
|
||||
"",
|
||||
"#### 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"),
|
||||
"- **Armor:** " + (_.sample(["Light armor", "Medium armor", "Heavy armor", "Shields"], _.random(0,3)).join(', ') || "None"),
|
||||
"- **Weapons:** " + (_.sample(["Squeegee", "Rubber Chicken", "Simple weapons", "Martial weapons"], _.random(0,2)).join(', ') || "None"),
|
||||
"- **Tools:** " + (_.sample(["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(', ')),
|
||||
"- **Saving Throws:** " + (_.sample(abilityList, 2).join(', ')),
|
||||
"- **Skills:** Choose two from " + (_.sample(skillList, _.random(4, 6)).join(', ')),
|
||||
"",
|
||||
"#### Equipment",
|
||||
"You start with the following equipment, in addition to the equipment granted by your background:",
|
||||
|
||||
@@ -1,105 +1,79 @@
|
||||
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"
|
||||
];
|
||||
module.exports = function(classname){
|
||||
|
||||
var classnames = ['Archivist', 'Fancyman', 'Linguist', 'Fletcher',
|
||||
'Notary', 'Berserker-Typist', 'Fishmongerer', 'Manicurist', 'Haberdasher', 'Concierge'];
|
||||
classname = classname || _.sample(['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 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"
|
||||
];
|
||||
|
||||
|
||||
module.exports = {
|
||||
full : function(classname){
|
||||
classname = 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 "##### The " + classname + "\n" +
|
||||
"___\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,
|
||||
"+" + Math.ceil(level/5 + 1),
|
||||
_.sampleSize(features, _.sample([0,1,1])).join(', ') || "Ability Score Improvement",
|
||||
cantrips,
|
||||
spells,
|
||||
drawSlots(slots)
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0,1);
|
||||
spells += _.random(0,1);
|
||||
slots += _.random(0,2);
|
||||
|
||||
return "| " + res + " |";
|
||||
}).join('\n') +'\n\n';
|
||||
},
|
||||
|
||||
half : function(classname){
|
||||
classname = classname || _.sample(classnames)
|
||||
|
||||
var featureScore = 1
|
||||
return "##### The " + classname + "\n" +
|
||||
"___\n" + "___\n" +
|
||||
"| Level | Proficiency Bonus | Features | " + _.sample(features) + "|\n" +
|
||||
"|:---:|:---:|:---|:---:|\n" +
|
||||
_.map(levels, function(levelName, level){
|
||||
var res = [
|
||||
levelName,
|
||||
"+" + Math.ceil(level/5 + 1),
|
||||
_.sampleSize(features, _.sample([0,1,1])).join(', ') || "Ability Score Improvement",
|
||||
"+" + featureScore
|
||||
].join(' | ');
|
||||
|
||||
featureScore += _.random(0,1);
|
||||
|
||||
return "| " + res + " |";
|
||||
}).join('\n') +'\n\n';
|
||||
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 extraWide = (_.random(0,1) === 0) ? "" : "___\n";
|
||||
|
||||
var cantrips = 3;
|
||||
var spells = 1;
|
||||
var slots = 2;
|
||||
return "##### The " + classname + "\n" +
|
||||
"___\n" + extraWide +
|
||||
"| Level | Proficiency Bonus | Features | Cantrips Known | Spells Known | 1st | 2nd | 3rd | 4th | 5th | 6th | 7th | 8th | 9th |\n"+
|
||||
"|:---:|:---:|:---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n" +
|
||||
_.map(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th", "11th", "12th", "13th", "14th", "15th", "16th", "17th", "18th", "19th", "20th"],function(levelName, level){
|
||||
var res = [
|
||||
levelName,
|
||||
"+" + Math.ceil(level/5 + 1),
|
||||
_.sample(features, _.sample([0,1,1])).join(', ') || "Ability Score Improvement",
|
||||
cantrips,
|
||||
spells,
|
||||
drawSlots(slots)
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0,1);
|
||||
spells += _.random(0,1);
|
||||
slots += _.random(0,2);
|
||||
|
||||
return "| " + res + " |";
|
||||
}).join('\n') +'\n\n';
|
||||
|
||||
};
|
||||
@@ -1,11 +1,12 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
var genList = function(list, max){
|
||||
return _.sampleSize(list, _.random(0,max)).join(', ') || "None";
|
||||
return _.sample(list, _.random(0,max)).join(', ') || "None";
|
||||
}
|
||||
|
||||
var getMonsterName = function(){
|
||||
return _.sample([
|
||||
module.exports = function(){
|
||||
|
||||
var monsterName = _.sample([
|
||||
"All-devouring Baseball Imp",
|
||||
"All-devouring Gumdrop Wraith",
|
||||
"Chocolate Hydra",
|
||||
@@ -58,14 +59,10 @@ var getMonsterName = function(){
|
||||
"Time Kangaroo",
|
||||
"Tomb Poodle",
|
||||
]);
|
||||
}
|
||||
|
||||
var getType = function(){
|
||||
return _.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast']) + " " + _.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])
|
||||
}
|
||||
var type = _.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast']) + " " + _.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])
|
||||
|
||||
var getAlignment = function(){
|
||||
return _.sample([
|
||||
var alignment =_.sample([
|
||||
"annoying evil",
|
||||
"chaotic gossipy",
|
||||
"chaotic sloppy",
|
||||
@@ -83,114 +80,89 @@ var getAlignment = function(){
|
||||
"wordy evil",
|
||||
"unaligned"
|
||||
]);
|
||||
};
|
||||
|
||||
var getStats = function(){
|
||||
return '>|' + _.times(6, function(){
|
||||
|
||||
|
||||
var stats = '>|' + _.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';
|
||||
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) ";
|
||||
}
|
||||
|
||||
|
||||
return [
|
||||
"___",
|
||||
"> ## " + monsterName,
|
||||
">*" + type + ", " + alignment+ "*",
|
||||
"> ___",
|
||||
"> - **Armor Class** " + _.random(10,20),
|
||||
"> - **Hit Points** " + _.random(1, 150) + "(1d4 + 5)",
|
||||
"> - **Speed** " + _.random(0,50) + "ft.",
|
||||
">___",
|
||||
">|STR|DEX|CON|INT|WIS|CHA|",
|
||||
">|:---:|:---:|:---:|:---:|:---:|:---:|",
|
||||
stats,
|
||||
">___",
|
||||
"> - **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';
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
@@ -4,172 +4,106 @@ var MonsterBlockGen = require('./monsterblock.gen.js');
|
||||
var ClassFeatureGen = require('./classfeature.gen.js');
|
||||
var FullClassGen = require('./fullclass.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 : "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"
|
||||
},
|
||||
|
||||
|
||||
]
|
||||
tooltip : 'Full Class',
|
||||
icon : 'fa-user',
|
||||
snippet : FullClassGen,
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
*/
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fa-book',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Spell',
|
||||
icon : 'fa-magic',
|
||||
gen : SpellGen,
|
||||
},
|
||||
{
|
||||
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 : 'Monster Stat Block',
|
||||
icon : 'fa-bug',
|
||||
gen : MonsterBlockGen.half,
|
||||
},
|
||||
{
|
||||
name : 'Wide Monster Stat Block',
|
||||
icon : 'fa-paw',
|
||||
gen : MonsterBlockGen.full,
|
||||
}
|
||||
]
|
||||
tooltip : 'Spell',
|
||||
icon : 'fa-magic',
|
||||
snippet : SpellGen,
|
||||
},
|
||||
|
||||
|
||||
|
||||
/********************* TABLES *********************/
|
||||
|
||||
{
|
||||
groupName : 'Tables',
|
||||
tooltip : 'Class Feature',
|
||||
icon : 'fa-trophy',
|
||||
snippet : ClassFeatureGen,
|
||||
},
|
||||
{
|
||||
tooltip : 'Note',
|
||||
icon : 'fa-sticky-note',
|
||||
snippet : function(){
|
||||
return [
|
||||
"> ##### Time to Drop Knowledge",
|
||||
"> Use notes to point out some interesting information. ",
|
||||
"> ",
|
||||
"> **Tables and lists** both work within a note."
|
||||
].join('\n');
|
||||
},
|
||||
},
|
||||
{
|
||||
tooltip : 'Table',
|
||||
icon : 'fa-list',
|
||||
snippet : 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');
|
||||
},
|
||||
},
|
||||
{
|
||||
tooltip : 'Monster Stat Block',
|
||||
icon : 'fa-bug',
|
||||
snippet : MonsterBlockGen,
|
||||
},
|
||||
{
|
||||
tooltip : "Class Table",
|
||||
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');
|
||||
},
|
||||
}
|
||||
]
|
||||
snippet : ClassTableGen,
|
||||
},
|
||||
{
|
||||
tooltip : "Column Break",
|
||||
icon : 'fa-columns',
|
||||
snippet : function(){
|
||||
return "```\n```\n\n";
|
||||
}
|
||||
},
|
||||
{
|
||||
tooltip : "New Page",
|
||||
icon : 'fa-file-text',
|
||||
snippet : function(){
|
||||
return "\\page\n\n";
|
||||
}
|
||||
},
|
||||
{
|
||||
tooltip : "Vertical Spacing",
|
||||
icon : 'fa-arrows-v',
|
||||
snippet : function(){
|
||||
return "<div style='margin-top:140px'></div>\n\n";
|
||||
}
|
||||
},
|
||||
{
|
||||
tooltip : "Insert Image",
|
||||
icon : 'fa-image',
|
||||
snippet : function(){
|
||||
return "<img src='https://i.imgur.com/RJ6S6eY.gif' style='position:absolute;bottom:-10px;right:-60px;' />";
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** 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')
|
||||
},
|
||||
]
|
||||
tooltip : "Page number & Footnote",
|
||||
icon : 'fa-book',
|
||||
snippet : function(){
|
||||
return "<div class='pageNumber'>1</div>\n<div class='footnote'>PART 1 | FANCINESS</div>\n\n";
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
{
|
||||
tooltip : "Ink Friendly",
|
||||
icon : 'fa-print',
|
||||
snippet : function(){
|
||||
return "<style>\n .phb{ background : white;}\n .phb img{ display : none;}\n .phb hr+blockquote{background : white;}\n</style>\n\n";
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
]
|
||||
@@ -56,9 +56,9 @@ module.exports = function(){
|
||||
var spellSchools = ["abjuration", "conjuration", "divination", "enchantment", "evocation", "illusion", "necromancy", "transmutation"];
|
||||
|
||||
|
||||
var components = _.sampleSize(["V", "S", "M"], _.random(1,3)).join(', ');
|
||||
var components = _.sample(["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(', ') + ")"
|
||||
components += " (" + _.sample(['a small doll', 'a crushed button worth at least 1cp', 'discarded gum wrapper'], _.random(1,3)).join(', ') + ")"
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
71
client/homebrew/homePage/homePage.jsx
Normal file
71
client/homebrew/homePage/homePage.jsx
Normal file
@@ -0,0 +1,71 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Statusbar = require('../statusbar/statusbar.jsx');
|
||||
var PageContainer = require('../pageContainer/pageContainer.jsx');
|
||||
var Editor = require('../editor/editor.jsx');
|
||||
|
||||
//var WelcomeText = require('./welcomeMessage.js');
|
||||
|
||||
|
||||
|
||||
var KEY = 'naturalCrit-homebrew';
|
||||
|
||||
var HomePage = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
welcomeText : ""
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
text: this.props.welcomeText
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
/*
|
||||
var storage = localStorage.getItem(KEY);
|
||||
if(storage){
|
||||
this.setState({
|
||||
text : storage
|
||||
})
|
||||
}
|
||||
*/
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text
|
||||
});
|
||||
|
||||
//localStorage.setItem(KEY, text);
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return(
|
||||
<div className='homePage'>
|
||||
<Statusbar
|
||||
printId="Nkbh52nx_l"
|
||||
/>
|
||||
<div className='paneSplit'>
|
||||
<div className='leftPane'>
|
||||
<Editor text={this.state.text} onChange={this.handleTextChange} />
|
||||
</div>
|
||||
<div className='rightPane'>
|
||||
<PageContainer text={this.state.text} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href='/homebrew/new' className='floatingNewButton'>
|
||||
Create your own <i className='fa fa-magic' />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = HomePage;
|
||||
@@ -1,7 +1,5 @@
|
||||
|
||||
.homePage{
|
||||
|
||||
|
||||
position : relative;
|
||||
a.floatingNewButton{
|
||||
.animate(background-color);
|
||||
120
client/homebrew/homePage/welcome_msg.txt
Normal file
120
client/homebrew/homePage/welcome_msg.txt
Normal file
@@ -0,0 +1,120 @@
|
||||
# The Homebrewery
|
||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite.
|
||||
|
||||
Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||
|
||||
### Homebrew made easy
|
||||
The Homebrewery allows for the creation and sharing of authentic looking Fifth-Edition homebrews, with just text editing. It accomplishes this by using [Markdown](https://help.github.com/articles/markdown-basics/) along with some custom CSS-styling.
|
||||
|
||||
Stop worrying about learning photoshop, fiddling with spacing, or tracking down the PHB assets. Just focus on making your homebrew **great**.
|
||||
|
||||
**Try it! **Simply edit the text on the left and watch it *update live* on the right.
|
||||
|
||||
|
||||
#### Features
|
||||
* Monster Stat Blocks
|
||||
* Full class tables
|
||||
* Notes and Tables
|
||||
* Images
|
||||
* Vertical spacing, column breaks, and multiple pages
|
||||
|
||||
|
||||
#### Snippets
|
||||
If you aren't used the Markdown-style syntax, don't worry! I've provided several **snippets** at the top of the editor. When clicked, these will *inject* text wherever your text cursor was.
|
||||
|
||||
Each snippet is a common format from the Player's Handbook or is a feature of The Homebrewery. You'll never have to memorize exactly how a Monster Stat Block is suppose to be formatted.
|
||||
|
||||
### Editing and Sharing
|
||||
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
|
||||
|
||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||
|
||||
|
||||
> ##### Words of Caution
|
||||
> ___
|
||||
> * **Concurrent Editing** The Homebrewery does not support concurrent user editing. It's best one user at a time makes edits to avoid overwriting each other.
|
||||
> * **Back-up your brews** I can not guarantee that I will support this project indefinitely. So if you'd like to hang on to your creation be sure to back up it up.
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## New Things!
|
||||
What's new in the latest update? Check out the full changelog [here](https://github.com/stolksdorf/NaturalCrit/blob/master/changelog.md)
|
||||
|
||||
* **View Source** on the share page to see the markdown text for the brew
|
||||
* **Fixed Server Issues** should increase stability of the site greatly
|
||||
* **Footnotes & Page Numbers**
|
||||
* **Print View** displays your brew ready for printing, saving to PDF or image.
|
||||
* **Footer Accent** now switches directions each page, neat!
|
||||
* **Standalone Styling** the PHB-style has been reduced to a single file
|
||||
* **Reduced asset sizes** This should help with page load times
|
||||
|
||||
>##### PDF Exporting
|
||||
>The best way to do a PDF export is to use the **print view** of a brew, print that page and save as PDF.
|
||||
>
|
||||
>***"But there's no columns when I do this in Chrome!"***
|
||||
>
|
||||
>This is a known bug in Chrome for **five years**. When saving to PDF, it doesn't respect columns. Amazingly this was just fixed [last month](https://code.google.com/p/chromium/issues/detail?id=99358), but hasn't been deployed yet.
|
||||
>
|
||||
>Converting to PDF *precisely* is **very** difficult. There are many services and libraries out there, but none of them have gotten it right to the level I'm happy with. Most of them use Chrome's engine which has the aforementioned bug in it.
|
||||
>
|
||||
>This is why I made the **print view**. It gives you a single completely standalone HTML version of your brew; Download it, export it, screenshot it, print it, *do whatever you want*.
|
||||
|
||||
|
||||
|
||||
## Bugs, Issues, Suggestions?
|
||||
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://github.com/stolksdorf/NaturalCrit/issues/new) and let me know!.
|
||||
|
||||
|
||||
|
||||
|
||||
<img src='http://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
|
||||
|
||||
|
||||
\page
|
||||
|
||||
# Appendix
|
||||
|
||||
### Not quite Markdown
|
||||
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
|
||||
|
||||
___
|
||||
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
|
||||
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
|
||||
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
|
||||
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
|
||||
|
||||
|
||||
|
||||
### Images
|
||||
Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
|
||||
|
||||
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. The image **snippet** provides an example of doing this.
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
### Legal Junk
|
||||
You are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself. If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
|
||||
### Things that don't work
|
||||
There are a few things I couldn't get right
|
||||
|
||||
* Spell save block, with centered text and sans serif are not support. Ran out of mark-up to use
|
||||
* Full page monster stat blocks
|
||||
* "Spell slots per level" text above the levels on a class table.
|
||||
* I built this for Chrome, so if it looks weird to you, use Chrome instead.
|
||||
|
||||
<div class='pageNumber'>2</div>
|
||||
<div class='footnote'>PART 2 | BORING STUFF</div>
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ var cx = require('classnames');
|
||||
|
||||
var CreateRouter = require('pico-router').createRouter;
|
||||
|
||||
var HomePage = require('./pages/homePage/homePage.jsx');
|
||||
var EditPage = require('./pages/editPage/editPage.jsx');
|
||||
var SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
var NewPage = require('./pages/newPage/newPage.jsx');
|
||||
var HomePage = require('./homePage/homePage.jsx');
|
||||
var EditPage = require('./editPage/editPage.jsx');
|
||||
var SharePage = require('./sharePage/sharePage.jsx');
|
||||
|
||||
var Router;
|
||||
var Homebrew = React.createClass({
|
||||
@@ -15,10 +14,8 @@ var Homebrew = React.createClass({
|
||||
return {
|
||||
url : "",
|
||||
welcomeText : "",
|
||||
changelog : "",
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
text : "",
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
@@ -29,17 +26,10 @@ var Homebrew = React.createClass({
|
||||
componentWillMount: function() {
|
||||
Router = CreateRouter({
|
||||
'/homebrew/edit/:id' : (args) => {
|
||||
return <EditPage id={args.id} brew={this.props.brew} />
|
||||
return <EditPage id={args.id} entry={this.props.brew} />
|
||||
},
|
||||
|
||||
'/homebrew/share/:id' : (args) => {
|
||||
return <SharePage id={args.id} brew={this.props.brew} />
|
||||
},
|
||||
'/homebrew/changelog' : (args) => {
|
||||
return <SharePage brew={{title : 'Changelog', text : this.props.changelog}} />
|
||||
},
|
||||
'/homebrew/new' : (args) => {
|
||||
return <NewPage />
|
||||
return <SharePage id={args.id} entry={this.props.brew} />
|
||||
},
|
||||
'/homebrew*' : <HomePage welcomeText={this.props.welcomeText} />,
|
||||
});
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.homebrew{
|
||||
height : 100%;
|
||||
@import 'naturalCrit/styles/reset.less';
|
||||
//@import 'naturalCrit/styles/elements.less';
|
||||
@import 'naturalCrit/styles/animations.less';
|
||||
@import 'naturalCrit/styles/colors.less';
|
||||
@import 'naturalCrit/styles/tooltip.less';
|
||||
|
||||
//TODO: Consider making backgroudn color lighter
|
||||
background-color : @steel;
|
||||
.page{
|
||||
display : flex;
|
||||
height : 100%;
|
||||
flex-direction : column;
|
||||
.content{
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
html,body, #reactContainer{
|
||||
min-height: 100%;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
.homebrew{
|
||||
background-color: @steel;
|
||||
height : 100%;
|
||||
|
||||
.paneSplit{
|
||||
width : 100%;
|
||||
height: 100vh;
|
||||
padding-top: 25px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
.leftPane, .rightPane{
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.leftPane{
|
||||
width : 40%;
|
||||
}
|
||||
.rightPane{
|
||||
width : 60%;
|
||||
height: 100%;
|
||||
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const MAX_TITLE_LENGTH = 50;
|
||||
|
||||
|
||||
var EditTitle = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title : '',
|
||||
onChange : function(){}
|
||||
};
|
||||
},
|
||||
|
||||
handleChange : function(e){
|
||||
if(e.target.value.length > MAX_TITLE_LENGTH) return;
|
||||
this.props.onChange(e.target.value);
|
||||
},
|
||||
render : function(){
|
||||
return <Nav.item className='editTitle'>
|
||||
<input placeholder='Brew Title' type='text' value={this.props.title} onChange={this.handleChange} />
|
||||
|
||||
<div className={cx('charCount', {'max' : this.props.title.length >= MAX_TITLE_LENGTH})}>
|
||||
{this.props.title.length}/{MAX_TITLE_LENGTH}
|
||||
</div>
|
||||
</Nav.item>
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = EditTitle;
|
||||
@@ -1,8 +0,0 @@
|
||||
var React = require('react');
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
var Navbar = React.createClass({
|
||||
render : function(){
|
||||
return <Nav.base>
|
||||
<Nav.section>
|
||||
<Nav.logo />
|
||||
<Nav.item href='/homebrew' className='homebrewLogo'>
|
||||
<div>The Homebrewery</div>
|
||||
</Nav.item>
|
||||
<Nav.item>v2.0.0</Nav.item>
|
||||
</Nav.section>
|
||||
{this.props.children}
|
||||
</Nav.base>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Navbar;
|
||||
@@ -1,58 +0,0 @@
|
||||
|
||||
.homebrew nav{
|
||||
.homebrewLogo{
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div{
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover{
|
||||
color : @blue;
|
||||
}
|
||||
}
|
||||
.editTitle.navItem{
|
||||
padding : 2px 12px;
|
||||
input{
|
||||
margin : 0;
|
||||
padding : 2px;
|
||||
width : 250px;
|
||||
background-color : #444;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
}
|
||||
.charCount{
|
||||
display : inline-block;
|
||||
vertical-align : bottom;
|
||||
margin-left : 8px;
|
||||
text-align : right;
|
||||
color : #666;
|
||||
&.max{
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brewTitle.navItem{
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-transform: initial;
|
||||
}
|
||||
.patreon.navItem{
|
||||
i{
|
||||
.animate(color);
|
||||
&:hover{
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
var React = require('react');
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item
|
||||
className='patreon'
|
||||
newTab={true}
|
||||
href='https://www.patreon.com/stolksdorf'
|
||||
color='green'
|
||||
icon='fa-heart'>
|
||||
help out
|
||||
</Nav.item>
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
var React = require('react');
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item newTab={true} href={'/homebrew/print/' + props.shareId +'?dialog=true'} color='purple' icon='fa-print'>
|
||||
print
|
||||
</Nav.item>
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
//var striptags = require('striptags');
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
const MAX_URL_SIZE = 2083;
|
||||
const MAIN_URL = "https://www.reddit.com/r/UnearthedArcana/submit?selftext=true"
|
||||
|
||||
|
||||
var RedditShare = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
sharedId : '',
|
||||
text : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getText : function(){
|
||||
|
||||
},
|
||||
|
||||
|
||||
handleClick : function(){
|
||||
var url = [
|
||||
MAIN_URL,
|
||||
'title=' + encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!'),
|
||||
|
||||
'text=' + encodeURIComponent(this.props.brew.text)
|
||||
|
||||
|
||||
].join('&');
|
||||
|
||||
window.open(url, '_blank');
|
||||
},
|
||||
|
||||
|
||||
render : function(){
|
||||
return <Nav.item icon='fa-reddit-alien' color='red' onClick={this.handleClick}>
|
||||
share on reddit
|
||||
</Nav.item>
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
module.exports = RedditShare;
|
||||
28
client/homebrew/pageContainer/pageContainer.jsx
Normal file
28
client/homebrew/pageContainer/pageContainer.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Markdown = require('marked');
|
||||
|
||||
var PageContainer = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
text : ""
|
||||
};
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
return _.map(this.props.text.split('\\page'), (pageText, index) => {
|
||||
return <div className='phb' dangerouslySetInnerHTML={{__html:Markdown(pageText)}} key={index} />
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return <div className="pageContainer">
|
||||
{this.renderPages()}
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PageContainer;
|
||||
12
client/homebrew/pageContainer/pageContainer.less
Normal file
12
client/homebrew/pageContainer/pageContainer.less
Normal file
@@ -0,0 +1,12 @@
|
||||
@import (less) './client/homebrew/phbStyle/phb.style.less';
|
||||
|
||||
.pageContainer{
|
||||
padding : 30px 0px;
|
||||
background-color : @steel;
|
||||
&>.phb{
|
||||
margin-right : auto;
|
||||
margin-bottom : 30px;
|
||||
margin-left : auto;
|
||||
box-shadow : 1px 4px 14px #000;
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
var request = require("superagent");
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
var Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
var EditTitle = require('../../navbar/editTitle.navitem.jsx');
|
||||
var ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
var PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
|
||||
|
||||
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
var Editor = require('../../editor/editor.jsx');
|
||||
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
|
||||
|
||||
|
||||
var EditPage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
id : null,
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
title : this.props.brew.title,
|
||||
text: this.props.brew.text,
|
||||
isSaving : false,
|
||||
isPending : false,
|
||||
errors : null,
|
||||
lastUpdated : this.props.brew.updatedAt
|
||||
};
|
||||
},
|
||||
savedBrew : null,
|
||||
|
||||
componentDidMount: function(){
|
||||
this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||
window.onbeforeunload = ()=>{
|
||||
if(this.state.isSaving || this.state.isPending){
|
||||
return 'You have unsaved changes!';
|
||||
}
|
||||
}
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
window.onbeforeunload = function(){};
|
||||
},
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.refs.editor.update();
|
||||
},
|
||||
|
||||
handleTitleChange : function(title){
|
||||
this.setState({
|
||||
title : title,
|
||||
isPending : true
|
||||
});
|
||||
|
||||
(this.hasChanges() ? this.debounceSave() : this.debounceSave.cancel());
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text,
|
||||
isPending : true
|
||||
});
|
||||
|
||||
(this.hasChanges() ? this.debounceSave() : this.debounceSave.cancel());
|
||||
},
|
||||
|
||||
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('/homebrew/api/remove/' + this.props.brew.editId)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
window.location.href = '/homebrew';
|
||||
});
|
||||
},
|
||||
|
||||
hasChanges : function(){
|
||||
if(this.savedBrew){
|
||||
if(this.state.text !== this.savedBrew.text) return true;
|
||||
if(this.state.title !== this.savedBrew.title) return true;
|
||||
}else{
|
||||
if(this.state.text !== this.props.brew.text) return true;
|
||||
if(this.state.title !== this.props.brew.title) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
save : function(){
|
||||
this.debounceSave.cancel();
|
||||
this.setState({
|
||||
isSaving : true
|
||||
});
|
||||
|
||||
request
|
||||
.put('/homebrew/api/update/' + this.props.brew.editId)
|
||||
.send({
|
||||
text : this.state.text,
|
||||
title : this.state.title
|
||||
})
|
||||
.end((err, res) => {
|
||||
this.savedBrew = res.body;
|
||||
this.setState({
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
lastUpdated : res.body.updatedAt
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item className='save' icon="fa-spinner fa-spin">saving...</Nav.item>
|
||||
}
|
||||
if(!this.state.isPending && !this.state.isSaving){
|
||||
return <Nav.item className='save saved'>saved.</Nav.item>
|
||||
}
|
||||
if(this.state.isPending && this.hasChanges()){
|
||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>
|
||||
}
|
||||
},
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<EditTitle title={this.state.title} onChange={this.handleTitleChange} />
|
||||
</Nav.section>
|
||||
<Nav.section>
|
||||
{this.renderSaveButton()}
|
||||
<Nav.item newTab={true} href={'/homebrew/share/' + this.props.brew.shareId} color='teal' icon='fa-share-alt'>
|
||||
Share
|
||||
</Nav.item>
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<Nav.item color='red' icon='fa-trash' onClick={this.handleDelete}>
|
||||
Delete
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='editPage page'>
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
|
||||
<BrewRenderer text={this.state.text} />
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = EditPage;
|
||||
@@ -1,12 +0,0 @@
|
||||
.editPage{
|
||||
|
||||
.navItem.save{
|
||||
width : 75px;
|
||||
text-align: center;
|
||||
&.saved{
|
||||
color : #666;
|
||||
cursor : initial;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
var Navbar = require('../../navbar/navbar.jsx');
|
||||
var PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
|
||||
|
||||
|
||||
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
var Editor = require('../../editor/editor.jsx');
|
||||
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
|
||||
var HomePage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
welcomeText : ""
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
text: this.props.welcomeText
|
||||
};
|
||||
},
|
||||
handleSplitMove : function(){
|
||||
this.refs.editor.update();
|
||||
},
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text
|
||||
});
|
||||
},
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
<Nav.item newTab={true} href='/homebrew/changelog' color='purple' icon='fa-file-text-o'>
|
||||
Changelog
|
||||
</Nav.item>
|
||||
<Nav.item href='/homebrew/new' color='green' icon='fa-external-link'>
|
||||
New Brew
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='homePage page'>
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
|
||||
<BrewRenderer text={this.state.text} />
|
||||
</SplitPane>
|
||||
</div>
|
||||
|
||||
<a href='/homebrew/new' className='floatingNewButton'>
|
||||
Create your own <i className='fa fa-magic' />
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = HomePage;
|
||||
@@ -1,107 +0,0 @@
|
||||
# The Homebrewery
|
||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||
|
||||
### Homebrew D&D made easy
|
||||
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||
|
||||
**Try it! **Simply edit the text on the left and watch it *update live* on the right.
|
||||
|
||||
|
||||
|
||||
|
||||
#### Features
|
||||
* Monster Stat Blocks
|
||||
* Full class tables
|
||||
* Notes and Tables
|
||||
* Images
|
||||
* Page numbering and footers
|
||||
* Vertical spacing, column breaks, and multiple pages
|
||||
|
||||
|
||||
|
||||
### Editing and Sharing
|
||||
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
|
||||
|
||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||
|
||||
## Helping out
|
||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/stolksdorf) to help me keep the servers running.
|
||||
|
||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||
|
||||
### Bugs, Issues, Suggestions?
|
||||
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://github.com/stolksdorf/NaturalCrit/issues/new) and let me know!.
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## New Things in v2.0.0!
|
||||
What's new in the latest update? Check out the full changelog [here](/homebrew/changelog)
|
||||
|
||||
* **A whole new look** The site has been re-built from the ground up!
|
||||
* **Better editor and Split Pane** Syntax highlighting will make writing your brews even easier, and now you can customize how large your editor is.
|
||||
* **More reliable rendering** Lots of work has been put into making the rendering more reliable, not just for web, but also for PDFs
|
||||
* **PDF Printing on Chrome** You don't need to use Chrome Canary anymore!
|
||||
* ** Performance Improvements** The site should load faster, save faster, and render large brews *much* faster.
|
||||
* **Patreon page** If you like this tool and want to show some thanks you can [head here](https://www.patreon.com/stolksdorf).
|
||||
|
||||
>##### PDF Exporting
|
||||
> After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up
|
||||
> * Set the **Destination** to "Save as PDF"
|
||||
> * Set **Paper Size** to "Letter"
|
||||
> * If you are printing on A4 paper, make sure to have the "A4 page size snippet" in your brew
|
||||
> * In **Options** make sure "Background Images" is selected.
|
||||
> * Hit print and enjoy! You're done!
|
||||
>
|
||||
> If you want to save ink or have a monochrome printer, add the **Ink Friendly** snippet to your brew before you print
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<img src='http://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
|
||||
|
||||
|
||||
\page
|
||||
|
||||
# Appendix
|
||||
|
||||
### Not quite Markdown
|
||||
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
|
||||
|
||||
___
|
||||
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
|
||||
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
|
||||
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
|
||||
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
|
||||
|
||||
|
||||
|
||||
### Images
|
||||
Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
|
||||
|
||||
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. I have added both a inflow image snippet and a background image snippet to give you exmaples of how to do it.
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
### Legal Junk
|
||||
You are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself. If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
### Crediting Me
|
||||
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
|
||||
|
||||
<div class='pageNumber'>2</div>
|
||||
<div class='footnote'>PART 2 | BORING STUFF</div>
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
var request = require("superagent");
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
var Navbar = require('../../navbar/navbar.jsx');
|
||||
var EditTitle = require('../../navbar/editTitle.navitem.jsx');
|
||||
|
||||
|
||||
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
var Editor = require('../../editor/editor.jsx');
|
||||
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
|
||||
const KEY = 'naturalCrit-homebrew-new';
|
||||
|
||||
var NewPage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
title : 'My Awesome Brew v99',
|
||||
text: '',
|
||||
isSaving : false
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
componentDidMount: function() {
|
||||
var storage = localStorage.getItem(KEY);
|
||||
if(storage){
|
||||
this.setState({
|
||||
text : storage
|
||||
})
|
||||
}
|
||||
window.onbeforeunload = (e)=>{
|
||||
if(this.state.text == '') return;
|
||||
return "Your homebrew isn't saved. Are you sure you want to leave?";
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.onbeforeunload = function(){};
|
||||
},
|
||||
|
||||
handleSplitMove : function(){
|
||||
this.refs.editor.update();
|
||||
},
|
||||
|
||||
handleTitleChange : function(title){
|
||||
this.setState({
|
||||
title : title
|
||||
});
|
||||
},
|
||||
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text
|
||||
});
|
||||
localStorage.setItem(KEY, text);
|
||||
},
|
||||
|
||||
handleSave : function(){
|
||||
this.setState({
|
||||
isSaving : true
|
||||
});
|
||||
request.post('/homebrew/api')
|
||||
.send({
|
||||
title : this.state.title,
|
||||
text : this.state.text
|
||||
})
|
||||
.end((err, res)=>{
|
||||
|
||||
if(err){
|
||||
this.setState({
|
||||
isSaving : false
|
||||
});
|
||||
return;
|
||||
}
|
||||
window.onbeforeunload = function(){};
|
||||
var brew = res.body;
|
||||
localStorage.removeItem(KEY);
|
||||
window.location = '/homebrew/edit/' + brew.editId;
|
||||
})
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
|
||||
save...
|
||||
</Nav.item>
|
||||
}else{
|
||||
return <Nav.item icon='fa-save' className='saveButton' onClick={this.handleSave}>
|
||||
save
|
||||
</Nav.item>
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<EditTitle title={this.state.title} onChange={this.handleTitleChange} />
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
{this.renderSaveButton()}
|
||||
<Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='newPage page'>
|
||||
{this.renderNavbar()}
|
||||
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
|
||||
<BrewRenderer text={this.state.text} />
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NewPage;
|
||||
@@ -1,10 +0,0 @@
|
||||
.newPage{
|
||||
|
||||
.saveButton{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
var Navbar = require('../../navbar/navbar.jsx');
|
||||
|
||||
var PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
|
||||
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
var SharePage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
shareId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
views : 0
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='sharePage page'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<Nav.item href={'/homebrew/source/' + this.props.brew.shareId} color='teal' icon='fa-code'>
|
||||
source
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.props.brew.text} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SharePage;
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
|
||||
@import (less) 'shared/naturalcrit/styles/reset.less';
|
||||
@import (less) 'shared/naturalcrit/phbStyle/phb.fonts.css';
|
||||
@import (less) 'shared/naturalcrit/phbStyle/phb.assets.less';
|
||||
@import (less) 'shared/naturalCrit/styles/reset.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
|
||||
@import (less) './client/homebrew/phbStyle/phb.assets.less';
|
||||
//Colors
|
||||
@background : #EEE5CE;
|
||||
@noteGreen : #e0e5c1;
|
||||
@@ -9,7 +9,6 @@
|
||||
@horizontalRule : #9c2b1b;
|
||||
@headerText : #58180D;
|
||||
@monsterStatBackground : #FDF1DC;
|
||||
@page { margin: 0; }
|
||||
.useSansSerif(){
|
||||
font-family : ScalySans;
|
||||
em{
|
||||
@@ -17,9 +16,9 @@
|
||||
font-style : italic;
|
||||
}
|
||||
strong{
|
||||
font-family : ScalySans;
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
font-family : ScalySans;
|
||||
font-weight : 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
}
|
||||
.useColumns(){
|
||||
@@ -36,21 +35,19 @@
|
||||
}
|
||||
.phb{
|
||||
.useColumns();
|
||||
position : relative;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
height : 279.4mm;
|
||||
width : 215.9mm;
|
||||
padding : 1.0cm 1.7cm;
|
||||
padding-bottom : 1.5cm;
|
||||
background-color : @background;
|
||||
background-image : @backgroundImage;
|
||||
font-family : BookSanity;
|
||||
font-size : 0.317cm;
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
position : relative;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
height : 279.4mm;
|
||||
width : 215.9mm;
|
||||
padding : 1.0cm 1.7cm;
|
||||
padding-bottom : 1.5cm;
|
||||
background-color : @background;
|
||||
background-image : @backgroundImage;
|
||||
font-family : BookSanity;
|
||||
font-size : 9pt;
|
||||
text-rendering : optimizeLegibility;
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
@@ -65,37 +62,25 @@
|
||||
ul{
|
||||
margin-bottom : 0.8em;
|
||||
line-height : 1.3em;
|
||||
list-style-position : outside;
|
||||
list-style-position : inside;
|
||||
list-style-type : disc;
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
ol{
|
||||
list-style-position: inside;
|
||||
margin-bottom : 0.8em;
|
||||
line-height : 1.3em;
|
||||
list-style-position : outside;
|
||||
list-style-type : decimal;
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
img{
|
||||
z-index : -1;
|
||||
}
|
||||
strong{
|
||||
font-weight : bold;
|
||||
letter-spacing : 0.03em;
|
||||
font-weight : bold;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
em{
|
||||
font-style : italic;
|
||||
}
|
||||
sup{
|
||||
vertical-align : super;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
sub{
|
||||
vertical-align : sub;
|
||||
font-size : smaller;
|
||||
line-height : 0;
|
||||
}
|
||||
//*****************************
|
||||
// * HEADERS
|
||||
// *****************************/
|
||||
@@ -107,33 +92,33 @@
|
||||
color : @headerText;
|
||||
}
|
||||
h1{
|
||||
column-span : all;
|
||||
font-size : 0.987cm;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
column-span : 2;
|
||||
font-size : 28pt;
|
||||
-webkit-column-span : 2;
|
||||
-moz-column-span : 2;
|
||||
&+p::first-letter{
|
||||
float : left;
|
||||
font-family : Solberry;
|
||||
font-size : 10em;
|
||||
color : #222;
|
||||
line-height : 0.8em;
|
||||
float : left;
|
||||
font-family : Solberry;
|
||||
line-height: 0.8em;
|
||||
font-size : 10em;
|
||||
color : #222;
|
||||
}
|
||||
}
|
||||
h2{
|
||||
font-size : 0.705cm;
|
||||
font-size : 20pt;
|
||||
}
|
||||
h3{
|
||||
font-size : 0.529cm;
|
||||
font-size : 15pt;
|
||||
border-bottom : 2px solid @headerUnderline;
|
||||
}
|
||||
h4{
|
||||
margin-bottom : 0.00em;
|
||||
font-size : 0.458cm;
|
||||
font-size : 12pt;
|
||||
}
|
||||
h5{
|
||||
margin-bottom : 0.2em;
|
||||
font-family : ScalySansSmallCaps;
|
||||
font-size : 0.423cm;
|
||||
font-size : 13pt;
|
||||
font-weight : 900;
|
||||
}
|
||||
//*****************************
|
||||
@@ -173,32 +158,22 @@
|
||||
margin-bottom : 1em;
|
||||
padding : 5px 10px;
|
||||
background-color : @noteGreen;
|
||||
border-style: solid;
|
||||
border-width: 11px;
|
||||
border-image: @noteBorderImage 11;
|
||||
border-image-outset: 9px 0px;
|
||||
border-top : 2px black solid;
|
||||
border-bottom : 2px black solid;
|
||||
box-shadow : 1px 4px 14px #888;
|
||||
p, ul{
|
||||
font-size : 0.352cm;
|
||||
font-size : 10pt;
|
||||
line-height : 1.1em;
|
||||
}
|
||||
}
|
||||
//If a note starts a column, give it space at the top to render border
|
||||
pre+blockquote{
|
||||
margin-top: 11px;
|
||||
}
|
||||
//*****************************
|
||||
// * MONSTER STAT BLOCK
|
||||
// *****************************/
|
||||
hr+blockquote{
|
||||
position : relative;
|
||||
padding-top : 15px;
|
||||
background-color : @monsterStatBackground;
|
||||
border-style : solid;
|
||||
border-width : 10px;
|
||||
border-image : @monsterBorderImage 10;
|
||||
border : none;
|
||||
h2{
|
||||
margin-top : -8px;
|
||||
margin-bottom : 0px;
|
||||
&+p{
|
||||
padding-bottom : 0px;
|
||||
@@ -212,7 +187,7 @@
|
||||
ul{
|
||||
.useSansSerif();
|
||||
padding-left : 1em;
|
||||
font-size : 0.352cm;
|
||||
font-size : 10pt;
|
||||
color : @headerText;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
@@ -223,7 +198,6 @@
|
||||
column-span : 1;
|
||||
background-color : transparent;
|
||||
border-image : none;
|
||||
border-style : none;
|
||||
-webkit-column-span : 1;
|
||||
tbody{
|
||||
tr:nth-child(odd), tr:nth-child(even){
|
||||
@@ -241,12 +215,44 @@
|
||||
}
|
||||
//Triangle dividers
|
||||
hr{
|
||||
visibility : visible;
|
||||
height : 6px;
|
||||
margin : 4px 0px;
|
||||
background-image : @redTriangleImage;
|
||||
background-size : 100% 100%;
|
||||
border : none;
|
||||
@height : 3px;
|
||||
position : relative;
|
||||
visibility : visible;
|
||||
margin : 8px 0px;
|
||||
border-color : transparent;
|
||||
&:after, &:before{
|
||||
content : "";
|
||||
position : absolute;
|
||||
left : 0px;
|
||||
height : @height;
|
||||
width : 100%;
|
||||
}
|
||||
&:before{
|
||||
top : -@height;
|
||||
background : linear-gradient(to right top, @horizontalRule 40%, transparent 50%)
|
||||
}
|
||||
&:after{
|
||||
top : 0px;
|
||||
background : linear-gradient(to right bottom, @horizontalRule 40%, transparent 50%)
|
||||
}
|
||||
}
|
||||
//Top and Bottom Borders
|
||||
&:after, &:before{
|
||||
content : "";
|
||||
position : absolute;
|
||||
height : 4px;
|
||||
width : 100%;
|
||||
padding : 0px 3px;
|
||||
background-color : #E69A28;
|
||||
border : 1px solid black;
|
||||
}
|
||||
&:before{
|
||||
top : 0px;
|
||||
left : -3px;
|
||||
}
|
||||
&:after{
|
||||
bottom : 0px;
|
||||
left : -3px;
|
||||
}
|
||||
}
|
||||
//Full Width
|
||||
@@ -254,7 +260,7 @@
|
||||
.useColumns();
|
||||
}
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// * FULL CLASS TABLE
|
||||
// *****************************/
|
||||
hr+table{
|
||||
margin-top : -5px;
|
||||
@@ -263,18 +269,12 @@
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 37px 17px;
|
||||
border-image-repeat : round;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
}
|
||||
h5+hr+table{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
//*****************************
|
||||
// * FOOTER
|
||||
// *****************************/
|
||||
@@ -333,7 +333,7 @@
|
||||
list-style-type : none;
|
||||
}
|
||||
//Double hr for full width elements
|
||||
hr+hr+blockquote{
|
||||
hr+hr+table, hr+hr+blockquote{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
@@ -356,21 +356,4 @@
|
||||
h4+p+hr+ul{
|
||||
margin-top : -0.5em
|
||||
}
|
||||
//Text indent right after table
|
||||
table+p{
|
||||
text-indent : 1em;
|
||||
}
|
||||
// Nested lists
|
||||
ul ul,ol ol,ul ol,ol ul{
|
||||
margin-bottom : 0px;
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * PRINT
|
||||
// *****************************/
|
||||
.phb.print{
|
||||
blockquote{
|
||||
box-shadow : none;
|
||||
}
|
||||
}
|
||||
40
client/homebrew/sharePage/sharePage.jsx
Normal file
40
client/homebrew/sharePage/sharePage.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Statusbar = require('../statusbar/statusbar.jsx');
|
||||
|
||||
var PageContainer = require('../pageContainer/pageContainer.jsx');
|
||||
|
||||
var SharePage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
id : null,
|
||||
entry : {
|
||||
text : "",
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
views : 0
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return(
|
||||
<div className='sharePage'>
|
||||
<Statusbar
|
||||
sourceText={this.props.entry.text}
|
||||
lastUpdated={this.props.entry.updatedAt}
|
||||
views={this.props.entry.views}
|
||||
printId={this.props.entry.shareId}
|
||||
/>
|
||||
|
||||
<PageContainer text={this.props.entry.text} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SharePage;
|
||||
134
client/homebrew/statusbar/statusbar.jsx
Normal file
134
client/homebrew/statusbar/statusbar.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
var Moment = require('moment');
|
||||
|
||||
var Logo = require('naturalCrit/logo/logo.jsx');
|
||||
|
||||
var replaceAll = function(str, find, replace) {
|
||||
return str.replace(new RegExp(find, 'g'), replace);
|
||||
}
|
||||
|
||||
var Statusbar = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
//editId: null,
|
||||
sourceText : null,
|
||||
shareId : null,
|
||||
printId : null,
|
||||
isPending : false,
|
||||
lastUpdated : null,
|
||||
info : null,
|
||||
views : 0
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
//Updates the last updated text every 10 seconds
|
||||
if(this.props.lastUpdated){
|
||||
this.refreshTimer = setInterval(()=>{
|
||||
this.forceUpdate();
|
||||
}, 10000)
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
clearInterval(this.refreshTimer);
|
||||
},
|
||||
|
||||
|
||||
openSourceWindow : function(){
|
||||
var sourceWindow = window.open();
|
||||
|
||||
var content = replaceAll(this.props.sourceText, '<', '<');
|
||||
content = replaceAll(content, '>', '>');
|
||||
|
||||
console.log(content);
|
||||
|
||||
sourceWindow.document.write('<code><pre>' + content + '</pre></code>');
|
||||
},
|
||||
|
||||
|
||||
|
||||
renderInfo : function(){
|
||||
if(!this.props.lastUpdated) return null;
|
||||
|
||||
return [
|
||||
<div className='views' key='views'>
|
||||
Views: {this.props.views}
|
||||
</div>,
|
||||
<div className='lastUpdated' key='lastUpdated'>
|
||||
Last updated: {Moment(this.props.lastUpdated).fromNow()}
|
||||
</div>
|
||||
];
|
||||
|
||||
},
|
||||
|
||||
renderSourceButton : function(){
|
||||
if(!this.props.sourceText) return null;
|
||||
|
||||
return <a className='sourceField' onClick={this.openSourceWindow}>
|
||||
View Source <i className='fa fa-code' />
|
||||
</a>
|
||||
},
|
||||
|
||||
renderNewButton : function(){
|
||||
if(this.props.editId || this.props.shareId) return null;
|
||||
|
||||
return <a className='newButton' target='_blank' href='/homebrew/new'>
|
||||
New Brew <i className='fa fa-external-link' />
|
||||
</a>
|
||||
},
|
||||
|
||||
renderShare : function(){
|
||||
if(!this.props.shareId) return null;
|
||||
|
||||
return <a className='shareField' key='share' href={'/homebrew/share/' + this.props.shareId} target="_blank">
|
||||
Share Link <i className='fa fa-external-link' />
|
||||
</a>
|
||||
},
|
||||
|
||||
renderPrintButton : function(){
|
||||
if(!this.props.printId) return null;
|
||||
|
||||
return <a className='printField' key='print' href={'/homebrew/print/' + this.props.printId} target="_blank">
|
||||
Print View <i className='fa fa-print' />
|
||||
</a>
|
||||
},
|
||||
|
||||
renderStatus : function(){
|
||||
if(!this.props.editId) return null;
|
||||
|
||||
var text = 'Saved.'
|
||||
if(this.props.isPending){
|
||||
text = 'Saving...'
|
||||
}
|
||||
return <div className='savingStatus'>
|
||||
{text}
|
||||
</div>
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='statusbar'>
|
||||
<Logo
|
||||
hoverSlide={true}
|
||||
/>
|
||||
<div className='left'>
|
||||
<a href='/homebrew' className='toolName'>
|
||||
The Home<small>Brewery</small>
|
||||
</a>
|
||||
</div>
|
||||
<div className='controls right'>
|
||||
{this.renderStatus()}
|
||||
{this.renderInfo()}
|
||||
{this.renderSourceButton()}
|
||||
{this.renderPrintButton()}
|
||||
{this.renderShare()}
|
||||
{this.renderNewButton()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Statusbar;
|
||||
110
client/homebrew/statusbar/statusbar.less
Normal file
110
client/homebrew/statusbar/statusbar.less
Normal file
@@ -0,0 +1,110 @@
|
||||
|
||||
.statusbar{
|
||||
position : fixed;
|
||||
z-index : 1000;
|
||||
height : 25px;
|
||||
width : 100%;
|
||||
background-color : black;
|
||||
font-size : 24px;
|
||||
color : white;
|
||||
line-height : 1.0em;
|
||||
border-bottom : 1px solid @grey;
|
||||
.logo{
|
||||
display : inline-block;
|
||||
vertical-align : middle;
|
||||
margin-top : -5px;
|
||||
margin-right : 20px;
|
||||
svg{
|
||||
margin-top : -6px;
|
||||
}
|
||||
}
|
||||
.left{
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
}
|
||||
.right{
|
||||
float : right;
|
||||
}
|
||||
.toolName{
|
||||
display : block;
|
||||
vertical-align : middle;
|
||||
font-family : CodeBold;
|
||||
font-size : 16px;
|
||||
color : white;
|
||||
line-height : 30px;
|
||||
text-decoration : none;
|
||||
small{
|
||||
font-family : CodeBold;
|
||||
}
|
||||
}
|
||||
.controls{
|
||||
font-size : 12px;
|
||||
>*{
|
||||
display : inline-block;
|
||||
height : 100%;
|
||||
padding : 0px 10px;
|
||||
border-left : 1px solid @grey;
|
||||
}
|
||||
.savingStatus{
|
||||
width : 56px;
|
||||
color : @grey;
|
||||
text-align : center;
|
||||
}
|
||||
.newButton{
|
||||
.animate(background-color);
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
background-color : fade(@green, 70%);
|
||||
}
|
||||
}
|
||||
.shareField{
|
||||
.animate(background-color);
|
||||
cursor : pointer;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
background-color : fade(@teal, 70%);
|
||||
}
|
||||
span{
|
||||
margin-right : 5px;
|
||||
}
|
||||
input{
|
||||
width : 100px;
|
||||
font-size : 12px;
|
||||
}
|
||||
}
|
||||
.printField{
|
||||
.animate(background-color);
|
||||
cursor : pointer;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
background-color : fade(@orange, 70%);
|
||||
}
|
||||
span{
|
||||
margin-right : 5px;
|
||||
}
|
||||
input{
|
||||
width : 100px;
|
||||
font-size : 12px;
|
||||
}
|
||||
}
|
||||
.sourceField{
|
||||
.animate(background-color);
|
||||
cursor : pointer;
|
||||
color : white;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
background-color : fade(@teal, 70%);
|
||||
}
|
||||
span{
|
||||
margin-right : 5px;
|
||||
}
|
||||
input{
|
||||
width : 100px;
|
||||
font-size : 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Router = require('pico-router');
|
||||
|
||||
var NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
var HomebrewIcon = require('naturalcrit/svg/homebrew.svg.jsx');
|
||||
|
||||
var Main = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
tools : [
|
||||
{
|
||||
id : 'homebrew',
|
||||
path : '/homebrew',
|
||||
name : 'The Homebrewery',
|
||||
icon : <HomebrewIcon />,
|
||||
desc : 'Make authentic-looking 5e homebrews using Markdown',
|
||||
|
||||
show : true,
|
||||
beta : false
|
||||
},
|
||||
{
|
||||
id : 'spellsort',
|
||||
path : '/spellsort',
|
||||
name : 'Spellsort',
|
||||
icon : <HomebrewIcon />,
|
||||
desc : 'Sort and search through spells',
|
||||
|
||||
show : true,
|
||||
beta : true
|
||||
},
|
||||
{
|
||||
id : 'homebrewfg2',
|
||||
path : '/homebrew',
|
||||
name : 'The Homebrewery',
|
||||
icon : <HomebrewIcon />,
|
||||
desc : 'Make authentic-looking 5e homebrews using Markdown',
|
||||
|
||||
show : false,
|
||||
beta : false
|
||||
}
|
||||
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
renderTool : function(tool){
|
||||
if(!tool.show) return null;
|
||||
|
||||
return <a href={tool.path} className={cx('tool', tool.id, {beta : tool.beta})} key={tool.id}>
|
||||
<div className='content'>
|
||||
{tool.icon}
|
||||
<h2>{tool.name}</h2>
|
||||
<p>{tool.desc}</p>
|
||||
</div>
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderTools : function(){
|
||||
return _.map(this.props.tools, (tool)=>{
|
||||
return this.renderTool(tool);
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='main'>
|
||||
<div className='top'>
|
||||
<div className='logo'>
|
||||
<NaturalCritIcon />
|
||||
<span className='name'>
|
||||
Natural
|
||||
<span className='crit'>Crit</span>
|
||||
</span>
|
||||
</div>
|
||||
<p>Top-tier tools for the discerning DM</p>
|
||||
</div>
|
||||
<div className='tools'>
|
||||
{this.renderTools()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Main;
|
||||
165
client/naturalCrit/combatManager/combatManager.jsx
Normal file
165
client/naturalCrit/combatManager/combatManager.jsx
Normal file
@@ -0,0 +1,165 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Sidebar = require('./sidebar/sidebar.jsx');
|
||||
var Encounter = require('./encounter/encounter.jsx');
|
||||
|
||||
var encounters = [
|
||||
{
|
||||
name : 'The Big Bad',
|
||||
desc : 'The big fight!',
|
||||
reward : 'gems',
|
||||
enemies : ['goblin', 'goblin'],
|
||||
reserve : ['goblin'],
|
||||
},
|
||||
{
|
||||
name : 'Demon Goats',
|
||||
desc : 'Gross fight',
|
||||
reward : 'curved horn, goat sac',
|
||||
enemies : ['demon_goat', 'demon_goat', 'demon_goat'],
|
||||
unique : {
|
||||
demon_goat : {
|
||||
"hp" : 140,
|
||||
"ac" : 16,
|
||||
"attr" : {
|
||||
"str" : 8,
|
||||
"con" : 8,
|
||||
"dex" : 8,
|
||||
"int" : 8,
|
||||
"wis" : 8,
|
||||
"cha" : 8
|
||||
},
|
||||
"attacks" : {
|
||||
"charge" : {
|
||||
"atk" : "1d20+5",
|
||||
"dmg" : "1d8+5",
|
||||
"type" : "bludge"
|
||||
}
|
||||
},
|
||||
"abilities" : ["charge"],
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
var defaultMonsterManual = require('naturalCrit/defaultMonsterManual.js');
|
||||
|
||||
var attrMod = function(attr){
|
||||
return Math.floor(attr/2) - 5;
|
||||
}
|
||||
|
||||
|
||||
var Store = require('naturalCrit/combat.store');
|
||||
var Actions = require('naturalCrit/combat.actions');
|
||||
|
||||
|
||||
|
||||
var CombatManager = React.createClass({
|
||||
mixins : [Store.mixin()],
|
||||
|
||||
|
||||
getInitialState: function() {
|
||||
var self = this;
|
||||
return {
|
||||
selectedEncounterIndex : 0,
|
||||
encounters : JSON.parse(localStorage.getItem('encounters')) || encounters,
|
||||
monsterManual : JSON.parse(localStorage.getItem('monsterManual')) || defaultMonsterManual,
|
||||
|
||||
players : localStorage.getItem('players') || 'jasper 13\nzatch 19',
|
||||
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
onStoreChange : function(){
|
||||
console.log('STORE CAHNGE', Store.getInc());
|
||||
this.setState({
|
||||
inc : Store.getInc()
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
handleEncounterJSONChange : function(encounterIndex, json){
|
||||
this.state.encounters[encounterIndex] = json;
|
||||
this.setState({
|
||||
encounters : this.state.encounters
|
||||
})
|
||||
localStorage.setItem("encounters", JSON.stringify(this.state.encounters));
|
||||
},
|
||||
handleMonsterManualJSONChange : function(json){
|
||||
this.setState({
|
||||
monsterManual : json
|
||||
});
|
||||
localStorage.setItem("monsterManual", JSON.stringify(this.state.monsterManual));
|
||||
},
|
||||
handlePlayerChange : function(e){
|
||||
this.setState({
|
||||
players : e.target.value
|
||||
});
|
||||
localStorage.setItem("players", e.target.value);
|
||||
},
|
||||
handleSelectedEncounterChange : function(encounterIndex){
|
||||
console.log(encounterIndex);
|
||||
this.setState({
|
||||
selectedEncounterIndex : encounterIndex
|
||||
});
|
||||
},
|
||||
handleRemoveEncounter : function(encounterIndex){
|
||||
this.state.encounters.splice(encounterIndex, 1);
|
||||
this.setState({
|
||||
encounters : this.state.encounters
|
||||
});
|
||||
localStorage.setItem("encounters", JSON.stringify(this.state.encounters));
|
||||
},
|
||||
|
||||
renderSelectedEncounter : function(){
|
||||
var self = this;
|
||||
|
||||
if(this.state.selectedEncounterIndex != null && this.state.encounters[this.state.selectedEncounterIndex]){
|
||||
var selectedEncounter = this.state.encounters[this.state.selectedEncounterIndex]
|
||||
return <Encounter
|
||||
key={selectedEncounter.name}
|
||||
{...selectedEncounter}
|
||||
monsterManual={this.state.monsterManual}
|
||||
players={this.state.players}
|
||||
/>
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
temp : function(){
|
||||
Actions.setInc(++this.state.inc);
|
||||
},
|
||||
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='combatManager'>
|
||||
<Sidebar
|
||||
selectedEncounter={this.state.selectedEncounterIndex}
|
||||
encounters={this.state.encounters}
|
||||
monsterManual={this.state.monsterManual}
|
||||
players={this.state.players}
|
||||
|
||||
onSelectEncounter={this.handleSelectedEncounterChange}
|
||||
onRemoveEncounter={this.handleRemoveEncounter}
|
||||
onJSONChange={this.handleEncounterJSONChange}
|
||||
onMonsterManualChange={this.handleMonsterManualJSONChange}
|
||||
onPlayerChange={this.handlePlayerChange}
|
||||
/>
|
||||
|
||||
|
||||
{this.renderSelectedEncounter()}
|
||||
|
||||
<button onClick={this.temp}>YUP {this.state.inc}</button>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = CombatManager;
|
||||
8
client/naturalCrit/combatManager/combatManager.less
Normal file
8
client/naturalCrit/combatManager/combatManager.less
Normal file
@@ -0,0 +1,8 @@
|
||||
.combatManager{
|
||||
|
||||
.encounterContainer{
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
}
|
||||
162
client/naturalCrit/combatManager/encounter/encounter.jsx
Normal file
162
client/naturalCrit/combatManager/encounter/encounter.jsx
Normal file
@@ -0,0 +1,162 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Store = require('naturalCrit/combat.store.js');
|
||||
|
||||
var MonsterCard = require('./monsterCard/monsterCard.jsx');
|
||||
|
||||
|
||||
|
||||
var attrMod = function(attr){
|
||||
return Math.floor(attr/2) - 5;
|
||||
}
|
||||
|
||||
var Encounter = React.createClass({
|
||||
mixins : [Store.mixin()],
|
||||
getInitialState: function() {
|
||||
return {
|
||||
enemies: this.createEnemies(this.props)
|
||||
};
|
||||
},
|
||||
|
||||
onStoreChange : function(){
|
||||
var players = Store.getplayersText();
|
||||
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
desc : '',
|
||||
reward : '',
|
||||
enemies : [],
|
||||
players : '',
|
||||
unique : {},
|
||||
|
||||
monsterManual : {}
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
this.setState({
|
||||
enemies : this.createEnemies(nextProps)
|
||||
})
|
||||
},
|
||||
|
||||
createEnemies : function(props){
|
||||
var self = this;
|
||||
return _.indexBy(_.map(props.enemies, function(type, index){
|
||||
return self.createEnemy(props, type, index)
|
||||
}), 'id')
|
||||
},
|
||||
|
||||
createEnemy : function(props, type, index){
|
||||
var stats = props.unique[type] || props.monsterManual[type];
|
||||
if(!stats) return;
|
||||
return _.extend({
|
||||
id : type + index,
|
||||
name : type,
|
||||
currentHP : stats.hp,
|
||||
initiative : _.random(1,20) + attrMod(stats.attr.dex)
|
||||
}, stats);
|
||||
},
|
||||
|
||||
|
||||
updateHP : function(enemyId, newHP){
|
||||
this.state.enemies[enemyId].currentHP = newHP;
|
||||
this.setState({
|
||||
enemies : this.state.enemies
|
||||
});
|
||||
},
|
||||
removeEnemy : function(enemyId){
|
||||
delete this.state.enemies[enemyId];
|
||||
this.setState({
|
||||
enemies : this.state.enemies
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
getPlayerObjects : function(){
|
||||
return _.reduce(this.props.players.split('\n'), function(r, line){
|
||||
var parts = line.split(' ');
|
||||
if(parts.length != 2) return r;
|
||||
r.push({
|
||||
name : parts[0],
|
||||
initiative : parts[1] * 1,
|
||||
isPC : true
|
||||
})
|
||||
return r;
|
||||
},[])
|
||||
},
|
||||
|
||||
|
||||
renderEnemies : function(){
|
||||
var self = this;
|
||||
|
||||
var sortedEnemies = _.sortBy(_.union(_.values(this.state.enemies), this.getPlayerObjects()), function(e){
|
||||
if(e && e.initiative) return -e.initiative;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return _.map(sortedEnemies, function(enemy){
|
||||
if(enemy.isPC){
|
||||
return <PlayerCard {...enemy} key={enemy.name} />
|
||||
}
|
||||
|
||||
return <MonsterCard
|
||||
{...enemy}
|
||||
key={enemy.id}
|
||||
updateHP={self.updateHP.bind(self, enemy.id)}
|
||||
remove={self.removeEnemy.bind(self, enemy.id)}
|
||||
/>
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
|
||||
var reward;
|
||||
if(this.props.reward){
|
||||
reward = <div className='reward'>
|
||||
<i className='fa fa-trophy' /> Rewards: {this.props.reward}
|
||||
</div>
|
||||
}
|
||||
|
||||
return(
|
||||
<div className='mainEncounter'>
|
||||
<div className='info'>
|
||||
<h1>{this.props.name}</h1>
|
||||
<p>{this.props.desc}</p>
|
||||
{reward}
|
||||
</div>
|
||||
|
||||
<div className='cardContainer'>
|
||||
{this.renderEnemies()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Encounter;
|
||||
|
||||
|
||||
var PlayerCard = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
initiative : 0
|
||||
};
|
||||
},
|
||||
render : function(){
|
||||
return <div className='playerCard'>
|
||||
<span className='name'>{_.startCase(this.props.name)}</span>
|
||||
<span className='initiative'><i className='fa fa-hourglass-2'/>{this.props.initiative}</span>
|
||||
</div>
|
||||
},
|
||||
|
||||
})
|
||||
36
client/naturalCrit/combatManager/encounter/encounter.less
Normal file
36
client/naturalCrit/combatManager/encounter/encounter.less
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
.mainEncounter{
|
||||
box-sizing : border-box;
|
||||
overflow : hidden;
|
||||
width : auto;
|
||||
|
||||
&>.info{
|
||||
|
||||
margin-left: 10px;
|
||||
padding-bottom : 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
h1{
|
||||
font-size: 2em;
|
||||
font-weight: 800;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
p{
|
||||
margin-left: 10px;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.5em;
|
||||
max-width: 600px;
|
||||
}
|
||||
.reward{
|
||||
font-size: 0.8em;
|
||||
font-weight: 800;
|
||||
margin-top: 5px;
|
||||
i{
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var RollDice = require('naturalCrit/rollDice');
|
||||
|
||||
var AttackSlot = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
uses : null
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
lastRoll: {},
|
||||
usedCount : 0
|
||||
};
|
||||
},
|
||||
|
||||
rollDice : function(key, notation){
|
||||
var res = RollDice(notation);
|
||||
this.state.lastRoll[key] = res
|
||||
this.state.lastRoll[key + 'key'] = _.uniqueId(key);
|
||||
this.setState({
|
||||
lastRoll : this.state.lastRoll
|
||||
})
|
||||
},
|
||||
|
||||
renderUses : function(){
|
||||
var self = this;
|
||||
if(!this.props.uses) return null;
|
||||
|
||||
return _.times(this.props.uses, function(index){
|
||||
var atCount = index < self.state.usedCount;
|
||||
return <i
|
||||
key={index}
|
||||
className={cx('fa', {'fa-circle-o' : !atCount, 'fa-circle' : atCount})}
|
||||
onClick={self.updateCount.bind(self, atCount)}
|
||||
/>
|
||||
})
|
||||
},
|
||||
updateCount : function(used){
|
||||
this.setState({
|
||||
usedCount : this.state.usedCount + (used ? -1 : 1)
|
||||
});
|
||||
},
|
||||
|
||||
renderNotes : function(){
|
||||
var notes = _.omit(this.props, ['name', 'atk', 'dmg', 'uses', 'heal']);
|
||||
return _.map(notes, function(text, key){
|
||||
return <div key={key}>{key + ': ' + text}</div>
|
||||
});
|
||||
},
|
||||
|
||||
renderRolls : function(){
|
||||
var self = this;
|
||||
|
||||
return _.map(['atk', 'dmg', 'heal'], function(type){
|
||||
if(!self.props[type]) return null;
|
||||
return <div className={cx('roll', type)} key={type}>
|
||||
|
||||
<button onClick={self.rollDice.bind(self, type, self.props[type])}>
|
||||
<i className={cx('fa', {
|
||||
'fa-hand-grab-o' : type=='dmg',
|
||||
'fa-bullseye' : type=='atk',
|
||||
'fa-plus' : type=='heal'
|
||||
})} />
|
||||
{self.props[type]}
|
||||
</button>
|
||||
<span key={self.state.lastRoll[type+'key']}>{self.state.lastRoll[type]}</span>
|
||||
</div>
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='attackSlot'>
|
||||
<div className='info'>
|
||||
<div className='name'>{this.props.name}</div>
|
||||
<div className='uses'>
|
||||
{this.renderUses()}
|
||||
</div>
|
||||
<div className='notes'>
|
||||
{this.renderNotes()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className='rolls'>
|
||||
{this.renderRolls()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AttackSlot;
|
||||
@@ -0,0 +1,71 @@
|
||||
|
||||
.attackSlot{
|
||||
//border : 1px solid black;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom : 5px;
|
||||
font-size : 0.8em;
|
||||
.info, .rolls{
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
}
|
||||
.info{
|
||||
width : 40%;
|
||||
.name{
|
||||
font-weight : 800;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.notes{
|
||||
font-size : 0.8em;
|
||||
}
|
||||
.uses{
|
||||
cursor : pointer;
|
||||
//font-size: 0.8em;
|
||||
//margin-top: 3px;
|
||||
}
|
||||
}
|
||||
.rolls{
|
||||
.roll{
|
||||
margin-bottom : 2px;
|
||||
&>span{
|
||||
font-weight: 800;
|
||||
.fadeInLeft();
|
||||
}
|
||||
button{
|
||||
width : 70px;
|
||||
margin-right : 5px;
|
||||
cursor : pointer;
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
text-align : left;
|
||||
border : none;
|
||||
outline : 0;
|
||||
i{
|
||||
width : 15px;
|
||||
margin-right : 5px;
|
||||
border-right : 1px solid white;
|
||||
}
|
||||
&:hover{
|
||||
//text-align: right;
|
||||
}
|
||||
}
|
||||
&.atk{
|
||||
button{
|
||||
background-color : fade(@blue, 40%);
|
||||
i { border-color: @blue}
|
||||
}
|
||||
}
|
||||
&.dmg{
|
||||
button{
|
||||
background-color : fade(@red, 40%);
|
||||
i { border-color: @red}
|
||||
}
|
||||
}
|
||||
&.heal{
|
||||
button{
|
||||
background-color : fade(@green, 40%);
|
||||
i { border-color: @green}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var AttackSlot = require('./attackSlot/attackSlot.jsx');
|
||||
|
||||
var MonsterCard = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
name : '',
|
||||
hp : 1,
|
||||
currentHP : 1,
|
||||
ac: 1,
|
||||
move : 30,
|
||||
attr : {
|
||||
str : 8,
|
||||
con : 8,
|
||||
dex : 8,
|
||||
int : 8,
|
||||
wis : 8,
|
||||
cha : 8
|
||||
},
|
||||
attacks : {},
|
||||
spells : {},
|
||||
abilities : [],
|
||||
items : [],
|
||||
|
||||
updateHP : function(){},
|
||||
remove : function(){},
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
status : 'normal',
|
||||
usedItems : [],
|
||||
lastRoll : { },
|
||||
mousePos : null,
|
||||
tempHP : 0
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener('mousemove', this.handleMouseDrag);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
},
|
||||
|
||||
handleMouseDown : function(e){
|
||||
this.setState({
|
||||
mousePos : {
|
||||
x : e.pageX,
|
||||
y : e.pageY,
|
||||
}
|
||||
});
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
},
|
||||
handleMouseUp : function(e){
|
||||
if(!this.state.mousePos) return;
|
||||
|
||||
|
||||
this.props.updateHP(this.props.currentHP + this.state.tempHP);
|
||||
this.setState({
|
||||
mousePos : null,
|
||||
tempHP : 0
|
||||
});
|
||||
},
|
||||
|
||||
handleMouseDrag : function(e){
|
||||
if (!this.state.mousePos) return;
|
||||
var distance = Math.sqrt(Math.pow(e.pageX - this.state.mousePos.x, 2) + Math.pow(e.pageY - this.state.mousePos.y, 2));
|
||||
var mult = (e.pageY > this.state.mousePos.y ? -1 : 1)
|
||||
|
||||
this.setState({
|
||||
tempHP : Math.floor(distance * mult/25)
|
||||
})
|
||||
},
|
||||
|
||||
addUsed : function(item, shouldRemove){
|
||||
if(!shouldRemove) this.state.usedItems.push(item);
|
||||
if(shouldRemove) this.state.usedItems.splice(this.state.usedItems.indexOf(item), 1);
|
||||
|
||||
this.setState({
|
||||
usedItems : this.state.usedItems
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
renderHPBox : function(){
|
||||
var self = this;
|
||||
|
||||
var tempHP
|
||||
if(this.state.tempHP){
|
||||
var sign = (this.state.tempHP > 0 ? '+' : '');
|
||||
tempHP = <span className='tempHP'>{['(',sign,this.state.tempHP,')'].join('')}</span>
|
||||
}
|
||||
|
||||
return <div className='hpBox' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
|
||||
<div className='currentHP'>
|
||||
{tempHP} {this.props.currentHP}
|
||||
</div>
|
||||
{self.renderStats()}
|
||||
</div>
|
||||
},
|
||||
|
||||
renderStats : function(){
|
||||
var stats = {
|
||||
'fa fa-shield' : this.props.ac,
|
||||
//'fa fa-hourglass-2' : this.props.initiative,
|
||||
}
|
||||
return _.map(stats, function(val, icon){
|
||||
return <div className='stat' key={icon}> {val} <i className={icon} /></div>
|
||||
})
|
||||
},
|
||||
|
||||
renderAttacks : function(){
|
||||
var self = this;
|
||||
return _.map(this.props.attacks, function(attack, name){
|
||||
return <AttackSlot key={name} name={name} {...attack} />
|
||||
})
|
||||
},
|
||||
|
||||
renderSpells : function(){
|
||||
var self = this;
|
||||
return _.map(this.props.spells, function(spell, name){
|
||||
return <AttackSlot key={name} name={name} {...spell} />
|
||||
})
|
||||
},
|
||||
|
||||
renderAbilities : function(){
|
||||
return _.map(this.props.abilities, function(text, name){
|
||||
return <div className='ability' key={name}>
|
||||
<span className='name'>{name}</span>: {text}
|
||||
</div>
|
||||
});
|
||||
},
|
||||
|
||||
renderItems : function(){
|
||||
var self = this;
|
||||
var usedItems = this.state.usedItems.slice(0);
|
||||
return _.map(this.props.items, function(item, index){
|
||||
var used = _.contains(usedItems, item);
|
||||
if(used){
|
||||
usedItems.splice(usedItems.indexOf(item), 1);
|
||||
}
|
||||
return <span
|
||||
key={index}
|
||||
className={cx({'used' : used})}
|
||||
onClick={self.addUsed.bind(self, item, used)}>
|
||||
{item}
|
||||
</span>
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
var condition = ''
|
||||
if(this.props.currentHP + this.state.tempHP > this.props.hp) condition='overhealed';
|
||||
if(this.props.currentHP + this.state.tempHP <= this.props.hp * 0.5) condition='hurt';
|
||||
if(this.props.currentHP + this.state.tempHP <= this.props.hp * 0.2) condition='last_legs';
|
||||
if(this.props.currentHP + this.state.tempHP <= 0) condition='dead';
|
||||
|
||||
|
||||
return(
|
||||
<div className={cx('monsterCard', condition)}>
|
||||
<div className='healthbar' style={{width : (this.props.currentHP + this.state.tempHP)/this.props.hp*100 + '%'}} />
|
||||
<div className='overhealbar' style={{width : (this.props.currentHP + this.state.tempHP - this.props.hp)/this.props.hp*100 + '%'}} />
|
||||
|
||||
|
||||
{this.renderHPBox()}
|
||||
<div className='info'>
|
||||
<span className='name'>{this.props.name}</span>
|
||||
</div>
|
||||
|
||||
<div className='attackContainer'>
|
||||
{this.renderAttacks()}
|
||||
</div>
|
||||
<div className='spellContainer'>
|
||||
{this.renderSpells()}
|
||||
</div>
|
||||
|
||||
<div className='abilitiesContainer'>
|
||||
{this.renderAbilities()}
|
||||
</div>
|
||||
<div className='itemContainer'>
|
||||
<i className='fa fa-flask' />
|
||||
{this.renderItems()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MonsterCard;
|
||||
|
||||
/*
|
||||
|
||||
|
||||
{this.props.initiative}
|
||||
|
||||
<i className='fa fa-times' onClick={this.props.remove} />
|
||||
*/
|
||||
@@ -0,0 +1,129 @@
|
||||
|
||||
@marginSize : 10px;
|
||||
.playerCard{
|
||||
display : inline-block;
|
||||
box-sizing : border-box;
|
||||
margin : @marginSize;
|
||||
padding : 10px;
|
||||
background-color : white;
|
||||
border : 1px solid #bbb;
|
||||
.name{
|
||||
margin-right : 20px;
|
||||
}
|
||||
.initiative{
|
||||
font-size : 0.8em;
|
||||
i{
|
||||
font-size : 0.8em;
|
||||
}
|
||||
}
|
||||
&:nth-child(5n + 1){ background-color: fade(@blue, 25%); }
|
||||
&:nth-child(5n + 2){ background-color: fade(@purple, 25%); }
|
||||
&:nth-child(5n + 3){ background-color: fade(@steel, 25%); }
|
||||
&:nth-child(5n + 4){ background-color: fade(@green, 25%); }
|
||||
&:nth-child(5n + 5){ background-color: fade(@orange, 25%); }
|
||||
}
|
||||
.monsterCard{
|
||||
position : relative;
|
||||
display : inline-block;
|
||||
vertical-align : top;
|
||||
box-sizing : border-box;
|
||||
width : 220px;
|
||||
margin : @marginSize;
|
||||
padding : 10px;
|
||||
background-color : white;
|
||||
border : 1px solid #bbb;
|
||||
.healthbar{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
left : 0px;
|
||||
z-index : 50;
|
||||
height : 3px;
|
||||
max-width : 100%;
|
||||
background-color : @green;
|
||||
}
|
||||
.overhealbar{
|
||||
position : absolute;
|
||||
top : 0px;
|
||||
left : 0px;
|
||||
z-index : 100;
|
||||
height : 3px;
|
||||
max-width : 100%;
|
||||
background-color : @blueLight;
|
||||
}
|
||||
&.hurt{
|
||||
.healthbar{
|
||||
background-color : orange;
|
||||
}
|
||||
}
|
||||
&.last_legs{
|
||||
background-color : lighten(@red, 49%);
|
||||
.healthbar{
|
||||
background-color : red;
|
||||
}
|
||||
}
|
||||
&.dead{
|
||||
opacity : 0.3;
|
||||
}
|
||||
&>.info{
|
||||
margin-bottom : 10px;
|
||||
.name{
|
||||
margin-right : 10px;
|
||||
font-size : 1.5em;
|
||||
}
|
||||
.stat{
|
||||
margin-right : 5px;
|
||||
font-size : 0.7em;
|
||||
i{
|
||||
font-size : 0.7em;
|
||||
}
|
||||
}
|
||||
}
|
||||
.hpBox{
|
||||
.noselect();
|
||||
position : absolute;
|
||||
top : 5px;
|
||||
right : 5px;
|
||||
cursor : pointer;
|
||||
text-align : right;
|
||||
.currentHP{
|
||||
font-size : 2em;
|
||||
font-weight : 800;
|
||||
line-height : 0.8em;
|
||||
.tempHP{
|
||||
vertical-align : top;
|
||||
font-size : 0.4em;
|
||||
line-height : 0.8em;
|
||||
}
|
||||
}
|
||||
.stat{
|
||||
font-size : 0.8em;
|
||||
}
|
||||
.hpText{
|
||||
font-size : 0.6em;
|
||||
font-weight : 800;
|
||||
}
|
||||
}
|
||||
.abilitiesContainer{
|
||||
margin-top : 10px;
|
||||
.ability{
|
||||
font-size: 0.7em;
|
||||
.name{
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
}
|
||||
.itemContainer{
|
||||
margin-top : 10px;
|
||||
i{
|
||||
font-size : 0.7em;
|
||||
}
|
||||
span{
|
||||
margin-right : 5px;
|
||||
cursor : pointer;
|
||||
font-size : 0.7em;
|
||||
&.used{
|
||||
text-decoration : line-through;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
client/naturalCrit/combatManager/sidebar/dmDice/dmDice.jsx
Normal file
59
client/naturalCrit/combatManager/sidebar/dmDice/dmDice.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var RollDice = require('naturalCrit/rollDice');
|
||||
|
||||
var DmDice = React.createClass({
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
lastRoll:{ },
|
||||
diceNotation : {
|
||||
a : "1d20",
|
||||
b : "6d6 + 3",
|
||||
c : "1d20 - 1"
|
||||
}
|
||||
|
||||
};
|
||||
},
|
||||
|
||||
roll : function(id){
|
||||
this.state.lastRoll[id] = RollDice(this.state.diceNotation[id]);
|
||||
this.setState({
|
||||
lastRoll : this.state.lastRoll
|
||||
});
|
||||
},
|
||||
handleChange : function(id, e){
|
||||
this.state.diceNotation[id] = e.target.value;
|
||||
this.setState({
|
||||
diceNotation : this.state.diceNotation
|
||||
});
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
renderRolls : function(){
|
||||
var self = this;
|
||||
return _.map(['a', 'b', 'c'], function(id){
|
||||
return <div className='roll' key={id} onClick={self.roll.bind(self, id)}>
|
||||
<input type="text" value={self.state.diceNotation[id]} onChange={self.handleChange.bind(self, id)} />
|
||||
<i className='fa fa-random' />
|
||||
<span key={self.state.lastRoll[id]}>{self.state.lastRoll[id]}</span>
|
||||
</div>
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='dmDice'>
|
||||
<h3> <i className='fa fa-random' /> DM Dice </h3>
|
||||
{this.renderRolls()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = DmDice;
|
||||
32
client/naturalCrit/combatManager/sidebar/dmDice/dmDice.less
Normal file
32
client/naturalCrit/combatManager/sidebar/dmDice/dmDice.less
Normal file
@@ -0,0 +1,32 @@
|
||||
.dmDice{
|
||||
|
||||
h3{
|
||||
color : white;
|
||||
background-color: @teal;
|
||||
}
|
||||
|
||||
.roll{
|
||||
cursor: pointer;
|
||||
.noselect();
|
||||
input[type="text"]{
|
||||
margin-left: 10px;
|
||||
margin-bottom: 6px;
|
||||
margin-top: 6px;
|
||||
width : 60px;
|
||||
font-family: monospace;
|
||||
padding : 5px;
|
||||
}
|
||||
i.fa-random{
|
||||
font-size: 0.8em;
|
||||
margin: 0 10px;
|
||||
}
|
||||
span{
|
||||
font-weight: 800;
|
||||
.fadeInLeft();
|
||||
}
|
||||
|
||||
&:hover{
|
||||
background-color: fade(@teal, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var JSONFileEditor = require('naturalCrit/jsonFileEditor/jsonFileEditor.jsx');
|
||||
|
||||
//var GetRandomEncounter = require('naturalCrit/randomEncounter.js');
|
||||
|
||||
var Store = require('naturalCrit/combat.store.js');
|
||||
var Actions = require('naturalCrit/combat.actions.js');
|
||||
|
||||
|
||||
var Encounters = React.createClass({
|
||||
mixins : [Store.mixin()],
|
||||
onStoreChange : function(){
|
||||
this.setState({
|
||||
encounters : Store.getEncounters(),
|
||||
selectedEncounter : Store.getSelectedEncounterIndex()
|
||||
});
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
encounters : Store.getEncounters(),
|
||||
selectedEncounter : Store.getSelectedEncounterIndex()
|
||||
};
|
||||
},
|
||||
/*
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
encounters : [],
|
||||
selectedEncounter : 0,
|
||||
|
||||
onJSONChange : function(encounterIndex, json){},
|
||||
onSelectEncounter : function(encounterIndex){},
|
||||
onRemoveEncounter : function(encounterIndex){}
|
||||
};
|
||||
},
|
||||
*/
|
||||
handleJSONChange : function(encounterIndex, json){
|
||||
//this.props.onJSONChange(encounterIndex, json);
|
||||
Actions.updateEncounter(encounterIndex, json);
|
||||
},
|
||||
handleSelectEncounter : function(encounterIndex){
|
||||
//this.props.onSelectEncounter(encounterIndex);
|
||||
Actions.selectEncounter(encounterIndex);
|
||||
},
|
||||
handleRemoveEncounter : function(encounterIndex){
|
||||
//this.props.onRemoveEncounter(encounterIndex);
|
||||
Actions.removeEncounter(encounterIndex);
|
||||
},
|
||||
addRandomEncounter : function(){
|
||||
Actions.addEncounter();
|
||||
},
|
||||
|
||||
|
||||
renderEncounters : function(){
|
||||
var self = this;
|
||||
return _.map(this.state.encounters, function(encounter, index){
|
||||
|
||||
var isSelected = self.state.selectedEncounter == index;
|
||||
return <div className={cx('encounter' , {'selected' : isSelected})} key={index}>
|
||||
|
||||
<i onClick={self.handleSelectEncounter.bind(self, index)} className={cx('select', 'fa', {
|
||||
'fa-square-o' : !isSelected,
|
||||
'fa-check-square-o' : isSelected,
|
||||
})} />
|
||||
|
||||
|
||||
<JSONFileEditor
|
||||
name={encounter.name}
|
||||
json={encounter}
|
||||
onJSONChange={self.handleJSONChange.bind(self, index)}
|
||||
/>
|
||||
|
||||
<i onClick={self.handleRemoveEncounter.bind(self, index)} className='remove fa fa-times' />
|
||||
</div>
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='encounters'>
|
||||
<h3>
|
||||
<i className='fa fa-flag' /> Encounters
|
||||
<button className='addEncounter'>
|
||||
<i className='fa fa-plus' onClick={this.addRandomEncounter}/>
|
||||
</button>
|
||||
</h3>
|
||||
{this.renderEncounters()}
|
||||
|
||||
<div className='controls'>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Encounters;
|
||||
@@ -0,0 +1,53 @@
|
||||
|
||||
.encounters{
|
||||
margin-bottom : 20px;
|
||||
h3{
|
||||
background-color : @red;
|
||||
color : white;
|
||||
button{
|
||||
.animate(color);
|
||||
float : right;
|
||||
cursor : pointer;
|
||||
background-color : transparent;
|
||||
border : none;
|
||||
outline : none;
|
||||
&:hover{
|
||||
color : white;
|
||||
}
|
||||
}
|
||||
}
|
||||
.encounter{
|
||||
position : relative;
|
||||
padding-left : 15px;
|
||||
border-left : 0px solid @teal;
|
||||
.animateAll();
|
||||
&:hover{
|
||||
i.remove{
|
||||
opacity : 1;
|
||||
}
|
||||
}
|
||||
i.remove{
|
||||
.animate(opacity);
|
||||
position : absolute;
|
||||
top : 3px;
|
||||
right : 3px;
|
||||
cursor : pointer;
|
||||
opacity : 0;
|
||||
font-size : 0.6em;
|
||||
color : #333;
|
||||
&:hover{
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
i.select{
|
||||
cursor : pointer;
|
||||
}
|
||||
.jsonFileEditor{
|
||||
display : inline-block;
|
||||
}
|
||||
&.selected{
|
||||
//background-color : fade(@green, 30%);
|
||||
border-left : 8px solid @teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
76
client/naturalCrit/combatManager/sidebar/sidebar.jsx
Normal file
76
client/naturalCrit/combatManager/sidebar/sidebar.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var JSONFileEditor = require('naturalCrit/jsonFileEditor/jsonFileEditor.jsx');
|
||||
|
||||
var DMDice = require('./dmDice/dmDice.jsx');
|
||||
var Encounters = require('./encounters/encounters.jsx');
|
||||
|
||||
var Store = require('naturalCrit/combat.store.js');
|
||||
var Actions = require('naturalCrit/combat.actions.js');
|
||||
|
||||
|
||||
var Sidebar = React.createClass({
|
||||
mixins : [Store.mixin()],
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
hide : false,
|
||||
|
||||
monsterManual : Store.getMonsterManual(),
|
||||
players : Store.getPlayersText()
|
||||
};
|
||||
},
|
||||
|
||||
onStoreChange : function(){
|
||||
this.setState({
|
||||
players : Store.getPlayersText(),
|
||||
monsterManual : Store.getMonsterManual()
|
||||
})
|
||||
},
|
||||
|
||||
handleLogoClick : function(){
|
||||
this.setState({
|
||||
hide : !this.state.hide
|
||||
})
|
||||
},
|
||||
handleMonsterManualChange : function(json){
|
||||
Actions.updateMonsterManual(json);
|
||||
},
|
||||
handlePlayerChange : function(e){
|
||||
Actions.updatePlayers(e.target.value);
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className={cx('sidebar', {'hide' : this.state.hide})}>
|
||||
<div className='logo'>
|
||||
<svg onClick={this.handleLogoClick} version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100"><path d="M80.644,87.982l16.592-41.483c0.054-0.128,0.088-0.26,0.108-0.394c0.006-0.039,0.007-0.077,0.011-0.116 c0.007-0.087,0.008-0.174,0.002-0.26c-0.003-0.046-0.007-0.091-0.014-0.137c-0.014-0.089-0.036-0.176-0.063-0.262 c-0.012-0.034-0.019-0.069-0.031-0.103c-0.047-0.118-0.106-0.229-0.178-0.335c-0.004-0.006-0.006-0.012-0.01-0.018L67.999,3.358 c-0.01-0.013-0.003-0.026-0.013-0.04L68,3.315V4c0,0-0.033,0-0.037,0c-0.403-1-1.094-1.124-1.752-0.976 c0,0.004-0.004-0.012-0.007-0.012C66.201,3.016,66.194,3,66.194,3H66.19h-0.003h-0.003h-0.004h-0.003c0,0-0.004,0-0.007,0 s-0.003-0.151-0.007-0.151L20.495,15.227c-0.025,0.007-0.046-0.019-0.071-0.011c-0.087,0.028-0.172,0.041-0.253,0.083 c-0.054,0.027-0.102,0.053-0.152,0.085c-0.051,0.033-0.101,0.061-0.147,0.099c-0.044,0.036-0.084,0.073-0.124,0.113 c-0.048,0.048-0.093,0.098-0.136,0.152c-0.03,0.039-0.059,0.076-0.085,0.117c-0.046,0.07-0.084,0.145-0.12,0.223 c-0.011,0.023-0.027,0.042-0.036,0.066L2.911,57.664C2.891,57.715,3,57.768,3,57.82v0.002c0,0.186,0,0.375,0,0.562 c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004v0.002c0,0.074-0.002,0.15,0.012,0.223 C3.015,58.631,3,58.631,3,58.633c0,0.004,0,0.004,0,0.008c0,0,0,0,0,0.002c0,0,0,0,0,0.004v0.004c0,0,0,0,0,0.002v0.004 c0,0.191-0.046,0.377,0.06,0.545c0-0.002-0.03,0.004-0.03,0.004c0,0.004-0.03,0.004-0.03,0.004c0,0.002,0,0.002,0,0.002 l-0.045,0.004c0.03,0.047,0.036,0.09,0.068,0.133l29.049,37.359c0.002,0.004,0,0.006,0.002,0.01c0.002,0.002,0,0.004,0.002,0.008 c0.006,0.008,0.014,0.014,0.021,0.021c0.024,0.029,0.052,0.051,0.078,0.078c0.027,0.029,0.053,0.057,0.082,0.082 c0.03,0.027,0.055,0.062,0.086,0.088c0.026,0.02,0.057,0.033,0.084,0.053c0.04,0.027,0.081,0.053,0.123,0.076 c0.005,0.004,0.01,0.008,0.016,0.01c0.087,0.051,0.176,0.09,0.269,0.123c0.042,0.014,0.082,0.031,0.125,0.043 c0.021,0.006,0.041,0.018,0.062,0.021c0.123,0.027,0.249,0.043,0.375,0.043c0.099,0,0.202-0.012,0.304-0.027l45.669-8.303 c0.057-0.01,0.108-0.021,0.163-0.037C79.547,88.992,79.562,89,79.575,89c0.004,0,0.004,0,0.004,0c0.021,0,0.039-0.027,0.06-0.035 c0.041-0.014,0.08-0.034,0.12-0.052c0.021-0.01,0.044-0.019,0.064-0.03c0.017-0.01,0.026-0.015,0.033-0.017 c0.014-0.008,0.023-0.021,0.037-0.028c0.14-0.078,0.269-0.174,0.38-0.285c0.014-0.016,0.024-0.034,0.038-0.048 c0.109-0.119,0.201-0.252,0.271-0.398c0.006-0.01,0.016-0.018,0.021-0.029c0.004-0.008,0.008-0.017,0.011-0.026 c0.002-0.004,0.003-0.006,0.005-0.01C80.627,88.021,80.635,88.002,80.644,87.982z M77.611,84.461L48.805,66.453l32.407-25.202 L77.611,84.461z M46.817,63.709L35.863,23.542l43.818,14.608L46.817,63.709z M84.668,40.542l8.926,5.952l-11.902,29.75 L84.668,40.542z M89.128,39.446L84.53,36.38l-6.129-12.257L89.128,39.446z M79.876,34.645L37.807,20.622L65.854,6.599L79.876,34.645 z M33.268,19.107l-6.485-2.162l23.781-6.487L33.268,19.107z M21.92,18.895l8.67,2.891L10.357,47.798L21.92,18.895z M32.652,24.649 l10.845,39.757L7.351,57.178L32.652,24.649z M43.472,67.857L32.969,92.363L8.462,60.855L43.472,67.857z M46.631,69.09l27.826,17.393 l-38.263,6.959L46.631,69.09z"></path></svg>
|
||||
<span className='name'>
|
||||
<div>Natural<span className='crit'>Crit</span></div>
|
||||
<small>Combat Manager</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className='contents'>
|
||||
<div className='monsterManualContainer'>
|
||||
<JSONFileEditor
|
||||
name="Monster Manual"
|
||||
json={this.state.monsterManual}
|
||||
onJSONChange={this.handleMonsterManualChange}
|
||||
/>
|
||||
</div>
|
||||
<Encounters />
|
||||
<div className='addPlayers'>
|
||||
<h3> <i className='fa fa-group' /> Players </h3>
|
||||
<textarea value={this.state.players} onChange={this.handlePlayerChange} />
|
||||
</div>
|
||||
<DMDice />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Sidebar;
|
||||
90
client/naturalCrit/combatManager/sidebar/sidebar.less
Normal file
90
client/naturalCrit/combatManager/sidebar/sidebar.less
Normal file
@@ -0,0 +1,90 @@
|
||||
|
||||
@font-face {
|
||||
font-family : CodeLight;
|
||||
src : url('/assets/naturalCrit/sidebar/CODE Light.otf');
|
||||
}
|
||||
@font-face {
|
||||
font-family : CodeBold;
|
||||
src : url('/assets/naturalCrit/sidebar/CODE Bold.otf');
|
||||
}
|
||||
.sidebar{
|
||||
.animateAll();
|
||||
float : left;
|
||||
box-sizing : border-box;
|
||||
height : 100%;
|
||||
width : @sidebarWidth;
|
||||
padding-bottom : 20px;
|
||||
background-color : white;
|
||||
//border : 1px solid @steel;
|
||||
&.hide{
|
||||
height : 50px;
|
||||
width : 50px;
|
||||
.logo .name{
|
||||
left : -200px;
|
||||
opacity : 0;
|
||||
}
|
||||
.contents{
|
||||
height : 0px;
|
||||
opacity : 0;
|
||||
}
|
||||
}
|
||||
.logo{
|
||||
padding : 10px 10px;
|
||||
background-color : @steel;
|
||||
font-family : 'CodeLight', sans-serif;
|
||||
font-size : 1.8em;
|
||||
color : white;
|
||||
svg{
|
||||
vertical-align : middle;
|
||||
height : 1em;
|
||||
margin-right : 0.2em;
|
||||
cursor : pointer;
|
||||
fill : white;
|
||||
}
|
||||
|
||||
span.name{
|
||||
.animateAll();
|
||||
position : absolute;
|
||||
top : 15px;
|
||||
left : 50px;
|
||||
opacity : 1;
|
||||
font-size: 0.9em;
|
||||
line-height: 0.5em;
|
||||
span.crit{
|
||||
font-family : 'CodeBold';
|
||||
}
|
||||
small{
|
||||
font-size: 0.3em;
|
||||
font-family : 'Open Sans';
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
.contents{
|
||||
.animate(opacity);
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
&>*{
|
||||
width : 100%;
|
||||
}
|
||||
h3{
|
||||
padding : 10px;
|
||||
font-size : 0.8em;
|
||||
font-weight : 800;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
.addPlayers{
|
||||
h3{
|
||||
color : white;
|
||||
background-color: @purple;
|
||||
}
|
||||
textarea{
|
||||
height : 80px;
|
||||
width : 100px;
|
||||
margin : 10px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
BIN
client/naturalCrit/home/bulldozer.png
Normal file
BIN
client/naturalCrit/home/bulldozer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
52
client/naturalCrit/home/home.jsx
Normal file
52
client/naturalCrit/home/home.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Router = require('pico-router');
|
||||
var Icon = require('naturalCrit/icon.svg.jsx');
|
||||
var Logo = require('naturalCrit/logo/logo.jsx');
|
||||
|
||||
|
||||
var HomebrewIcon = require('naturalCrit/homebrewIcon.svg.jsx');
|
||||
var CombatIcon = require('naturalCrit/combatIcon.svg.jsx');
|
||||
|
||||
var Home = React.createClass({
|
||||
|
||||
navigate : function(){
|
||||
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='home'>
|
||||
|
||||
|
||||
<div className='top'>
|
||||
<Logo />
|
||||
<p>Top-tier tools for the discerning DM</p>
|
||||
</div>
|
||||
|
||||
<div className='tools'>
|
||||
|
||||
<div className='homebrew toolContainer' onClick={Router.navigate.bind(self, '/homebrew')}>
|
||||
<div className='content'>
|
||||
<HomebrewIcon />
|
||||
<h2>The Homebrewery</h2>
|
||||
<p>Make authentic-looking 5e homebrews using Markdown</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='combat toolContainer underConstruction' onClick={Router.navigate.bind(self, '/combat')}>
|
||||
<div className='content'>
|
||||
<CombatIcon />
|
||||
<h2>Combat Manager</h2>
|
||||
<p>Easily create and manage complex encouters for your party</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Home;
|
||||
@@ -1,5 +1,5 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.main{
|
||||
|
||||
.home{
|
||||
height : 100vh;
|
||||
background-color : white;
|
||||
.top{
|
||||
@@ -12,16 +12,7 @@
|
||||
font-size : 4em;
|
||||
color : black;
|
||||
svg{
|
||||
height : .9em;
|
||||
margin-right : .2em;
|
||||
cursor : pointer;
|
||||
fill : black;
|
||||
}
|
||||
.name{
|
||||
font-family : 'CodeLight';
|
||||
.crit{
|
||||
font-family : 'CodeBold';
|
||||
}
|
||||
fill : black;
|
||||
}
|
||||
}
|
||||
p{
|
||||
@@ -34,25 +25,24 @@
|
||||
.tools{
|
||||
width : 100%;
|
||||
text-align : center;
|
||||
.tool{
|
||||
.toolContainer{
|
||||
.sequentialDelay(0.5s, 1s);
|
||||
.fadeInDown(1s);
|
||||
.keep();
|
||||
display : inline-block;
|
||||
cursor : pointer;
|
||||
opacity : 0;
|
||||
color : black;
|
||||
text-align : center;
|
||||
text-decoration : none;
|
||||
&+.tool{
|
||||
border-left : 1px solid #666;
|
||||
display : inline-block;
|
||||
cursor : pointer;
|
||||
opacity : 0;
|
||||
text-align : center;
|
||||
border-right : 1px solid #333;
|
||||
&:last-child{
|
||||
border : none;
|
||||
}
|
||||
.content{
|
||||
.addSketch(360px);
|
||||
.animateAll(0.5s);
|
||||
position : relative;
|
||||
width : 320px;
|
||||
padding : 35px;
|
||||
width : 500px;
|
||||
padding : 40px;
|
||||
&:hover{
|
||||
svg, h2{
|
||||
.transform(scale(1.3));
|
||||
@@ -73,11 +63,19 @@
|
||||
height : 10em;
|
||||
}
|
||||
}
|
||||
.content:hover{
|
||||
background-color : fade(@teal, 20%);
|
||||
//Proejct specific styles
|
||||
&.homebrew{
|
||||
.content:hover{
|
||||
background-color : fade(@teal, 20%);
|
||||
}
|
||||
}
|
||||
//Beta styles
|
||||
&.beta{
|
||||
&.combat{
|
||||
.content:hover{
|
||||
background-color : fade(@red, 20%);
|
||||
}
|
||||
}
|
||||
//Under Construction styles
|
||||
&.underConstruction{
|
||||
cursor : initial;
|
||||
.content{
|
||||
&:hover{
|
||||
@@ -90,7 +88,7 @@
|
||||
}
|
||||
&:after{
|
||||
.animateAll();
|
||||
content : "beta!";
|
||||
content : "Under Construction";
|
||||
position : absolute;
|
||||
display : block;
|
||||
top : 120px;
|
||||
@@ -104,6 +102,21 @@
|
||||
text-align : center;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
&:before{
|
||||
.rumble(6s);
|
||||
content : "";
|
||||
position : absolute;
|
||||
display : block;
|
||||
top : 130px;
|
||||
right : 30px;
|
||||
height : 50px;
|
||||
width : 40px;
|
||||
//opacity : 0;
|
||||
background-image : url('/assets/naturalCrit/home/bulldozer.png');
|
||||
background-repeat : no-repeat;
|
||||
background-size : contain;
|
||||
animation-iteration-count : infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,4 +146,21 @@
|
||||
@-ms-keyframes sketch {.sketchKeyFrames();}
|
||||
@-o-keyframes sketch {.sketchKeyFrames();}
|
||||
@keyframes sketch {.sketchKeyFrames();}
|
||||
}
|
||||
}
|
||||
/*
|
||||
.sketch(@length, @color : black, @duration : 3s, @easing : @defaultEasing){
|
||||
.createAnimation(bounce, @duration, @easing);
|
||||
.sketchKeyFrames(){
|
||||
0% { stroke-dashoffset : 0px; fill:@color;}
|
||||
15% { stroke-dashoffset : 0px; fill : transparent}
|
||||
50% { stroke-dashoffset : @length; fill: transparent}
|
||||
85% { stroke-dashoffset : 0px; fill:transparent;}
|
||||
100% { stroke-dashoffset : 0px; fill:@color;}
|
||||
}
|
||||
@-webkit-keyframes bounce {.sketchKeyFrames();}
|
||||
@-moz-keyframes bounce {.sketchKeyFrames();}
|
||||
@-ms-keyframes bounce {.sketchKeyFrames();}
|
||||
@-o-keyframes bounce {.sketchKeyFrames();}
|
||||
@keyframes bounce {.sketchKeyFrames();}
|
||||
}
|
||||
*/
|
||||
0
client/naturalCrit/logo.svg.jsx
Normal file
0
client/naturalCrit/logo.svg.jsx
Normal file
36
client/naturalCrit/naturalCrit.jsx
Normal file
36
client/naturalCrit/naturalCrit.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
var CreateRouter = require('pico-router').createRouter;
|
||||
|
||||
var Home = require('./home/home.jsx');
|
||||
var CombatManager = require('./combatManager/combatManager.jsx');
|
||||
//var Homebrew = require('./homebrew/homebrew.jsx');
|
||||
|
||||
|
||||
var Router = CreateRouter({
|
||||
'/' : <Home />,
|
||||
'/combat' : <CombatManager />,
|
||||
//'/homebrew' : <Homebrew />,
|
||||
});
|
||||
|
||||
|
||||
var NaturalCrit = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
url : '/'
|
||||
};
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='naturalCrit'>
|
||||
<Router initialUrl={this.props.url} />
|
||||
</div>
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = NaturalCrit;
|
||||
|
||||
|
||||
|
||||
|
||||
39
client/naturalCrit/naturalCrit.less
Normal file
39
client/naturalCrit/naturalCrit.less
Normal file
@@ -0,0 +1,39 @@
|
||||
@import 'naturalCrit/styles/reset.less';
|
||||
//@import 'naturalCrit/styles/elements.less';
|
||||
@import 'naturalCrit/styles/animations.less';
|
||||
@import 'naturalCrit/styles/colors.less';
|
||||
@import 'naturalCrit/styles/tooltip.less';
|
||||
|
||||
@import 'font-awesome/css/font-awesome.css';
|
||||
|
||||
html,body, #reactContainer, .naturalCrit{
|
||||
min-height : 100%;
|
||||
}
|
||||
|
||||
@sidebarWidth : 250px;
|
||||
|
||||
body{
|
||||
background-color : #eee;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
color : #4b5055;
|
||||
font-weight : 100;
|
||||
text-rendering : optimizeLegibility;
|
||||
margin : 0;
|
||||
padding : 0;
|
||||
height : 100%;
|
||||
}
|
||||
|
||||
.naturalCrit{
|
||||
color : #333;
|
||||
background-color: #eee;
|
||||
|
||||
}
|
||||
|
||||
.noselect(){
|
||||
-webkit-touch-callout : none;
|
||||
-webkit-user-select : none;
|
||||
-khtml-user-select : none;
|
||||
-moz-user-select : none;
|
||||
-ms-user-select : none;
|
||||
user-select : none;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
var Navbar = React.createClass({
|
||||
render : function(){
|
||||
return <Nav.base>
|
||||
<Nav.section>
|
||||
<Nav.logo />
|
||||
<Nav.item href='/spellsort' className='spellsortLogo'>
|
||||
<div>Spellsort</div>
|
||||
</Nav.item>
|
||||
<Nav.item>v0.0.0</Nav.item>
|
||||
</Nav.section>
|
||||
{this.props.children}
|
||||
</Nav.base>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Navbar;
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
.spellsort nav{
|
||||
.spellsortLogo{
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div{
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover{
|
||||
color : @teal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Sorter = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
spells : []
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
renderSpell : function(spell){
|
||||
return <div className='spell' key={spell.id}>
|
||||
{spell.name}
|
||||
</div>
|
||||
},
|
||||
|
||||
renderSpells : function(){
|
||||
return _.map(this.props.spells, (spell)=>{
|
||||
return this.renderSpell(spell)
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='sorter'>
|
||||
{this.renderSpells()}
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Sorter;
|
||||
@@ -1,3 +0,0 @@
|
||||
.COM{
|
||||
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Markdown = require('marked');
|
||||
|
||||
var SpellRenderer = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
spells : []
|
||||
};
|
||||
},
|
||||
|
||||
//TODO: Add in ritual tag
|
||||
getSubtitle : function(spell){
|
||||
if(spell.level == 0) return <p><em>{spell.school} cantrip</em></p>;
|
||||
if(spell.level == 1) return <p><em>{spell.level}st-level {spell.school}</em></p>;
|
||||
if(spell.level == 2) return <p><em>{spell.level}nd-level {spell.school}</em></p>;
|
||||
if(spell.level == 3) return <p><em>{spell.level}rd-level {spell.school}</em></p>;
|
||||
return <p><em>{spell.level}th-level {spell.school}</em></p>;
|
||||
},
|
||||
|
||||
getComponents : function(spell){
|
||||
var result = [];
|
||||
if(spell.components.v) result.push('V');
|
||||
if(spell.components.s) result.push('S');
|
||||
if(spell.components.m) result.push('M ' + spell.components.m);
|
||||
return result.join(', ');
|
||||
},
|
||||
|
||||
getHigherLevels : function(spell){
|
||||
if(!spell.scales) return null;
|
||||
return <p>
|
||||
<strong><em>At Higher Levels. </em></strong>
|
||||
<span dangerouslySetInnerHTML={{__html: Markdown(spell.scales)}} />
|
||||
</p>;
|
||||
},
|
||||
|
||||
getClasses : function(spell){
|
||||
if(!spell.classes || !spell.classes.length) return null;
|
||||
|
||||
var classes = _.map(spell.classes, (cls)=>{
|
||||
return _.capitalize(cls);
|
||||
}).join(', ');
|
||||
|
||||
return <li>
|
||||
<strong>Classes:</strong> {classes}
|
||||
</li>
|
||||
},
|
||||
|
||||
|
||||
renderSpell : function(spell){
|
||||
console.log('rendering', spell);
|
||||
return <div className='spell' key={spell.id}>
|
||||
|
||||
<h4>{spell.name}</h4>
|
||||
{this.getSubtitle(spell)}
|
||||
<hr />
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Casting Time:</strong> {spell.casting_time}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Range:</strong> {spell.range}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Components:</strong> {this.getComponents(spell)}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Duration:</strong> {spell.duration}
|
||||
</li>
|
||||
{this.getClasses(spell)}
|
||||
</ul>
|
||||
|
||||
<span dangerouslySetInnerHTML={{__html: Markdown(spell.description)}} />
|
||||
{this.getHigherLevels(spell)}
|
||||
</div>
|
||||
},
|
||||
|
||||
renderSpells : function(){
|
||||
return _.map(this.props.spells, (spell)=>{
|
||||
return this.renderSpell(spell);
|
||||
})
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='spellRenderer'>
|
||||
|
||||
<div className='phb'>
|
||||
{this.renderSpells()}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SpellRenderer;
|
||||
@@ -1,40 +0,0 @@
|
||||
@import (less) 'naturalcrit/phbStyle/phb.style.less';
|
||||
.pane{
|
||||
position : relative;
|
||||
}
|
||||
.spellRenderer{
|
||||
overflow-y : scroll;
|
||||
|
||||
&>.phb{
|
||||
margin-right : auto;
|
||||
margin-bottom : 30px;
|
||||
margin-left : auto;
|
||||
box-shadow : 1px 4px 14px #000;
|
||||
|
||||
height : auto;
|
||||
width : 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
|
||||
column-count : initial;
|
||||
column-fill : initial;
|
||||
column-gap : initial;
|
||||
column-width : initial;
|
||||
-webkit-column-count : initial;
|
||||
-moz-column-count : initial;
|
||||
-webkit-column-width : initial;
|
||||
-moz-column-width : initial;
|
||||
-webkit-column-gap : initial;
|
||||
-moz-column-gap : initial;
|
||||
|
||||
.spell{
|
||||
display: inline;
|
||||
width : 8cm;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
var Navbar = require('./navbar/navbar.jsx');
|
||||
|
||||
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
var SpellRenderer = require('./spellRenderer/spellRenderer.jsx');
|
||||
var Sorter = require('./sorter/sorter.jsx');
|
||||
|
||||
var SpellSort = React.createClass({
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
spells : []
|
||||
};
|
||||
},
|
||||
|
||||
handleSplitMove : function(){
|
||||
|
||||
},
|
||||
|
||||
render : function(){
|
||||
console.log(this.props.spells);
|
||||
|
||||
return <div className='spellsort page'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
yo
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
|
||||
<Sorter spells={this.props.spells} />
|
||||
<SpellRenderer spells={this.props.spells} />
|
||||
|
||||
</SplitPane>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = SpellSort;
|
||||
@@ -1,4 +0,0 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.spellsort{
|
||||
|
||||
}
|
||||
@@ -2,12 +2,24 @@
|
||||
<html>
|
||||
<head>
|
||||
<script>global=window</script>
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.6.2/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link rel="icon" href="/assets/main/favicon.ico" type="image/x-icon" />
|
||||
<link rel="icon" href="/assets/naturalCrit/favicon.ico" type="image/x-icon" />
|
||||
{{=vitreum.css}}
|
||||
{{=vitreum.globals}}
|
||||
<title>Natural Crit - D&D Tools</title>
|
||||
|
||||
{{? vitreum.inProduction}}
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-72212009-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{{?}}
|
||||
</head>
|
||||
<body>
|
||||
<div id="reactContainer">{{=vitreum.component}}</div>
|
||||
@@ -15,16 +27,4 @@
|
||||
{{=vitreum.libs}}
|
||||
{{=vitreum.js}}
|
||||
{{=vitreum.reactRender}}
|
||||
|
||||
{{? vitreum.inProduction}}
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','http://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-72212009-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{{?}}
|
||||
</html>
|
||||
|
||||
26
gulpfile.js
26
gulpfile.js
@@ -5,19 +5,19 @@ var gulp = require("gulp");
|
||||
|
||||
|
||||
var gulp = vitreumTasks(gulp, {
|
||||
entryPoints: [
|
||||
'./client/main',
|
||||
'./client/homebrew',
|
||||
'./client/spellsort',
|
||||
'./client/admin'
|
||||
],
|
||||
entryPoints: ["./client/naturalCrit", "./client/homebrew", "./client/admin"],
|
||||
|
||||
DEV: true,
|
||||
|
||||
buildPath: "./build/",
|
||||
pageTemplate: "./client/template.dot",
|
||||
projectModules: ["./shared/naturalcrit","./shared/codemirror"],
|
||||
additionalRequirePaths : ['./shared', './node_modules'],
|
||||
|
||||
projectModules: ["./shared/naturalCrit"],
|
||||
|
||||
additionalRequirePaths : ['./shared'],
|
||||
|
||||
assetExts: ["*.svg", "*.png", "*.jpg", "*.pdf", "*.eot", "*.otf", "*.woff", "*.woff2", "*.ico", "*.ttf"],
|
||||
|
||||
serverWatchPaths: ["server"],
|
||||
serverScript: "server.js",
|
||||
libs: [
|
||||
@@ -25,14 +25,12 @@ var gulp = vitreumTasks(gulp, {
|
||||
"react-dom",
|
||||
"lodash",
|
||||
"classnames",
|
||||
|
||||
//From ./shared
|
||||
"codemirror",
|
||||
"codemirror/mode/gfm/gfm.js",
|
||||
'codemirror/mode/javascript/javascript.js',
|
||||
"jsoneditor",
|
||||
|
||||
"moment",
|
||||
|
||||
"superagent",
|
||||
|
||||
"marked",
|
||||
"pico-router",
|
||||
"pico-flux"
|
||||
@@ -44,7 +42,7 @@ var gulp = vitreumTasks(gulp, {
|
||||
var rename = require('gulp-rename');
|
||||
var less = require('gulp-less');
|
||||
gulp.task('phb', function(){
|
||||
gulp.src('./shared/naturalcrit/phbStyle/phb.style.less')
|
||||
gulp.src('./client/homebrew/phbStyle/phb.style.less')
|
||||
.pipe(less())
|
||||
.pipe(rename('phb.standalone.css'))
|
||||
.pipe(gulp.dest('./'));
|
||||
|
||||
20
package.json
20
package.json
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "naturalcrit",
|
||||
"description": "D&D Tools for the discerning DM",
|
||||
"version": "2.0.0",
|
||||
"name": "naturalCrit",
|
||||
"description": "A super rad project!",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"postinstall": "gulp prod",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"author": "stolksdorf",
|
||||
"author": "",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"app-module-path": "^1.0.4",
|
||||
@@ -15,17 +15,17 @@
|
||||
"classnames": "^2.2.0",
|
||||
"express": "^4.13.3",
|
||||
"gulp": "^3.9.0",
|
||||
"lodash": "^4.11.2",
|
||||
"jsoneditor": "^4.2.1",
|
||||
"lodash": "^3.10.1",
|
||||
"marked": "^0.3.5",
|
||||
"moment": "^2.11.0",
|
||||
"mongoose": "^4.3.3",
|
||||
"pico-flux": "^1.1.0",
|
||||
"pico-router": "^1.1.0",
|
||||
"react": "^15.0.2",
|
||||
"react-dom": "^15.0.2",
|
||||
"pico-router": "^1.0.0",
|
||||
"react": "^0.14.2",
|
||||
"react-dom": "^0.14.2",
|
||||
"shortid": "^2.2.4",
|
||||
"striptags": "^2.1.1",
|
||||
"superagent": "^1.6.1",
|
||||
"vitreum": "^3.2.1"
|
||||
"vitreum": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
51
server.js
51
server.js
@@ -1,5 +1,4 @@
|
||||
'use strict';
|
||||
var _ = require('lodash');
|
||||
require('app-module-path').addPath('./shared');
|
||||
var vitreumRender = require('vitreum/render');
|
||||
var bodyParser = require('body-parser')
|
||||
@@ -8,6 +7,9 @@ var app = express();
|
||||
app.use(express.static(__dirname + '/build'));
|
||||
app.use(bodyParser.json());
|
||||
|
||||
|
||||
|
||||
|
||||
//Mongoose
|
||||
var mongoose = require('mongoose');
|
||||
var mongoUri = process.env.MONGOLAB_URI || process.env.MONGOHQ_URL || 'mongodb://localhost/naturalcrit';
|
||||
@@ -16,49 +18,58 @@ mongoose.connection.on('error', function(){
|
||||
console.log(">>>ERROR: Run Mongodb.exe ya goof!");
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//Admin route
|
||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password';
|
||||
process.env.ADMIN_KEY = process.env.ADMIN_KEY || 'admin_key';
|
||||
var auth = require('basic-auth');
|
||||
|
||||
var HomebrewModel = require('./server/homebrew.model.js').model;
|
||||
|
||||
app.get('/admin', function(req, res){
|
||||
var credentials = auth(req)
|
||||
if (!credentials || credentials.name !== process.env.ADMIN_USER || credentials.pass !== process.env.ADMIN_PASS) {
|
||||
res.setHeader('WWW-Authenticate', 'Basic realm="example"')
|
||||
return res.status(401).send('Access denied')
|
||||
}
|
||||
vitreumRender({
|
||||
page: './build/admin/bundle.dot',
|
||||
prerenderWith : './client/admin/admin.jsx',
|
||||
clearRequireCache : !process.env.PRODUCTION,
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
admin_key : process.env.ADMIN_KEY,
|
||||
},
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
HomebrewModel.find({}, function(err, homebrews){
|
||||
vitreumRender({
|
||||
page: './build/admin/bundle.dot',
|
||||
prerenderWith : './client/admin/admin.jsx',
|
||||
clearRequireCache : true,
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
admin_key : process.env.ADMIN_KEY,
|
||||
|
||||
homebrews : homebrews,
|
||||
},
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
//Populate homebrew routes
|
||||
app = require('./server/homebrew.api.js')(app);
|
||||
app = require('./server/homebrew.server.js')(app);
|
||||
|
||||
|
||||
//Populate Spellsort routes
|
||||
app = require('./server/spellsort.server.js')(app);
|
||||
|
||||
|
||||
app.get('*', function (req, res) {
|
||||
vitreumRender({
|
||||
page: './build/main/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/main/main.jsx',
|
||||
page: './build/naturalCrit/bundle.dot',
|
||||
globals:{
|
||||
|
||||
},
|
||||
//prerenderWith : './client/naturalCrit/naturalCrit.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl
|
||||
},
|
||||
clearRequireCache : !process.env.PRODUCTION,
|
||||
clearRequireCache : true,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
|
||||
@@ -1,164 +0,0 @@
|
||||
module.exports = {
|
||||
"Abi-Dalzim's Horrid Wilting": {
|
||||
"athigherlevel": "",
|
||||
"description": "(a bit of sponge)\r\nYou draw the moisture from every creature in a 30-foot cube centered on a point you choose within range. Each creature in that area must make a Constitution saving throw. Constructs and undead aren't affected, and plants and water elementals make this saving throw with disadvantage. A creature takes 10d8 necrotic damage on a failed save, or half as much damage on a successful one.",
|
||||
"source": "Elemental Evil",
|
||||
"level": "8",
|
||||
"range": "150 feet",
|
||||
"casting_time": "1 Action",
|
||||
"id": "406",
|
||||
"slug": "abi-dalzims-horrid-wilting",
|
||||
"scag": "0",
|
||||
"page": "15",
|
||||
"components": "V, S, M",
|
||||
"ritual": "No",
|
||||
"name": "Abi-Dalzim's Horrid Wilting",
|
||||
"school": "Necromancy",
|
||||
"ee": "1",
|
||||
"classes": [
|
||||
"Sorcerer",
|
||||
"Wizard"
|
||||
],
|
||||
"duration": "Instantaneous",
|
||||
"concentration": "No"
|
||||
},
|
||||
"Absorb Elements ": {
|
||||
"athigherlevel": " When you cast this spell using a spell slot of 2nd level or higher, the extra damage increases by 1d6 for each slot level above 1st.",
|
||||
"description": "1 Reaction, which you take when you take acid, cold, fire, lightning, or thunder damage\n\nThe spell captures some of the incoming energy, lessening its effect on you and storing it for your next melee attack. You have resistance to the triggering damage type until the start of your next turn. Also, the first time you hit with a melee attack on your next turn, the target takes an extra 1d6 damage of the triggering type, and the spell ends.\r\n",
|
||||
"source": "Elemental Evil",
|
||||
"level": "1",
|
||||
"range": "Self",
|
||||
"casting_time": "Special",
|
||||
"id": "377",
|
||||
"slug": "absorb-elements",
|
||||
"scag": "0",
|
||||
"page": "15",
|
||||
"components": "S",
|
||||
"ritual": "No",
|
||||
"name": "Absorb Elements ",
|
||||
"school": "Abjuration",
|
||||
"ee": "1",
|
||||
"classes": [
|
||||
"Druid",
|
||||
"Ranger",
|
||||
"Wizard"
|
||||
],
|
||||
"duration": "1 round",
|
||||
"concentration": "No"
|
||||
},
|
||||
"Acid Splash": {
|
||||
"athigherlevel": "This spell's damage increases by 1d6 when you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).",
|
||||
"description": "You hurl a bubble of acid. \nChoose one creature within range, or choose two creatures within range that are within 5 feet of each other. A target must succeed on a Dexterity saving throw or take 1d6 acid damage. \n\n",
|
||||
"source": "Player's Handbook",
|
||||
"level": "0",
|
||||
"range": "60 feet",
|
||||
"casting_time": "1 Action",
|
||||
"id": "1",
|
||||
"slug": "acid-splash",
|
||||
"scag": "0",
|
||||
"page": "211",
|
||||
"components": "V, S",
|
||||
"ritual": "No",
|
||||
"name": "Acid Splash",
|
||||
"school": "Conjuration",
|
||||
"ee": "0",
|
||||
"classes": [
|
||||
"Sorcerer",
|
||||
"Wizard"
|
||||
],
|
||||
"duration": "Instantaneous",
|
||||
"concentration": "No"
|
||||
},
|
||||
"Aganazzar's Scorcher": {
|
||||
"athigherlevel": " When you cast this spell using a spell slot of 3rd level or higher, the damage increases by 1d8 for each slot level above 2nd.",
|
||||
"description": "A line of roaring flame 30 feet long and 5 feet wide emanates from you in a direction you choose. Each creature in the line must make a Dexterity saving throw. A creature takes 3d8 fire damage on a failed save, or half as much damage on a successful one.\r\n",
|
||||
"source": "Elemental Evil",
|
||||
"level": "2",
|
||||
"range": "30 feet",
|
||||
"casting_time": "1 Action",
|
||||
"id": "399",
|
||||
"slug": "aganazzars-scorcher",
|
||||
"scag": "0",
|
||||
"page": "15",
|
||||
"components": "V, S, M",
|
||||
"ritual": "No",
|
||||
"name": "Aganazzar's Scorcher",
|
||||
"school": "Evocation",
|
||||
"ee": "1",
|
||||
"classes": [
|
||||
"Sorcerer",
|
||||
"Wizard"
|
||||
],
|
||||
"duration": "Instantaneous",
|
||||
"concentration": "No"
|
||||
},
|
||||
"Aid": {
|
||||
"athigherlevel": "When you cast this spell using a spell slot of 3rd level or higher, a target's hit points increase by an additional 5 for each slot level above 2nd.",
|
||||
"description": "Your spell bolsters your allies with toughness and resolve. \nChoose up to three creatures within range. Each target's hit point maximum and current hit points increase by 5 for the duration.",
|
||||
"source": "Player's Handbook",
|
||||
"level": "2",
|
||||
"range": "30 feet",
|
||||
"casting_time": "1 Action",
|
||||
"id": "2",
|
||||
"slug": "aid",
|
||||
"scag": "0",
|
||||
"page": "211",
|
||||
"components": "V, S, M (a tiny strip of white cloth)",
|
||||
"ritual": "No",
|
||||
"name": "Aid",
|
||||
"school": "Abjuration",
|
||||
"ee": "0",
|
||||
"classes": [
|
||||
"Cleric",
|
||||
"Paladin"
|
||||
],
|
||||
"duration": "8 hours",
|
||||
"concentration": "No"
|
||||
},
|
||||
"Alarm (Ritual)": {
|
||||
"athigherlevel": "",
|
||||
"description": "You set an alarm against unwanted intrusion. \r\nChoose a door, a window, or an area within range that is no larger than a 20-foot cube. Until the spell ends, an alarm alerts you whenever a tiny or larger creature touches or enters the warded area. When you cast the spell, you can designate creatures that won't set off the alarm. You also choose whether the alarm is mental or audible. \n\nA mental alarm alerts you with a ping in your mind if you are within 1 mile of the warded area. This ping awakens you if you are sleeping. \r\nAn audible alarm produces the sound of a hand bell for 10 seconds within 60 feet.",
|
||||
"source": "Player's Handbook",
|
||||
"level": "1",
|
||||
"range": "30 feet",
|
||||
"casting_time": "1 Minute",
|
||||
"id": "3",
|
||||
"slug": "alarm-ritual",
|
||||
"scag": "0",
|
||||
"page": "211",
|
||||
"components": "V, S, M (a tiny bell and a piece of fine silver wire)",
|
||||
"ritual": "Yes",
|
||||
"name": "Alarm (Ritual)",
|
||||
"school": "Abjuration",
|
||||
"ee": "0",
|
||||
"classes": [
|
||||
"Ranger",
|
||||
"Wizard"
|
||||
],
|
||||
"duration": "8 hours",
|
||||
"concentration": "No"
|
||||
},
|
||||
"Alter Self": {
|
||||
"athigherlevel": "",
|
||||
"description": "You assume a different form.\r\nWhen you cast the spell, choose one of the following options, the effects of which last for the duration of the spell. While the spell lasts, you can end one option as an action to gain the benefits of a different one.\r\n\r\nAquatic Adaptation. You adapt your body to an aquatic environment, sprouting gills, and growing webbing between your fingers. You can breathe underwater and gain a swimming speed equal to your walking speed.\r\n\r\nChange Appearance. You transform your appearnce. You decide what you look like, including your height, weight, facial features, sound of your voice, hair length, coloration, and distinguishing characteristics, if any. \r\nYou can make yourself appear as a member of another race, though none of your statistics change. You also don't appear as a creature of a different size than you, and your basic shape stays the same; if you're bipedal, you can't use this spell to become quadrupedal, for instance. At any time for the duration of the spell, you can use your action to change your appearance in this way again.\r\n\n Natural Weapons. You grow claws, fangs, spines, horns, or a different natural weapon of your choice. Your unarmed strikes deal 1d6 bludgeoning, piercing, or slashing damage, as appropriate to the natural weapon you chose, and you are proficient with your unarmed strikes. Finally, the natural weapon is magic and you have a +1 bonus to the attack and damage rolls you make using it.",
|
||||
"source": "Player's Handbook",
|
||||
"level": "2",
|
||||
"range": "Self",
|
||||
"casting_time": "1 Action",
|
||||
"id": "4",
|
||||
"slug": "alter-self",
|
||||
"scag": "0",
|
||||
"page": "211",
|
||||
"components": "V, S",
|
||||
"ritual": "No",
|
||||
"name": "Alter Self",
|
||||
"school": "Transmutation",
|
||||
"ee": "0",
|
||||
"classes": [
|
||||
"Sorcerer",
|
||||
"Wizard"
|
||||
],
|
||||
"duration": "Concentration, up to 1 hour",
|
||||
"concentration": "Yes"
|
||||
},
|
||||
}
|
||||
@@ -1,55 +1,24 @@
|
||||
var _ = require('lodash');
|
||||
var Moment = require('moment');
|
||||
var vitreumRender = require('vitreum/render');
|
||||
var HomebrewModel = require('./homebrew.model.js').model;
|
||||
|
||||
var homebrewTotal = 0;
|
||||
var refreshCount = function(){
|
||||
HomebrewModel.count({}, function(err, total){
|
||||
homebrewTotal = total;
|
||||
});
|
||||
};
|
||||
refreshCount()
|
||||
|
||||
var mw = {
|
||||
adminOnly : function(req, res, next){
|
||||
if(req.query && req.query.admin_key == process.env.ADMIN_KEY){
|
||||
next();
|
||||
}else{
|
||||
return res.status(401).send('Access denied');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var getTopBrews = function(cb){
|
||||
HomebrewModel.find().sort({views: -1}).limit(5).exec(function(err, brews) {
|
||||
cb(brews);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
module.exports = function(app){
|
||||
|
||||
app.get('/homebrew/top', function(req, res){
|
||||
getTopBrews(function(topBrews){
|
||||
return res.json(topBrews);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/homebrew/api', function(req, res){
|
||||
var newHomebrew = new HomebrewModel(req.body);
|
||||
app.get('/homebrew/new', function(req, res){
|
||||
var newHomebrew = new HomebrewModel();
|
||||
newHomebrew.save(function(err, obj){
|
||||
if(err) return;
|
||||
return res.json(obj);
|
||||
return res.redirect('/homebrew/edit/' + obj.editId);
|
||||
})
|
||||
});
|
||||
})
|
||||
|
||||
app.put('/homebrew/api/update/:id', function(req, res){
|
||||
|
||||
//Updating
|
||||
app.put('/homebrew/update/:id', function(req, res){
|
||||
HomebrewModel.find({editId : req.params.id}, function(err, objs){
|
||||
if(!objs.length || err) return res.status(404).send("Can not find homebrew with that id");
|
||||
var resEntry = objs[0];
|
||||
resEntry.text = req.body.text;
|
||||
resEntry.title = req.body.title;
|
||||
resEntry.updatedAt = new Date();
|
||||
resEntry.save(function(err, obj){
|
||||
if(err) return res.status(500).send("Error while saving");
|
||||
@@ -58,65 +27,164 @@ module.exports = function(app){
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/homebrew/api/remove/:id', function(req, res){
|
||||
HomebrewModel.find({editId : req.params.id}, function(err, objs){
|
||||
if(!objs.length || err) return res.status(404).send("Can not find homebrew with that id");
|
||||
var resEntry = objs[0];
|
||||
resEntry.remove(function(err){
|
||||
if(err) return res.status(500).send("Error while removing");
|
||||
return res.status(200).send();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
|
||||
app.get('/homebrew/api/invalid', mw.adminOnly, function(req, res){
|
||||
var invalidBrewQuery = HomebrewModel.find({
|
||||
'$where' : "this.text.length < 140",
|
||||
createdAt: {
|
||||
$lt: Moment().subtract(3, 'days').toDate()
|
||||
}
|
||||
});
|
||||
|
||||
if(req.query.do_it){
|
||||
invalidBrewQuery.remove().exec((err, objs)=>{
|
||||
refreshCount();
|
||||
return res.send(200);
|
||||
})
|
||||
}else{
|
||||
invalidBrewQuery.exec((err, objs)=>{
|
||||
if(err) console.log(err);
|
||||
return res.json({
|
||||
count : objs.length
|
||||
app.get('/homebrew/remove/:id', function(req, res){
|
||||
if(req.query && req.query.admin_key == process.env.ADMIN_KEY){
|
||||
HomebrewModel.find({editId : req.params.id}, function(err, objs){
|
||||
if(!objs.length || err) return res.status(404).send("Can not find homebrew with that id");
|
||||
var resEntry = objs[0];
|
||||
resEntry.remove(function(err){
|
||||
if(err) return res.status(500).send("Error while removing");
|
||||
return res.status(200).send();
|
||||
})
|
||||
})
|
||||
});
|
||||
}else{
|
||||
return res.status(401).send('Access denied');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.get('/homebrew/api/search', mw.adminOnly, function(req, res){
|
||||
var page = req.query.page || 0;
|
||||
var count = req.query.count || 20;
|
||||
|
||||
HomebrewModel.find({}, {
|
||||
text : 0 //omit the text
|
||||
}, {
|
||||
skip: page*count,
|
||||
limit: count*1
|
||||
}, function(err, objs){
|
||||
if(err) console.log(err);
|
||||
return res.json({
|
||||
page : page,
|
||||
count : count,
|
||||
total : homebrewTotal,
|
||||
brews : objs
|
||||
|
||||
//Edit Page
|
||||
app.get('/homebrew/edit/:id', function(req, res){
|
||||
HomebrewModel.find({editId : req.params.id}, function(err, objs){
|
||||
if(err || !objs.length) return res.status(404).send('Could not find Homebrew with that id');
|
||||
|
||||
var resObj = null;
|
||||
var errObj = {text: "# oops\nCould not find the homebrew."}
|
||||
if(objs.length){
|
||||
resObj = objs[0];
|
||||
}
|
||||
|
||||
vitreumRender({
|
||||
page: './build/homebrew/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/homebrew/homebrew.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
brew : resObj || errObj
|
||||
},
|
||||
clearRequireCache : true,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
//Share Page
|
||||
app.get('/homebrew/share/:id', function(req, res){
|
||||
HomebrewModel.find({shareId : req.params.id}, function(err, objs){
|
||||
if(err || !objs.length) return res.status(404).send('Could not find Homebrew with that id');
|
||||
|
||||
var resObj = null;
|
||||
var errObj = {text: "# oops\nCould not find the homebrew."}
|
||||
|
||||
if(objs.length){
|
||||
resObj = objs[0];
|
||||
resObj.lastViewed = new Date();
|
||||
resObj.views = resObj.views + 1;
|
||||
resObj.save();
|
||||
}
|
||||
|
||||
vitreumRender({
|
||||
page: './build/homebrew/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/homebrew/homebrew.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
brew : resObj || errObj
|
||||
},
|
||||
clearRequireCache : true,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
//Print Page
|
||||
var Markdown = require('marked');
|
||||
var PHBStyle = '<style>' + require('fs').readFileSync('./phb.standalone.css', 'utf8') + '</style>'
|
||||
app.get('/homebrew/print/:id', function(req, res){
|
||||
HomebrewModel.find({shareId : req.params.id}, function(err, objs){
|
||||
if(err || !objs.length) return res.status(404).send('Could not find Homebrew with that id');
|
||||
|
||||
var resObj = null;
|
||||
if(objs.length){
|
||||
resObj = objs[0];
|
||||
}
|
||||
|
||||
var content = _.map(resObj.text.split('\\page'), function(pageText){
|
||||
return '<div class="phb">' + Markdown(pageText) + '</div>';
|
||||
}).join('\n');
|
||||
|
||||
var title = '<title>' + resObj.text.split('\n')[0] + '</title>';
|
||||
var page = '<html><head>' + title + PHBStyle + '</head><body>' + content +'</body></html>'
|
||||
|
||||
return res.send(page)
|
||||
})
|
||||
});
|
||||
|
||||
//PDF download
|
||||
/*
|
||||
var pdf = require('html-pdf');
|
||||
app.get('/homebrew/pdf/:id', function(req, res){
|
||||
HomebrewModel.find({shareId : req.params.id}, function(err, objs){
|
||||
if(err) return res.status(404).send();
|
||||
|
||||
var resObj = null;
|
||||
var errObj = {text: "# oops\nCould not find the homebrew."}
|
||||
if(objs.length){
|
||||
resObj = objs[0];
|
||||
}
|
||||
|
||||
var content = _.map(resObj.text.split('\\page'), function(pageText){
|
||||
return '<div class="phb">' + Markdown(pageText) + '</div>';
|
||||
}).join('\n');
|
||||
|
||||
var title = '<title>' + resObj.text.split('\n')[0] + '</title>';
|
||||
var page = '<html><head>' + title + PHBStyle + '</head><body>' + content +'</body></html>'
|
||||
|
||||
|
||||
|
||||
var config = {
|
||||
"height": (279.4 - 56) + "mm",
|
||||
"width": (215.9 - 43) + "mm",
|
||||
"border": "0",
|
||||
}
|
||||
|
||||
pdf.create(page, config).toStream(function(err, stream){
|
||||
|
||||
res.attachment('pdfname.pdf');
|
||||
return stream.pipe(res);
|
||||
});
|
||||
|
||||
|
||||
})
|
||||
});
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
//Home and 404, etc.
|
||||
var welcomeText = require('fs').readFileSync('./client/homebrew/homePage/welcome_msg.txt', 'utf8');
|
||||
|
||||
app.get('/homebrew*', function (req, res) {
|
||||
vitreumRender({
|
||||
page: './build/homebrew/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/homebrew/homebrew.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
welcomeText : welcomeText
|
||||
},
|
||||
clearRequireCache : true,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -3,9 +3,8 @@ var shortid = require('shortid');
|
||||
var _ = require('lodash');
|
||||
|
||||
var HomebrewSchema = mongoose.Schema({
|
||||
shareId : {type : String, default: shortid.generate, index: { unique: true }},
|
||||
editId : {type : String, default: shortid.generate, index: { unique: true }},
|
||||
title : {type : String, default : ""},
|
||||
shareId : {type : String, default: shortid.generate},
|
||||
editId : {type : String, default: shortid.generate},
|
||||
text : {type : String, default : ""},
|
||||
|
||||
createdAt : { type: Date, default: Date.now },
|
||||
@@ -16,6 +15,20 @@ var HomebrewSchema = mongoose.Schema({
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
HomebrewSchema.options.toJSON.transform = function (doc, ret, options) {
|
||||
delete ret._id;
|
||||
delete ret.__t;
|
||||
delete ret.__v;
|
||||
}
|
||||
HomebrewSchema.options.toObject.transform = function (doc, ret, options) {
|
||||
delete ret._id;
|
||||
delete ret.__t;
|
||||
delete ret.__v;
|
||||
}
|
||||
*/
|
||||
|
||||
var Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var vitreumRender = require('vitreum/render');
|
||||
var HomebrewModel = require('./homebrew.model.js').model;
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = function(app){
|
||||
|
||||
|
||||
//Edit Page
|
||||
app.get('/homebrew/edit/:id', function(req, res){
|
||||
HomebrewModel.find({editId : req.params.id}, function(err, objs){
|
||||
if(err || !objs.length) return res.status(404).send('Could not find Homebrew with that id');
|
||||
|
||||
var resObj = null;
|
||||
var errObj = {text: "# oops\nCould not find the homebrew."}
|
||||
if(objs.length){
|
||||
resObj = objs[0];
|
||||
}
|
||||
|
||||
vitreumRender({
|
||||
page: './build/homebrew/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/homebrew/homebrew.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
brew : resObj || errObj
|
||||
},
|
||||
clearRequireCache : !process.env.PRODUCTION,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
//Share Page
|
||||
app.get('/homebrew/share/:id', function(req, res){
|
||||
HomebrewModel.find({shareId : req.params.id}, function(err, objs){
|
||||
if(err || !objs.length) return res.status(404).send('Could not find Homebrew with that id');
|
||||
|
||||
var resObj = null;
|
||||
var errObj = {text: "# oops\nCould not find the homebrew."}
|
||||
|
||||
if(objs.length){
|
||||
resObj = objs[0];
|
||||
resObj.lastViewed = new Date();
|
||||
resObj.views = resObj.views + 1;
|
||||
resObj.save();
|
||||
}
|
||||
|
||||
vitreumRender({
|
||||
page: './build/homebrew/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/homebrew/homebrew.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
brew : resObj || errObj
|
||||
},
|
||||
clearRequireCache : !process.env.PRODUCTION,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
//Print Page
|
||||
var Markdown = require('marked');
|
||||
var PHBStyle = '<style>' + require('fs').readFileSync('./phb.standalone.css', 'utf8') + '</style>'
|
||||
app.get('/homebrew/print/:id', function(req, res){
|
||||
HomebrewModel.find({shareId : req.params.id}, function(err, objs){
|
||||
if(err || !objs.length) return res.status(404).send('Could not find Homebrew with that id');
|
||||
|
||||
var brew = null;
|
||||
if(objs.length){
|
||||
brew = objs[0];
|
||||
}
|
||||
|
||||
var content = _.map(brew.text.split('\\page'), function(pageText){
|
||||
return '<div class="phb print">' + Markdown(pageText) + '</div>';
|
||||
}).join('\n');
|
||||
|
||||
var dialog = '';
|
||||
if(req.query && req.query.dialog) dialog = 'onload="window.print()"';
|
||||
|
||||
var title = '<title>' + brew.title + '</title>';
|
||||
var page = `<html><head>${title} ${PHBStyle}</head><body ${dialog}>${content}</body></html>`
|
||||
|
||||
return res.send(page)
|
||||
});
|
||||
});
|
||||
|
||||
//Source page
|
||||
String.prototype.replaceAll = function(s,r){return this.split(s).join(r)}
|
||||
app.get('/homebrew/source/:id', function(req, res){
|
||||
HomebrewModel.find({shareId : req.params.id}, function(err, objs){
|
||||
if(err || !objs.length) return res.status(404).send('Could not find Homebrew with that id');
|
||||
var brew = null;
|
||||
if(objs.length) brew = objs[0];
|
||||
var text = brew.text.replaceAll('<', '<').replaceAll('>', '>');
|
||||
return res.send(`<code><pre>${text}</pre></code>`);
|
||||
});
|
||||
});
|
||||
|
||||
//Home and 404, etc.
|
||||
var welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.txt', 'utf8');
|
||||
var changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
|
||||
app.get('/homebrew*', function (req, res) {
|
||||
vitreumRender({
|
||||
page: './build/homebrew/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/homebrew/homebrew.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
welcomeText : welcomeText,
|
||||
changelog : changelogText
|
||||
},
|
||||
clearRequireCache : !process.env.PRODUCTION,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
var spells = require('./5espells.js');
|
||||
|
||||
String.prototype.replaceAll = function(s,r){return this.split(s).join(r)}
|
||||
|
||||
|
||||
var parsedSpells = _.map(spells, (spell)=>{
|
||||
|
||||
var comp = {}
|
||||
|
||||
var name = spell.name.replace(' (Ritual)', '');
|
||||
|
||||
|
||||
return {
|
||||
id : _.snakeCase(name),
|
||||
name : name,
|
||||
description : spell.description
|
||||
.replaceAll('\r\n', '\n')
|
||||
.replaceAll(' ', ''),
|
||||
|
||||
scales : spell.athigherlevel,
|
||||
|
||||
components : {},
|
||||
classes : _.map(spell.classes || [], (cls)=>{return cls.toLowerCase();}),
|
||||
|
||||
|
||||
level : Number(spell.level),
|
||||
|
||||
ritual : spell.ritual == "Yes",
|
||||
concentration : spell.concentration == "Yes",
|
||||
|
||||
range : spell.range,
|
||||
duration : spell.duration,
|
||||
|
||||
|
||||
school : spell.school.toLowerCase(),
|
||||
|
||||
source : spell.source,
|
||||
page : spell.page,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
module.exports = parsedSpells;
|
||||
|
||||
31
server/pdfConvert.js
Normal file
31
server/pdfConvert.js
Normal file
@@ -0,0 +1,31 @@
|
||||
var pdf = require('html-pdf');
|
||||
var Markdown = require('marked');
|
||||
|
||||
var PHBStyle = '<style>' + require('fs').readFileSync('../phb.standalone.css', 'utf8') + '</style>'
|
||||
|
||||
|
||||
var content = Markdown('# oh hey \n welcome! isnt this neat \n \\page ##### test');
|
||||
|
||||
|
||||
var html = "<html><head>" + PHBStyle + "</head><body><div class='phb'>"+ content +"</div></body></html>"
|
||||
|
||||
//var h = 279.4 - 20*2.8;
|
||||
var h = 279.4 - 56;
|
||||
|
||||
|
||||
|
||||
//var w = 215.9 - 56*1.7
|
||||
|
||||
var w = 215.9 - 43;
|
||||
|
||||
|
||||
var config = {
|
||||
"height": (279.4 - 56) + "mm",
|
||||
"width": (215.9 - 43) + "mm",
|
||||
"border": "0",
|
||||
}
|
||||
|
||||
pdf.create(html, config).toFile('./temp.pdf', function(err, res){
|
||||
console.log(err);
|
||||
console.log(res.filename);
|
||||
});
|
||||
BIN
server/pdfs/businesscard.pdf
Normal file
BIN
server/pdfs/businesscard.pdf
Normal file
Binary file not shown.
@@ -1,27 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
var vitreumRender = require('vitreum/render');
|
||||
|
||||
|
||||
|
||||
console.log(JSON.stringify(require('./parsespell.js'), null, ' '));
|
||||
|
||||
|
||||
module.exports = function(app){
|
||||
app.get('/spellsort*', (req, res)=>{
|
||||
vitreumRender({
|
||||
page: './build/spellsort/bundle.dot',
|
||||
globals:{},
|
||||
prerenderWith : './client/spellsort/spellsort.jsx',
|
||||
initialProps: {
|
||||
url: req.originalUrl,
|
||||
//spells : require('./spellsort.spells.js')
|
||||
spells : require('./parsespell.js')
|
||||
},
|
||||
clearRequireCache : !process.env.PRODUCTION,
|
||||
}, function (err, page) {
|
||||
return res.send(page)
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
var _ = require('lodash');
|
||||
|
||||
var spells = [
|
||||
{
|
||||
name : "Acid Splash",
|
||||
casting_time : "1 action",
|
||||
components : {v : true, s : true},
|
||||
description : `You hurl a bubble of acid. Choose one creature within range, or choose two creatures within range that are within 5 feet of each other.
|
||||
A target must succeed on a Dexterity saving throw or take 1d6 acid damage.`,
|
||||
scales : 'This spell’s damage increases by 1d6 when you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).',
|
||||
duration : "Instantaneous",
|
||||
concentration : false,
|
||||
level : 0,
|
||||
ritual : false,
|
||||
range : "60 feet",
|
||||
school : "Conjuration",
|
||||
classes : ["sorcerer", "wizard"],
|
||||
source : "Player's Handbook",
|
||||
page : "pg.211"
|
||||
},
|
||||
{
|
||||
name : "Aid",
|
||||
casting_time : "1 action",
|
||||
components : {v : true, s : true, m : "(a tiny strip of white cloth)"},
|
||||
description : `Your spell bolsters your allies with toughness and resolve. Choose up to three creatures within range. Each target’s hit point maximum and current hit points increase by 5 for the duration. At Higher Levels. When you cast this spell using a spell slot of 3rd level or higher, a target’s hit points increase by an additional 5 for each slot level above 2nd.`,
|
||||
duration : "8 hours",
|
||||
concentration : false,
|
||||
level : 2,
|
||||
ritual : false,
|
||||
range : "30 feet",
|
||||
school : "Abjuration",
|
||||
classes : [],
|
||||
source : "Player's Handbook",
|
||||
page : "pg.211"
|
||||
},
|
||||
{
|
||||
name : "Antimagic Field",
|
||||
casting_time : "1 action",
|
||||
components : {v : true, s : true, m : "(a pinch of powdered iron or iron filings)"},
|
||||
description : `A 10-foot-radius invisible sphere of antimagic surrounds you. This area is divorced from the magical energy that suffuses the multiverse. Within the sphere, spells can’t be cast, summoned creatures disappear, and even magic items become mundane. Until the spell ends, the sphere moves with you, centered on you. Spells and other magical effects, except those created by an artifact or a deity, are suppressed in the sphere and can’t protrude into it. A slot expended to cast a suppressed spell is consumed. While an effect is suppressed, it doesn’t function, but the time it spends suppressed counts against its duration. Targeted Effects. Spells and other magical effects, such as magic missile and charm person, that target a creature or an object in the sphere have no effect on that target. Areas of Magic. The area of another spell or magical effect, such as fireball, can’t extend into the sphere. If the sphere overlaps an area of magic, the part of the area that is covered by the sphere is suppressed. For example, the flames created by a wall of fire are suppressed within the sphere, creating a gap in the wall if the overlap is large enough. Spells. Any active spell or other magical effect on a creature or an object in the sphere is suppressed while the creature or object is in it. Magic Items. The properties and powers of magic items are suppressed in the sphere. For example, a +1 longsword in the sphere functions as a nonmagical longsword. A magic weapon’s properties and powers are suppressed if it is used against a target in the sphere or wielded by an attacker in the sphere. If a magic weapon or a piece of magic ammunition fully leaves the sphere (for example, if you fire a magic arrow or throw a magic spear at a target outside the sphere), the magic of the item ceases to be suppressed as soon as it exits. Magical Travel. Teleportation and planar travel fail to work in the sphere, whether the sphere is the destination or the departure point for such magical travel. A portal to another location, world, or plane of existence, as well as an opening to an extradimensional space such as that created by the rope trick spell, temporarily closes while in the sphere. Creatures and Objects. A creature or object summoned or created by magic temporarily winks out of existence in the sphere. Such a creature instantly reappears once the space the creature occupied is no longer within the sphere. Dispel Magic. Spells and magical effects such as dispel magic have no effect on the sphere. Likewise, the spheres created by different antimagic field spells don’t nullify each other.`,
|
||||
duration : "Concentration, up to 1 hour",
|
||||
concentration : true,
|
||||
level : 8,
|
||||
ritual : false,
|
||||
range : "Self (10-foot-radius sphere)",
|
||||
school : "Abjuration",
|
||||
classes : [],
|
||||
source : "Player's Handbook",
|
||||
page : "pg.213"
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
module.exports = _.map(spells, (spell)=>{
|
||||
spell.id = _.snakeCase(spell.name);
|
||||
return spell;
|
||||
});
|
||||
BIN
server/temp.pdf
Normal file
BIN
server/temp.pdf
Normal file
Binary file not shown.
203
shared/codemirror/addon/comment/comment.js
vendored
203
shared/codemirror/addon/comment/comment.js
vendored
@@ -1,203 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
var noOptions = {};
|
||||
var nonWS = /[^\s\u00a0]/;
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function firstNonWS(str) {
|
||||
var found = str.search(nonWS);
|
||||
return found == -1 ? 0 : found;
|
||||
}
|
||||
|
||||
CodeMirror.commands.toggleComment = function(cm) {
|
||||
cm.toggleComment();
|
||||
};
|
||||
|
||||
CodeMirror.defineExtension("toggleComment", function(options) {
|
||||
if (!options) options = noOptions;
|
||||
var cm = this;
|
||||
var minLine = Infinity, ranges = this.listSelections(), mode = null;
|
||||
for (var i = ranges.length - 1; i >= 0; i--) {
|
||||
var from = ranges[i].from(), to = ranges[i].to();
|
||||
if (from.line >= minLine) continue;
|
||||
if (to.line >= minLine) to = Pos(minLine, 0);
|
||||
minLine = from.line;
|
||||
if (mode == null) {
|
||||
if (cm.uncomment(from, to, options)) mode = "un";
|
||||
else { cm.lineComment(from, to, options); mode = "line"; }
|
||||
} else if (mode == "un") {
|
||||
cm.uncomment(from, to, options);
|
||||
} else {
|
||||
cm.lineComment(from, to, options);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Rough heuristic to try and detect lines that are part of multi-line string
|
||||
function probablyInsideString(cm, pos, line) {
|
||||
return /\bstring\b/.test(cm.getTokenTypeAt(Pos(pos.line, 0))) && !/^[\'\"`]/.test(line)
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("lineComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = self.getModeAt(from);
|
||||
var firstLine = self.getLine(from.line);
|
||||
if (firstLine == null || probablyInsideString(self, from, firstLine)) return;
|
||||
|
||||
var commentString = options.lineComment || mode.lineComment;
|
||||
if (!commentString) {
|
||||
if (options.blockCommentStart || mode.blockCommentStart) {
|
||||
options.fullLines = true;
|
||||
self.blockComment(from, to, options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line + 1 : to.line, self.lastLine() + 1);
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
var blankLines = options.commentBlankLines || from.line == to.line;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.indent) {
|
||||
var baseString = null;
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var whitespace = line.slice(0, firstNonWS(line));
|
||||
if (baseString == null || baseString.length > whitespace.length) {
|
||||
baseString = whitespace;
|
||||
}
|
||||
}
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
var line = self.getLine(i), cut = baseString.length;
|
||||
if (!blankLines && !nonWS.test(line)) continue;
|
||||
if (line.slice(0, cut) != baseString) cut = firstNonWS(line);
|
||||
self.replaceRange(baseString + commentString + pad, Pos(i, 0), Pos(i, cut));
|
||||
}
|
||||
} else {
|
||||
for (var i = from.line; i < end; ++i) {
|
||||
if (blankLines || nonWS.test(self.getLine(i)))
|
||||
self.replaceRange(commentString + pad, Pos(i, 0));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("blockComment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = self.getModeAt(from);
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) {
|
||||
if ((options.lineComment || mode.lineComment) && options.fullLines != false)
|
||||
self.lineComment(from, to, options);
|
||||
return;
|
||||
}
|
||||
|
||||
var end = Math.min(to.line, self.lastLine());
|
||||
if (end != from.line && to.ch == 0 && nonWS.test(self.getLine(end))) --end;
|
||||
|
||||
var pad = options.padding == null ? " " : options.padding;
|
||||
if (from.line > end) return;
|
||||
|
||||
self.operation(function() {
|
||||
if (options.fullLines != false) {
|
||||
var lastLineHasText = nonWS.test(self.getLine(end));
|
||||
self.replaceRange(pad + endString, Pos(end));
|
||||
self.replaceRange(startString + pad, Pos(from.line, 0));
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
if (lead != null) for (var i = from.line + 1; i <= end; ++i)
|
||||
if (i != end || lastLineHasText)
|
||||
self.replaceRange(lead + pad, Pos(i, 0));
|
||||
} else {
|
||||
self.replaceRange(endString, to);
|
||||
self.replaceRange(startString, from);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("uncomment", function(from, to, options) {
|
||||
if (!options) options = noOptions;
|
||||
var self = this, mode = self.getModeAt(from);
|
||||
var end = Math.min(to.ch != 0 || to.line == from.line ? to.line : to.line - 1, self.lastLine()), start = Math.min(from.line, end);
|
||||
|
||||
// Try finding line comments
|
||||
var lineString = options.lineComment || mode.lineComment, lines = [];
|
||||
var pad = options.padding == null ? " " : options.padding, didSomething;
|
||||
lineComment: {
|
||||
if (!lineString) break lineComment;
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = self.getLine(i);
|
||||
var found = line.indexOf(lineString);
|
||||
if (found > -1 && !/comment/.test(self.getTokenTypeAt(Pos(i, found + 1)))) found = -1;
|
||||
if (found == -1 && (i != end || i == start) && nonWS.test(line)) break lineComment;
|
||||
if (found > -1 && nonWS.test(line.slice(0, found))) break lineComment;
|
||||
lines.push(line);
|
||||
}
|
||||
self.operation(function() {
|
||||
for (var i = start; i <= end; ++i) {
|
||||
var line = lines[i - start];
|
||||
var pos = line.indexOf(lineString), endPos = pos + lineString.length;
|
||||
if (pos < 0) continue;
|
||||
if (line.slice(endPos, endPos + pad.length) == pad) endPos += pad.length;
|
||||
didSomething = true;
|
||||
self.replaceRange("", Pos(i, pos), Pos(i, endPos));
|
||||
}
|
||||
});
|
||||
if (didSomething) return true;
|
||||
}
|
||||
|
||||
// Try block comments
|
||||
var startString = options.blockCommentStart || mode.blockCommentStart;
|
||||
var endString = options.blockCommentEnd || mode.blockCommentEnd;
|
||||
if (!startString || !endString) return false;
|
||||
var lead = options.blockCommentLead || mode.blockCommentLead;
|
||||
var startLine = self.getLine(start), endLine = end == start ? startLine : self.getLine(end);
|
||||
var open = startLine.indexOf(startString), close = endLine.lastIndexOf(endString);
|
||||
if (close == -1 && start != end) {
|
||||
endLine = self.getLine(--end);
|
||||
close = endLine.lastIndexOf(endString);
|
||||
}
|
||||
if (open == -1 || close == -1 ||
|
||||
!/comment/.test(self.getTokenTypeAt(Pos(start, open + 1))) ||
|
||||
!/comment/.test(self.getTokenTypeAt(Pos(end, close + 1))))
|
||||
return false;
|
||||
|
||||
// Avoid killing block comments completely outside the selection.
|
||||
// Positions of the last startString before the start of the selection, and the first endString after it.
|
||||
var lastStart = startLine.lastIndexOf(startString, from.ch);
|
||||
var firstEnd = lastStart == -1 ? -1 : startLine.slice(0, from.ch).indexOf(endString, lastStart + startString.length);
|
||||
if (lastStart != -1 && firstEnd != -1 && firstEnd + endString.length != from.ch) return false;
|
||||
// Positions of the first endString after the end of the selection, and the last startString before it.
|
||||
firstEnd = endLine.indexOf(endString, to.ch);
|
||||
var almostLastStart = endLine.slice(to.ch).lastIndexOf(startString, firstEnd - to.ch);
|
||||
lastStart = (firstEnd == -1 || almostLastStart == -1) ? -1 : to.ch + almostLastStart;
|
||||
if (firstEnd != -1 && lastStart != -1 && lastStart != to.ch) return false;
|
||||
|
||||
self.operation(function() {
|
||||
self.replaceRange("", Pos(end, close - (pad && endLine.slice(close - pad.length, close) == pad ? pad.length : 0)),
|
||||
Pos(end, close + endString.length));
|
||||
var openEnd = open + startString.length;
|
||||
if (pad && startLine.slice(openEnd, openEnd + pad.length) == pad) openEnd += pad.length;
|
||||
self.replaceRange("", Pos(start, open), Pos(start, openEnd));
|
||||
if (lead) for (var i = start + 1; i <= end; ++i) {
|
||||
var line = self.getLine(i), found = line.indexOf(lead);
|
||||
if (found == -1 || nonWS.test(line.slice(0, found))) continue;
|
||||
var foundEnd = found + lead.length;
|
||||
if (pad && line.slice(foundEnd, foundEnd + pad.length) == pad) foundEnd += pad.length;
|
||||
self.replaceRange("", Pos(i, found), Pos(i, foundEnd));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
var modes = ["clike", "css", "javascript"];
|
||||
|
||||
for (var i = 0; i < modes.length; ++i)
|
||||
CodeMirror.extendMode(modes[i], {blockCommentContinue: " * "});
|
||||
|
||||
function continueComment(cm) {
|
||||
if (cm.getOption("disableInput")) return CodeMirror.Pass;
|
||||
var ranges = cm.listSelections(), mode, inserts = [];
|
||||
for (var i = 0; i < ranges.length; i++) {
|
||||
var pos = ranges[i].head, token = cm.getTokenAt(pos);
|
||||
if (token.type != "comment") return CodeMirror.Pass;
|
||||
var modeHere = CodeMirror.innerMode(cm.getMode(), token.state).mode;
|
||||
if (!mode) mode = modeHere;
|
||||
else if (mode != modeHere) return CodeMirror.Pass;
|
||||
|
||||
var insert = null;
|
||||
if (mode.blockCommentStart && mode.blockCommentContinue) {
|
||||
var end = token.string.indexOf(mode.blockCommentEnd);
|
||||
var full = cm.getRange(CodeMirror.Pos(pos.line, 0), CodeMirror.Pos(pos.line, token.end)), found;
|
||||
if (end != -1 && end == token.string.length - mode.blockCommentEnd.length && pos.ch >= end) {
|
||||
// Comment ended, don't continue it
|
||||
} else if (token.string.indexOf(mode.blockCommentStart) == 0) {
|
||||
insert = full.slice(0, token.start);
|
||||
if (!/^\s*$/.test(insert)) {
|
||||
insert = "";
|
||||
for (var j = 0; j < token.start; ++j) insert += " ";
|
||||
}
|
||||
} else if ((found = full.indexOf(mode.blockCommentContinue)) != -1 &&
|
||||
found + mode.blockCommentContinue.length > token.start &&
|
||||
/^\s*$/.test(full.slice(0, found))) {
|
||||
insert = full.slice(0, found);
|
||||
}
|
||||
if (insert != null) insert += mode.blockCommentContinue;
|
||||
}
|
||||
if (insert == null && mode.lineComment && continueLineCommentEnabled(cm)) {
|
||||
var line = cm.getLine(pos.line), found = line.indexOf(mode.lineComment);
|
||||
if (found > -1) {
|
||||
insert = line.slice(0, found);
|
||||
if (/\S/.test(insert)) insert = null;
|
||||
else insert += mode.lineComment + line.slice(found + mode.lineComment.length).match(/^\s*/)[0];
|
||||
}
|
||||
}
|
||||
if (insert == null) return CodeMirror.Pass;
|
||||
inserts[i] = "\n" + insert;
|
||||
}
|
||||
|
||||
cm.operation(function() {
|
||||
for (var i = ranges.length - 1; i >= 0; i--)
|
||||
cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
|
||||
});
|
||||
}
|
||||
|
||||
function continueLineCommentEnabled(cm) {
|
||||
var opt = cm.getOption("continueComments");
|
||||
if (opt && typeof opt == "object")
|
||||
return opt.continueLineComment !== false;
|
||||
return true;
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("continueComments", null, function(cm, val, prev) {
|
||||
if (prev && prev != CodeMirror.Init)
|
||||
cm.removeKeyMap("continueComment");
|
||||
if (val) {
|
||||
var key = "Enter";
|
||||
if (typeof val == "string")
|
||||
key = val;
|
||||
else if (typeof val == "object" && val.key)
|
||||
key = val.key;
|
||||
var map = {name: "continueComment"};
|
||||
map[key] = continueComment;
|
||||
cm.addKeyMap(map);
|
||||
}
|
||||
});
|
||||
});
|
||||
32
shared/codemirror/addon/dialog/dialog.css
vendored
32
shared/codemirror/addon/dialog/dialog.css
vendored
@@ -1,32 +0,0 @@
|
||||
.CodeMirror-dialog {
|
||||
position: absolute;
|
||||
left: 0; right: 0;
|
||||
background: inherit;
|
||||
z-index: 15;
|
||||
padding: .1em .8em;
|
||||
overflow: hidden;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-top {
|
||||
border-bottom: 1px solid #eee;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog-bottom {
|
||||
border-top: 1px solid #eee;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
width: 20em;
|
||||
color: inherit;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.CodeMirror-dialog button {
|
||||
font-size: 70%;
|
||||
}
|
||||
157
shared/codemirror/addon/dialog/dialog.js
vendored
157
shared/codemirror/addon/dialog/dialog.js
vendored
@@ -1,157 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
// Open simple dialogs on top of an editor. Relies on dialog.css.
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
function dialogDiv(cm, template, bottom) {
|
||||
var wrap = cm.getWrapperElement();
|
||||
var dialog;
|
||||
dialog = wrap.appendChild(document.createElement("div"));
|
||||
if (bottom)
|
||||
dialog.className = "CodeMirror-dialog CodeMirror-dialog-bottom";
|
||||
else
|
||||
dialog.className = "CodeMirror-dialog CodeMirror-dialog-top";
|
||||
|
||||
if (typeof template == "string") {
|
||||
dialog.innerHTML = template;
|
||||
} else { // Assuming it's a detached DOM element.
|
||||
dialog.appendChild(template);
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
function closeNotification(cm, newVal) {
|
||||
if (cm.state.currentNotificationClose)
|
||||
cm.state.currentNotificationClose();
|
||||
cm.state.currentNotificationClose = newVal;
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("openDialog", function(template, callback, options) {
|
||||
if (!options) options = {};
|
||||
|
||||
closeNotification(this, null);
|
||||
|
||||
var dialog = dialogDiv(this, template, options.bottom);
|
||||
var closed = false, me = this;
|
||||
function close(newVal) {
|
||||
if (typeof newVal == 'string') {
|
||||
inp.value = newVal;
|
||||
} else {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
|
||||
if (options.onClose) options.onClose(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
var inp = dialog.getElementsByTagName("input")[0], button;
|
||||
if (inp) {
|
||||
inp.focus();
|
||||
|
||||
if (options.value) {
|
||||
inp.value = options.value;
|
||||
if (options.selectValueOnOpen !== false) {
|
||||
inp.select();
|
||||
}
|
||||
}
|
||||
|
||||
if (options.onInput)
|
||||
CodeMirror.on(inp, "input", function(e) { options.onInput(e, inp.value, close);});
|
||||
if (options.onKeyUp)
|
||||
CodeMirror.on(inp, "keyup", function(e) {options.onKeyUp(e, inp.value, close);});
|
||||
|
||||
CodeMirror.on(inp, "keydown", function(e) {
|
||||
if (options && options.onKeyDown && options.onKeyDown(e, inp.value, close)) { return; }
|
||||
if (e.keyCode == 27 || (options.closeOnEnter !== false && e.keyCode == 13)) {
|
||||
inp.blur();
|
||||
CodeMirror.e_stop(e);
|
||||
close();
|
||||
}
|
||||
if (e.keyCode == 13) callback(inp.value, e);
|
||||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(inp, "blur", close);
|
||||
} else if (button = dialog.getElementsByTagName("button")[0]) {
|
||||
CodeMirror.on(button, "click", function() {
|
||||
close();
|
||||
me.focus();
|
||||
});
|
||||
|
||||
if (options.closeOnBlur !== false) CodeMirror.on(button, "blur", close);
|
||||
|
||||
button.focus();
|
||||
}
|
||||
return close;
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("openConfirm", function(template, callbacks, options) {
|
||||
closeNotification(this, null);
|
||||
var dialog = dialogDiv(this, template, options && options.bottom);
|
||||
var buttons = dialog.getElementsByTagName("button");
|
||||
var closed = false, me = this, blurring = 1;
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
me.focus();
|
||||
}
|
||||
buttons[0].focus();
|
||||
for (var i = 0; i < buttons.length; ++i) {
|
||||
var b = buttons[i];
|
||||
(function(callback) {
|
||||
CodeMirror.on(b, "click", function(e) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
close();
|
||||
if (callback) callback(me);
|
||||
});
|
||||
})(callbacks[i]);
|
||||
CodeMirror.on(b, "blur", function() {
|
||||
--blurring;
|
||||
setTimeout(function() { if (blurring <= 0) close(); }, 200);
|
||||
});
|
||||
CodeMirror.on(b, "focus", function() { ++blurring; });
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* openNotification
|
||||
* Opens a notification, that can be closed with an optional timer
|
||||
* (default 5000ms timer) and always closes on click.
|
||||
*
|
||||
* If a notification is opened while another is opened, it will close the
|
||||
* currently opened one and open the new one immediately.
|
||||
*/
|
||||
CodeMirror.defineExtension("openNotification", function(template, options) {
|
||||
closeNotification(this, close);
|
||||
var dialog = dialogDiv(this, template, options && options.bottom);
|
||||
var closed = false, doneTimer;
|
||||
var duration = options && typeof options.duration !== "undefined" ? options.duration : 5000;
|
||||
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
clearTimeout(doneTimer);
|
||||
dialog.parentNode.removeChild(dialog);
|
||||
}
|
||||
|
||||
CodeMirror.on(dialog, 'click', function(e) {
|
||||
CodeMirror.e_preventDefault(e);
|
||||
close();
|
||||
});
|
||||
|
||||
if (duration)
|
||||
doneTimer = setTimeout(close, duration);
|
||||
|
||||
return close;
|
||||
});
|
||||
});
|
||||
47
shared/codemirror/addon/display/autorefresh.js
vendored
47
shared/codemirror/addon/display/autorefresh.js
vendored
@@ -1,47 +0,0 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: http://codemirror.net/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"))
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod)
|
||||
else // Plain browser env
|
||||
mod(CodeMirror)
|
||||
})(function(CodeMirror) {
|
||||
"use strict"
|
||||
|
||||
CodeMirror.defineOption("autoRefresh", false, function(cm, val) {
|
||||
if (cm.state.autoRefresh) {
|
||||
stopListening(cm, cm.state.autoRefresh)
|
||||
cm.state.autoRefresh = null
|
||||
}
|
||||
if (val && cm.display.wrapper.offsetHeight == 0)
|
||||
startListening(cm, cm.state.autoRefresh = {delay: val.delay || 250})
|
||||
})
|
||||
|
||||
function startListening(cm, state) {
|
||||
function check() {
|
||||
if (cm.display.wrapper.offsetHeight) {
|
||||
stopListening(cm, state)
|
||||
if (cm.display.lastWrapHeight != cm.display.wrapper.clientHeight)
|
||||
cm.refresh()
|
||||
} else {
|
||||
state.timeout = setTimeout(check, state.delay)
|
||||
}
|
||||
}
|
||||
state.timeout = setTimeout(check, state.delay)
|
||||
state.hurry = function() {
|
||||
clearTimeout(state.timeout)
|
||||
state.timeout = setTimeout(check, 50)
|
||||
}
|
||||
CodeMirror.on(window, "mouseup", state.hurry)
|
||||
CodeMirror.on(window, "keyup", state.hurry)
|
||||
}
|
||||
|
||||
function stopListening(_cm, state) {
|
||||
clearTimeout(state.timeout)
|
||||
CodeMirror.off(window, "mouseup", state.hurry)
|
||||
CodeMirror.off(window, "keyup", state.hurry)
|
||||
}
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
.CodeMirror-fullscreen {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
height: auto;
|
||||
z-index: 9;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user