0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-27 13:53:09 +00:00

Compare commits

...

117 Commits

Author SHA1 Message Date
Scott Tolksdorf
20b719d0de Added in a function to sanatize script tags specifically 2018-12-03 09:55:49 +00:00
Scott Tolksdorf
8b04cc9269 Adding some more text to the default issue title to guide peopel to add an issue title 2018-11-27 10:38:29 +00:00
Marius
96466211c7 Make imgur link https in order to display image
Newer browsers warn and/or do not display content that gets loaded via HTTP if site is visited via HTTPS.
2018-11-26 20:13:25 -05:00
Kurtis Miller
34741291c7 Fix ESLint errors 2018-11-26 20:12:59 -05:00
Kurtis Miller
fcb0cd8ee7 Reset to previous commit 2018-11-26 20:12:59 -05:00
Kurtis Miller
aafac16af2 Add bold/italic shortcuts, reposition renderer warning 2018-11-26 20:12:59 -05:00
Trevor Buckner
911d1d4f9c Hide duplicate scrollbar in brewRenderer 2018-11-13 15:24:46 -05:00
Trevor Buckner
2cd7b44e12 Merge branch 'PRODUCTION' into master 2018-11-11 04:02:12 -05:00
Trevor Buckner
bfccd1d9e4 Wrap the text in the "source" page.
See #332.
2018-11-11 03:10:27 -05:00
Trevor Buckner
bf1bf6c191 Added spaces for lint... 2018-11-08 16:11:10 -05:00
Trevor Buckner
cc5cb677a1 Found the lint-friendly way to do it. Just needed to pass along the event data. 2018-11-08 16:11:10 -05:00
Trevor Buckner
2dcd7101f3 Fix metadata not working. Some earlier linting caused the handleFieldChange and handleSystem functions to not have access to event data. Returning these to their pre-lint state for now, but there might be a more lint-friendly way to do this later. 2018-11-08 16:11:10 -05:00
Trevor Buckner
f4e7e46a04 Added spaces for lint... 2018-11-08 16:04:14 -05:00
Trevor Buckner
8b3b7cb5aa Found the lint-friendly way to do it. Just needed to pass along the event data. 2018-11-08 16:04:14 -05:00
Trevor Buckner
77081b39b4 Fix metadata not working. Some earlier linting caused the handleFieldChange and handleSystem functions to not have access to event data. Returning these to their pre-lint state for now, but there might be a more lint-friendly way to do this later. 2018-11-08 16:04:14 -05:00
Trevor Buckner
a06236e3ff Fix regression with updated Pico-Router
This variable name changed with the v2.0.0 of Pico-Router and it wasn't changed here. This was causing every page to display a  `<div class="homePage page">` rather than the appropriate `editPage` or `sharePage`. It just wasn't immediately noticeable since `homePage` and `editPage` are almost identical. The problem is clearly visible on `sharePage` however.

`homebrew.jsx` was set to always render a defaultUrl of ` ` which creates a `<div class="homePage page">` by default, overwriting the expected `<div>` on for the current url.
2018-11-07 16:11:18 -05:00
Rae Che
c8585775be Change circleci mongo version to one that still exists 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
439cdfa1ea upgrading to mongoose v5 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
4c36f254c4 Removing picofluc for now 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
b6815593fd adding bithoundrc and updating pico-router 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
84054d1aae Updating some packages 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
6cd6b3c1c6 Upping version 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
907dbe4416 Updating to user create-react-class 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
47429d804a Upping the node ver on circleci 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
1d54ab49e6 Updating superagent 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
f634c451ff Adding in the missing repository field 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
778c0ca0b1 Updating build scripts and libraries 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
f4985b68ca Updating build scripts and libraries 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
c8875cff94 lint 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
40f1d4c790 Updating navbar link to go to the new subreddit 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
7818b6db46 Adding mongo image to the circleci config 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
4163e95938 Updating contributing and issue guides, added new verify npm task 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
fed0536673 Moving babelrc into package 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
a4b92af351 Adding in test case runner 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
4584b83c60 adding a circleCI task 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
c57ca0b27f Adding circleCI and eslint 2018-11-07 16:00:24 -05:00
Scott Tolksdorf
c04cd23213 Adding circleCI 2018-11-07 16:00:24 -05:00
Trevor Buckner
140f064786 Re-enable Box Shadows in PDF
Box shadows have been fixed in Chrome. No reason to hide them anymore.
2018-11-07 16:00:24 -05:00
Scott Tolksdorf
643f6f933d Fixing issue#413 class feature duping entire brew 2018-11-07 16:00:24 -05:00
Trevor Buckner
24aec1c649 Fixed typo "is" -> "in" 2018-11-07 15:15:07 -05:00
sprigunn
8caae18a12 Update README.md
Typos and additional license to sync up with http://homebrewery.naturalcrit.com.
2018-11-07 15:15:07 -05:00
Trevor Buckner
a1140f75d8 Fix border-image bug on PDFs
I think this might be caused by a new bug in Chrome or something since it seems to have popped up in GMBinder as well in the last couple weeks. See #683

Anyway, the error only occurs on the Class Table and Descriptive Text Box, which are also the only ones to use `border-image-repeat: round`. I changed it to `border-image-repeat: stretch` which solves the graphical issue and doesn't affect the appearance otherwise in this particular case.
2018-11-07 14:57:52 -05:00
Rae Che
3532d75365 s/let/const/ to satisfy ESLint 2018-08-24 10:12:53 -04:00
Rae2che5
ea81bb5ebc Add missing volume declaration. 2018-08-22 01:44:48 -07:00
Schemen
8d217f8785 Update according to change request 2018-08-22 01:44:48 -07:00
Schemen
d35db1f702 Add dockerization and example compose file 2018-08-22 01:44:48 -07:00
Rae Che
f6b058f3c9 brewRenderer: Capture event properties while still in event loop.
React [pools events](https://github.com/facebook/react/issues/2850),
which means that once the lambda runs later on the properties we want
to read will be null. Instead, we capture the properties in the event
loop.
2018-08-20 14:14:52 -04:00
Rae Che
5ab7c29b9d Change circleci mongo version to one that still exists 2018-08-20 13:20:05 -04:00
Rae Che
c5ecb9d57d ESLint reformatting 2018-05-31 10:53:07 -04:00
Rae Che
d0c473878a BrewSearch: Remove stale piece of React state 2018-05-31 10:53:07 -04:00
Rae Che
796df9a1ac EditPage: Remove stale bit of React state 2018-05-31 10:53:07 -04:00
Rae Che
9e8e403195 HomebrewAdmin: Remove stale piece of React state 2018-05-31 10:53:07 -04:00
Rae Che
a369871a06 ESLint reformatting 2018-05-31 10:53:07 -04:00
Rae Che
f2f45f3657 RenderWarnings: Remove dead code 2018-05-31 10:53:07 -04:00
Rae Che
e86ce5cf06 PrintPage: Fix potentially inconsistent React state update 2018-05-31 10:53:07 -04:00
Rae Che
8d73ff6833 BrewLookup: Mark links as noreferrer 2018-05-31 10:53:07 -04:00
Rae Che
c4397d34f8 BrewItem: Mark links as noreferrer 2018-05-31 10:53:07 -04:00
Rae Che
66c0c96a4f EditPage: Mark link as noreferrer 2018-05-31 10:53:07 -04:00
Rae Che
838b64c589 EditPage: Simplify hasChanges.
In particular, remove unreachable `return` statement.
2018-05-31 10:53:07 -04:00
Rae Che
730dde730c MetadataEditor: Mark link as noreferrer 2018-05-31 10:53:07 -04:00
Rae Che
d867aa7ce1 EditPage: Consistent trySave on handle[Metadata|Text]change
Previously, one of these happened in React's post-state-update callback,
while the other happened directly. They now both use the callback.
2018-05-31 10:53:07 -04:00
Rae Che
761597e71f EditPage: Fix potentially inconsistent React state updates 2018-05-31 10:53:07 -04:00
Rae Che
4bde5fcbf8 HomebrewAdmin: Mark links as noreferrer. 2018-05-31 10:53:07 -04:00
Rae Che
ff0aa56ddc HomebrewAdmin: Remove direct React state mutation. 2018-05-31 10:53:07 -04:00
Rae Che
74f92f3e44 Use noreferrer in recent-brew links. 2018-05-31 10:53:07 -04:00
Rae Che
84285f7359 Remove redundant expression. 2018-05-31 10:53:07 -04:00
Rae Che
ec0de7a408 BrewRenderer: Remove redundant errors state property.
The errors are in fact tracked in `this.props`, not `this.state`.
2018-05-31 10:53:07 -04:00
Rae Che
9ea99236ff Omit redundant property definition in brew renderer 2018-05-31 10:53:07 -04:00
Rae Che
f806328e75 Omit redundant property in eslintrc 2018-05-31 10:53:07 -04:00
Rae Che
15ac397b63 Fix inconsistent React state update.
Flagged by lgtm.com (as js/react/inconsistent-state-update).
2018-05-31 10:53:07 -04:00
Rae Che
b8105eb147 Config: Read MongoDB URL from config files too. 2018-05-29 11:04:57 -04:00
Trevor Buckner
31cbd9ef40 Fix regression with updated Pico-Router
This variable name changed with the v2.0.0 of Pico-Router and it wasn't changed here. This was causing every page to display a  `<div class="homePage page">` rather than the appropriate `editPage` or `sharePage`. It just wasn't immediately noticeable since `homePage` and `editPage` are almost identical. The problem is clearly visible on `sharePage` however.

`homebrew.jsx` was set to always render a defaultUrl of ` ` which creates a `<div class="homePage page">` by default, overwriting the expected `<div>` on for the current url.
2018-05-23 17:41:50 +01:00
Trevor Buckner
eaab6de691 Fix border-image bug on PDFs
I think this might be caused by a new bug in Chrome or something since it seems to have popped up in GMBinder as well in the last couple weeks. See #683

Anyway, the error only occurs on the Class Table and Descriptive Text Box, which are also the only ones to use `border-image-repeat: round`. I changed it to `border-image-repeat: stretch` which solves the graphical issue and doesn't affect the appearance otherwise in this particular case.
2018-05-22 10:01:03 +01:00
Trevor Buckner
d417c76c56 Remove unused event triggers
Removed `onMouseOver` and `onMouseOut` since they are no longer needed to trigger `will-change`
2018-05-14 08:30:19 +01:00
Trevor Buckner
2f15cc5611 Lint 2018-05-14 08:30:19 +01:00
Trevor Buckner
eb08172fb1 Move will-change to stylesheet
This makes `will-change: transform` permanent in the brewRenderer stylesheet rather than trying to load and unload it programatically.
2018-05-14 08:30:19 +01:00
Trevor Buckner
0cc87a4f0f Lint Fixes 2? 2018-05-14 08:30:19 +01:00
Trevor Buckner
004dc79eb2 Lint fixes 2018-05-14 08:30:19 +01:00
Trevor Buckner
a8a70c2d70 Unified spacing 2018-05-14 08:30:19 +01:00
Trevor Buckner
825c259fba Optimize for smooth scrolling of BrewRenderer
The will-change property allows the browser to optimize for smoother animations. This completely eliminates the scrolling stutter.
2018-05-14 08:30:19 +01:00
Rae2che5
cbbb7292d9 Revert "Fix duplicated text from Class Feature snippet"
This reverts commit 9519a0b4e4.
2018-05-11 20:44:53 +01:00
Trevor Buckner
9519a0b4e4 Fix duplicated text from Class Feature snippet
When adding the Class Feature snippet, all curent text in the document is inserted in place of the randomly generated class name, creating several duplicates of the document and generally creating a mess.  See #413 which was closed but the issue was never fixed.

Most of the snippet code generators do not have any input arguments in the `module.exports` function, so they don't have this issue. However, Table of Contents needs to parse the text in the brew, so it is passed a copy of the brew document. Unfortunately, Class Feature (classfeature.gen.js) also has an input parameter for when it is used as part of the Full Class snippet.  Thus, the unintended consequence occurs in snippetbar.jsx in the `execute` function.

This fix simply adds a check to the execute function and only passes in the brew as an argument if the clicked snipped actually is the Table of Contents. Some restructuring might later reveal a cleaner way to do this rather than an awkward special case check like this.
2018-05-11 10:20:18 +01:00
Eric Scheid
9dd16b6dd5 add page-break-inside to support Firefox, the laggard 2018-05-10 14:36:24 -04:00
Eric Scheid
d693301c37 Fix property name break-inside 2018-05-10 14:36:24 -04:00
Rae Che
fd5d142c16 Fix calculation of currently-viewed page.
Previously, this relied on a hard-coded constant determining the
height of a page. However, that's unnecessary -- we know the scroll
height of the window and the number of pages, so we can compute it.
2018-05-10 14:35:03 -04:00
Trevor Buckner
ee63d2d857 Fix duplicate table headers
Fixes a common issue. See #318.
2018-05-10 18:26:46 +01:00
Vincent Durmont
7c3946fb03 Some typos 2018-05-10 13:11:40 -04:00
Trevor Buckner
3e69e8c1aa Revert "Merge pull request #1 from calculuschild/Fix-Duplicated-Table-Headers"
This reverts commit 1a0bc1952c, reversing
changes made to 4f4ef908e0.
2018-05-08 21:33:34 -04:00
Trevor Buckner
1a0bc1952c Merge pull request #1 from calculuschild/Fix-Duplicated-Table-Headers
Fix Duplicated Table Header
2018-05-08 12:45:02 -04:00
Trevor Buckner
7e2c3381ae Fix Duplicated Table Header
Fixes a pretty common issue. See #318.
2018-05-08 12:42:11 -04:00
Trevor Buckner
4f4ef908e0 Merge branch 'master' of https://github.com/calculuschild/homebrewery 2018-05-08 12:39:50 -04:00
Trevor Buckner
3fe2360d92 Revert "Fix duplicated table headers"
This reverts commit 13a2a7efd2.
2018-05-08 12:36:30 -04:00
Trevor Buckner
13a2a7efd2 Fix duplicated table headers
Fixes a pretty common issue. See #318.
2018-05-08 12:33:36 -04:00
Trevor Buckner
073fb73bde Wrap the text in the "source" page.
See #332.
2018-05-07 10:56:23 +01:00
Trevor Buckner
847615ef8e Wrap the text in the "source" page.
See #332.
2018-04-17 21:03:50 -04:00
Scott Tolksdorf
3583c2e776 upgrading to mongoose v5 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
2a753ccc7c Removing picofluc for now 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
55c529473a adding bithoundrc and updating pico-router 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
164f646e08 Updating some packages 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
6f4962926c Upping version 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
f18a181e2e Updating to user create-react-class 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
c4bff6afa0 Upping the node ver on circleci 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
63e8a0d9b7 Updating superagent 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
ce3fda683b Adding in the missing repository field 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
9594b73b0d Updating build scripts and libraries 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
78ad4bea09 Updating build scripts and libraries 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
ed1b5252be lint 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
bf27250990 Updating navbar link to go to the new subreddit 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
bab11d692e Adding mongo image to the circleci config 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
3350b04f64 Updating contributing and issue guides, added new verify npm task 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
5ebba25183 Moving babelrc into package 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
8f77ac9e56 Adding in test case runner 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
3f728e7993 adding a circleCI task 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
3f22572f98 Adding circleCI and eslint 2018-04-09 00:12:56 -04:00
Scott Tolksdorf
a52f628fdf Adding circleCI 2018-04-09 00:12:56 -04:00
Trevor Buckner
fe63133d7c Re-enable Box Shadows in PDF
Box shadows have been fixed in Chrome. No reason to hide them anymore.
2018-04-08 23:23:49 -04:00
Scott Tolksdorf
3247cab214 Fixing issue#413 class feature duping entire brew 2017-06-04 16:07:49 -04:00
72 changed files with 11374 additions and 3011 deletions

View File

@@ -1,6 +0,0 @@
{
"presets": [
"env",
"react"
]
}

7
.bithoundrc Normal file
View File

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

33
.circleci/config.yml Normal file
View File

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

6
.dockerignore Normal file
View File

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

77
.eslintrc.js Normal file
View File

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

1
.gitattributes vendored Normal file
View File

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

View File

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

14
.gitignore vendored
View File

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

14
Dockerfile Normal file
View File

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

View File

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

View File

@@ -1,21 +1,21 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
const cx = require('classnames');
var HomebrewAdmin = require('./homebrewAdmin/homebrewAdmin.jsx'); const HomebrewAdmin = require('./homebrewAdmin/homebrewAdmin.jsx');
var Admin = React.createClass({ const Admin = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
url : "", url : '',
admin_key : "", admin_key : '',
homebrews : [], homebrews : [],
}; };
}, },
render : function(){ render : function(){
var self = this; return (
return(
<div className='admin'> <div className='admin'>
<header> <header>

View File

@@ -1,4 +1,5 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -6,36 +7,36 @@ const request = require('superagent');
const Moment = require('moment'); const Moment = require('moment');
const BrewLookup = React.createClass({ const BrewLookup = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
adminKey : '', adminKey : '',
}; };
}, },
getInitialState: function() { getInitialState : function() {
return { return {
query:'', query : '',
resultBrew : null, resultBrew : null,
searching : false searching : false
}; };
}, },
handleChange : function(e){ handleChange : function(e){
this.setState({ this.setState({
query : e.target.value query : e.target.value
}) });
}, },
lookup : function(){ lookup : function(){
this.setState({ searching : true }); this.setState({ searching: true });
request.get(`/admin/lookup/${this.state.query}`) request.get(`/admin/lookup/${this.state.query}`)
.query({ admin_key : this.props.adminKey }) .query({ admin_key: this.props.adminKey })
.end((err, res) => { .end((err, res)=>{
this.setState({ this.setState({
searching : false, searching : false,
resultBrew : (err ? null : res.body) resultBrew : (err ? null : res.body)
}); });
}) });
}, },
renderFoundBrew : function(){ renderFoundBrew : function(){
@@ -46,21 +47,21 @@ const BrewLookup = React.createClass({
return <div className='brewRow'> return <div className='brewRow'>
<div>{brew.title}</div> <div>{brew.title}</div>
<div>{brew.authors.join(', ')}</div> <div>{brew.authors.join(', ')}</div>
<div><a href={'/edit/' + brew.editId} target='_blank'>/edit/{brew.editId}</a></div> <div><a href={`/edit/${brew.editId}`} target='_blank' rel='noopener noreferrer'>/edit/{brew.editId}</a></div>
<div><a href={'/share/' + brew.shareId} target='_blank'>/share/{brew.shareId}</a></div> <div><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>/share/{brew.shareId}</a></div>
<div>{Moment(brew.updatedAt).fromNow()}</div> <div>{Moment(brew.updatedAt).fromNow()}</div>
<div>{brew.views}</div> <div>{brew.views}</div>
</div> </div>;
}, },
render: function(){ render : function(){
return <div className='brewLookup'> return <div className='brewLookup'>
<h1>Brew Lookup</h1> <h1>Brew Lookup</h1>
<input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id...' /> <input type='text' value={this.state.query} onChange={this.handleChange} placeholder='edit or share id...' />
<button onClick={this.lookup}><i className='fa fa-search'/></button> <button onClick={this.lookup}><i className='fa fa-search'/></button>
{this.renderFoundBrew()} {this.renderFoundBrew()}
</div> </div>;
} }
}); });

View File

@@ -1,32 +1,28 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
const cx = require('classnames');
var request = require('superagent'); const request = require('superagent');
var BrewSearch = React.createClass({ const BrewSearch = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
admin_key : '' admin_key : ''
}; };
}, },
getInitialState: function() { getInitialState : function() {
return { return {
searchTerm: '', searchTerm : '',
brew : null, brew : null,
searching : false
}; };
}, },
search : function(){ search : function(){
this.setState({ request.get(`/homebrew/api/search?id=${this.state.searchTerm}`)
searching : true
});
request.get('/homebrew/api/search?id=' + this.state.searchTerm)
.query({ .query({
admin_key : this.props.admin_key, admin_key : this.props.admin_key,
}) })
@@ -34,9 +30,7 @@ var BrewSearch = React.createClass({
console.log(err, res, res.body.brews[0]); console.log(err, res, res.body.brews[0]);
this.setState({ this.setState({
brew : res.body.brews[0], brew : res.body.brews[0],
});
searching : false
})
}); });
}, },
@@ -54,7 +48,7 @@ var BrewSearch = React.createClass({
return <div className='brew'> return <div className='brew'>
<div>Edit id : {this.state.brew.editId}</div> <div>Edit id : {this.state.brew.editId}</div>
<div>Share id : {this.state.brew.shareId}</div> <div>Share id : {this.state.brew.shareId}</div>
</div> </div>;
}, },
render : function(){ render : function(){
@@ -64,7 +58,7 @@ var BrewSearch = React.createClass({
<button onClick={this.handleSearchClick}>Search</button> <button onClick={this.handleSearchClick}>Search</button>
{this.renderBrew()} {this.renderBrew()}
</div> </div>;
}, },
}); });

View File

@@ -1,30 +1,28 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
var request = require('superagent'); const cx = require('classnames');
const request = require('superagent');
var Moment = require('moment'); const Moment = require('moment');
var BrewSearch = require('./brewSearch.jsx');
var BrewLookup = require('./brewLookup/brewLookup.jsx');
var HomebrewAdmin = React.createClass({ const BrewLookup = require('./brewLookup/brewLookup.jsx');
getDefaultProps: function() {
const HomebrewAdmin = createClass({
getDefaultProps : function() {
return { return {
admin_key : '' admin_key : ''
}; };
}, },
getInitialState: function() { getInitialState : function() {
return { return {
page: 0, page : 0,
count : 20, count : 20,
brewCache : {}, brewCache : {},
total : 0, total : 0,
processingOldBrews : false
}; };
}, },
@@ -33,21 +31,22 @@ var HomebrewAdmin = React.createClass({
request.get('/api/search') request.get('/api/search')
.query({ .query({
admin_key : this.props.admin_key, admin_key : this.props.admin_key,
count : this.state.count, count : this.state.count,
page : page page : page
}) })
.end((err, res)=>{ .end((err, res)=>{
if(err || !res.body || !res.body.brews) return; if(err || !res.body || !res.body.brews) return;
this.state.brewCache[page] = res.body.brews; const newCache = _.extend({}, this.state.brewCache);
newCache[page] = res.body.brews;
this.setState({ this.setState({
brewCache : this.state.brewCache, brewCache : newCache,
total : res.body.total, total : res.body.total,
count : res.body.count count : res.body.count
}) });
}) });
}, },
componentDidMount: function() { componentDidMount : function() {
this.fetchBrews(this.state.page); this.fetchBrews(this.state.page);
}, },
@@ -57,31 +56,31 @@ var HomebrewAdmin = React.createClass({
} }
this.setState({ this.setState({
page : page page : page
}) });
}, },
clearInvalidBrews : function(){ clearInvalidBrews : function(){
request.get('/api/invalid') request.get('/api/invalid')
.query({admin_key : this.props.admin_key}) .query({ admin_key: this.props.admin_key })
.end((err, res)=>{ .end((err, res)=>{
if(!confirm("This will remove " + res.body.count + " brews. Are you sure?")) return; if(!confirm(`This will remove ${res.body.count} brews. Are you sure?`)) return;
request.get('/api/invalid') request.get('/api/invalid')
.query({admin_key : this.props.admin_key, do_it : true}) .query({ admin_key: this.props.admin_key, do_it: true })
.end((err, res)=>{ .end((err, res)=>{
alert("Done!") alert('Done!');
}); });
}); });
}, },
deleteBrew : function(brewId){ deleteBrew : function(brewId){
if(!confirm("Are you sure you want to delete '" + brewId + "'?")) return; if(!confirm(`Are you sure you want to delete '${brewId}'?`)) return;
request.get('/api/remove/' + brewId) request.get(`/api/remove/${brewId}`)
.query({admin_key : this.props.admin_key}) .query({ admin_key: this.props.admin_key })
.end(function(err, res){ .end(function(err, res){
window.location.reload(); window.location.reload();
}) });
}, },
handlePageChange : function(dir){ handlePageChange : function(dir){
@@ -90,34 +89,34 @@ var HomebrewAdmin = React.createClass({
renderPagnination : function(){ renderPagnination : function(){
var outOf; let outOf;
if(this.state.total){ if(this.state.total){
outOf = this.state.page + ' / ' + Math.round(this.state.total/this.state.count); outOf = `${this.state.page} / ${Math.round(this.state.total/this.state.count)}`;
} }
return <div className='pagnination'> return <div className='pagnination'>
<i className='fa fa-chevron-left' onClick={this.handlePageChange.bind(this, -1)}/> <i className='fa fa-chevron-left' onClick={()=>this.handlePageChange(-1)}/>
{outOf} {outOf}
<i className='fa fa-chevron-right' onClick={this.handlePageChange.bind(this, 1)}/> <i className='fa fa-chevron-right' onClick={()=>this.handlePageChange(1)}/>
</div> </div>;
}, },
renderBrews : function(){ renderBrews : function(){
var brews = this.state.brewCache[this.state.page] || _.times(this.state.count); const brews = this.state.brewCache[this.state.page] || _.times(this.state.count);
return _.map(brews, (brew)=>{ return _.map(brews, (brew)=>{
return <tr className={cx('brewRow', {'isEmpty' : brew.text == "false"})} key={brew.shareId || brew}> return <tr className={cx('brewRow', { 'isEmpty': brew.text == 'false' })} key={brew.shareId || brew}>
<td><a href={'/edit/' + brew.editId} target='_blank'>{brew.editId}</a></td> <td><a href={`/edit/${brew.editId}`} target='_blank' rel='noopener noreferrer'>{brew.editId}</a></td>
<td><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></td> <td><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>{brew.shareId}</a></td>
<td>{Moment(brew.createdAt).fromNow()}</td> <td>{Moment(brew.createdAt).fromNow()}</td>
<td>{Moment(brew.updatedAt).fromNow()}</td> <td>{Moment(brew.updatedAt).fromNow()}</td>
<td>{Moment(brew.lastViewed).fromNow()}</td> <td>{Moment(brew.lastViewed).fromNow()}</td>
<td>{brew.views}</td> <td>{brew.views}</td>
<td> <td>
<div className='deleteButton' onClick={this.deleteBrew.bind(this, brew.editId)}> <div className='deleteButton' onClick={()=>this.deleteBrew(brew.editId)}>
<i className='fa fa-trash' /> <i className='fa fa-trash' />
</div> </div>
</td> </td>
</tr> </tr>;
}); });
}, },
@@ -138,11 +137,10 @@ var HomebrewAdmin = React.createClass({
{this.renderBrews()} {this.renderBrews()}
</tbody> </tbody>
</table> </table>
</div> </div>;
}, },
render : function(){ render : function(){
var self = this;
return <div className='homebrewAdmin'> return <div className='homebrewAdmin'>
<BrewLookup adminKey={this.props.admin_key} /> <BrewLookup adminKey={this.props.admin_key} />
@@ -165,7 +163,7 @@ var HomebrewAdmin = React.createClass({
<BrewSearch admin_key={this.props.admin_key} /> <BrewSearch admin_key={this.props.admin_key} />
*/} */}
</div> </div>;
} }
}); });

View File

@@ -1,4 +1,5 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -6,77 +7,67 @@ const Markdown = require('naturalcrit/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx'); const ErrorBar = require('./errorBar/errorBar.jsx');
//TODO: move to the brew renderer //TODO: move to the brew renderer
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx') const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50; const PPR_THRESHOLD = 50;
const BrewRenderer = React.createClass({ const BrewRenderer = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
text : '', text : '',
errors : [] errors : []
}; };
}, },
getInitialState: function() { getInitialState : function() {
const pages = this.props.text.split('\\page'); const pages = this.props.text.split('\\page');
return { return {
viewablePageNumber: 0, viewablePageNumber : 0,
height : 0, height : 0,
isMounted : false, isMounted : false,
usePPR : true, pages : pages,
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD, usePPR : pages.length >= PPR_THRESHOLD,
errors : []
}; };
}, },
height : 0, height : 0,
pageHeight : PAGE_HEIGHT,
lastRender : <div></div>, lastRender : <div></div>,
componentDidMount: function() { componentDidMount : function() {
this.updateSize(); this.updateSize();
window.addEventListener("resize", this.updateSize); window.addEventListener('resize', this.updateSize);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
window.removeEventListener("resize", this.updateSize); window.removeEventListener('resize', this.updateSize);
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps : function(nextProps) {
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
const pages = nextProps.text.split('\\page'); const pages = nextProps.text.split('\\page');
this.setState({ this.setState({
pages : pages, pages : pages,
usePPR : pages.length >= PPR_THRESHOLD usePPR : pages.length >= PPR_THRESHOLD
}) });
}, },
updateSize : function() { updateSize : function() {
setTimeout(()=>{
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
}, 1);
this.setState({ this.setState({
height : this.refs.main.parentNode.clientHeight, height : this.refs.main.parentNode.clientHeight,
isMounted : true isMounted : true
}); });
}, },
handleScroll : function(e){ handleScroll : function(e){
this.setState({ const target = e.target;
viewablePageNumber : Math.floor(e.target.scrollTop / this.pageHeight) this.setState((prevState)=>({
}); viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
}));
}, },
shouldRender : function(pageText, index){ shouldRender : function(pageText, index){
if(!this.state.isMounted) return false; if(!this.state.isMounted) return false;
var viewIndex = this.state.viewablePageNumber; const viewIndex = this.state.viewablePageNumber;
if(index == viewIndex - 3) return true; if(index == viewIndex - 3) return true;
if(index == viewIndex - 2) return true; if(index == viewIndex - 2) return true;
if(index == viewIndex - 1) return true; if(index == viewIndex - 1) return true;
@@ -94,7 +85,7 @@ const BrewRenderer = React.createClass({
renderPageInfo : function(){ renderPageInfo : function(){
return <div className='pageInfo'> return <div className='pageInfo'>
{this.state.viewablePageNumber + 1} / {this.state.pages.length} {this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div> </div>;
}, },
renderPPRmsg : function(){ renderPPRmsg : function(){
@@ -102,17 +93,17 @@ const BrewRenderer = React.createClass({
return <div className='ppr_msg'> return <div className='ppr_msg'>
Partial Page Renderer enabled, because your brew is so large. May effect rendering. Partial Page Renderer enabled, because your brew is so large. May effect rendering.
</div> </div>;
}, },
renderDummyPage : function(index){ renderDummyPage : function(index){
return <div className='phb' id={`p${index + 1}`} key={index}> return <div className='phb' id={`p${index + 1}`} key={index}>
<i className='fa fa-spinner fa-spin' /> <i className='fa fa-spinner fa-spin' />
</div> </div>;
}, },
renderPage : function(pageText, index){ renderPage : function(pageText, index){
return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{__html:Markdown.render(pageText)}} key={index} /> return <div className='phb' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
}, },
renderPages : function(){ renderPages : function(){
@@ -120,7 +111,7 @@ const BrewRenderer = React.createClass({
return _.map(this.state.pages, (page, index)=>{ return _.map(this.state.pages, (page, index)=>{
if(this.shouldRender(page, index)){ if(this.shouldRender(page, index)){
return this.renderPage(page, index); return this.renderPage(page, index);
}else{ } else {
return this.renderDummyPage(index); return this.renderDummyPage(index);
} }
}); });
@@ -133,20 +124,24 @@ const BrewRenderer = React.createClass({
}, },
render : function(){ render : function(){
return <div className='brewRenderer' return (
onScroll={this.handleScroll} <React.Fragment>
ref='main' <div className='brewRenderer'
style={{height : this.state.height}}> onScroll={this.handleScroll}
ref='main'
style={{ height: this.state.height }}>
<ErrorBar errors={this.props.errors} /> <ErrorBar errors={this.props.errors} />
<RenderWarnings /> <RenderWarnings />
<div className='pages' ref='pages'> <div className='pages' ref='pages'>
{this.renderPages()} {this.renderPages()}
</div> </div>
{this.renderPageInfo()} </div>;
{this.renderPPRmsg()} {this.renderPageInfo()}
</div> {this.renderPPRmsg()}
</React.Fragment>
);
} }
}); });

View File

@@ -4,29 +4,8 @@
position : relative; position : relative;
} }
.brewRenderer{ .brewRenderer{
overflow-y : scroll; will-change : transform;
.pageInfo{ overflow-y : scroll;
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}
.ppr_msg{
position : absolute;
left : 0px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}
.pages{ .pages{
margin : 30px 0px; margin : 30px 0px;
&>.phb{ &>.phb{
@@ -37,3 +16,25 @@
} }
} }
} }
.pageInfo{
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}
.ppr_msg{
position : absolute;
left : 0px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}

View File

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

View File

@@ -1,4 +1,5 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -13,33 +14,33 @@ const splice = function(str, index, inject){
const SNIPPETBAR_HEIGHT = 25; const SNIPPETBAR_HEIGHT = 25;
const Editor = React.createClass({ const Editor = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
value : '', value : '',
onChange : ()=>{}, onChange : ()=>{},
metadata : {}, metadata : {},
onMetadataChange : ()=>{}, onMetadataChange : ()=>{},
}; };
}, },
getInitialState: function() { getInitialState : function() {
return { return {
showMetadataEditor: false showMetadataEditor : false
}; };
}, },
cursorPosition : { cursorPosition : {
line : 0, line : 0,
ch : 0 ch : 0
}, },
componentDidMount: function() { componentDidMount : function() {
this.updateEditorSize(); this.updateEditorSize();
this.highlightPageLines(); this.highlightPageLines();
window.addEventListener("resize", this.updateEditorSize); window.addEventListener('resize', this.updateEditorSize);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
window.removeEventListener("resize", this.updateEditorSize); window.removeEventListener('resize', this.updateEditorSize);
}, },
updateEditorSize : function() { updateEditorSize : function() {
@@ -64,12 +65,12 @@ const Editor = React.createClass({
handgleToggle : function(){ handgleToggle : function(){
this.setState({ this.setState({
showMetadataEditor : !this.state.showMetadataEditor showMetadataEditor : !this.state.showMetadataEditor
}) });
}, },
getCurrentPage : function(){ getCurrentPage : function(){
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1); const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1);
return _.reduce(lines, (r, line) => { return _.reduce(lines, (r, line)=>{
if(line.indexOf('\\page') !== -1) r++; if(line.indexOf('\\page') !== -1) r++;
return r; return r;
}, 1); }, 1);
@@ -86,13 +87,13 @@ const Editor = React.createClass({
} }
return r; return r;
}, []); }, []);
return lineNumbers return lineNumbers;
}, },
brewJump : function(){ brewJump : function(){
const currentPage = this.getCurrentPage(); const currentPage = this.getCurrentPage();
window.location.hash = 'p' + currentPage; window.location.hash = `p${currentPage}`;
}, },
//Called when there are changes to the editor's dimensions //Called when there are changes to the editor's dimensions
@@ -105,12 +106,12 @@ const Editor = React.createClass({
return <MetadataEditor return <MetadataEditor
metadata={this.props.metadata} metadata={this.props.metadata}
onChange={this.props.onMetadataChange} onChange={this.props.onMetadataChange}
/> />;
}, },
render : function(){ render : function(){
this.highlightPageLines(); this.highlightPageLines();
return( return (
<div className='editor' ref='main'> <div className='editor' ref='main'>
<SnippetBar <SnippetBar
brew={this.props.value} brew={this.props.value}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -13,76 +14,76 @@ const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx'); const PrintPage = require('./pages/printPage/printPage.jsx');
let Router; let Router;
const Homebrew = React.createClass({ const Homebrew = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
url : '', url : '',
welcomeText : '', welcomeText : '',
changelog : '', changelog : '',
version : '0.0.0', version : '0.0.0',
account : null, account : null,
brew : { brew : {
title : '', title : '',
text : '', text : '',
shareId : null, shareId : null,
editId : null, editId : null,
createdAt : null, createdAt : null,
updatedAt : null, updatedAt : null,
} }
}; };
}, },
componentWillMount: function() { componentWillMount : function() {
global.account = this.props.account; global.account = this.props.account;
global.version = this.props.version; global.version = this.props.version;
Router = CreateRouter({ Router = CreateRouter({
'/edit/:id' : (args) => { '/edit/:id' : (args)=>{
if(!this.props.brew.editId){ if(!this.props.brew.editId){
return <ErrorPage errorId={args.id}/> return <ErrorPage errorId={args.id}/>;
} }
return <EditPage return <EditPage
id={args.id} id={args.id}
brew={this.props.brew} /> brew={this.props.brew} />;
}, },
'/share/:id' : (args) => { '/share/:id' : (args)=>{
if(!this.props.brew.shareId){ if(!this.props.brew.shareId){
return <ErrorPage errorId={args.id}/> return <ErrorPage errorId={args.id}/>;
} }
return <SharePage return <SharePage
id={args.id} id={args.id}
brew={this.props.brew} /> brew={this.props.brew} />;
}, },
'/user/:username' : (args) => { '/user/:username' : (args)=>{
return <UserPage return <UserPage
username={args.username} username={args.username}
brews={this.props.brews} brews={this.props.brews}
/> />;
}, },
'/print/:id' : (args, query) => { '/print/:id' : (args, query)=>{
return <PrintPage brew={this.props.brew} query={query}/>; return <PrintPage brew={this.props.brew} query={query}/>;
}, },
'/print' : (args, query) => { '/print' : (args, query)=>{
return <PrintPage query={query}/>; return <PrintPage query={query}/>;
}, },
'/new' : (args) => { '/new' : (args)=>{
return <NewPage /> return <NewPage />;
}, },
'/changelog' : (args) => { '/changelog' : (args)=>{
return <SharePage return <SharePage
brew={{title : 'Changelog', text : this.props.changelog}} /> brew={{ title: 'Changelog', text: this.props.changelog }} />;
}, },
'*' : <HomePage '*' : <HomePage
welcomeText={this.props.welcomeText} />, welcomeText={this.props.welcomeText} />,
}); });
}, },
render : function(){ render : function(){
return <div className='homebrew'> return <div className='homebrew'>
<Router initialUrl={this.props.url}/> <Router defaultUrl={this.props.url}/>
</div> </div>;
} }
}); });

View File

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

View File

@@ -1,15 +1,16 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
var Nav = require('naturalcrit/nav/nav.jsx'); const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const MAX_TITLE_LENGTH = 50; const MAX_TITLE_LENGTH = 50;
var EditTitle = React.createClass({ const EditTitle = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
title : '', title : '',
onChange : function(){} onChange : function(){}
}; };
}, },
@@ -22,10 +23,10 @@ var EditTitle = React.createClass({
return <Nav.item className='editTitle'> return <Nav.item className='editTitle'>
<input placeholder='Brew Title' type='text' value={this.props.title} onChange={this.handleChange} /> <input placeholder='Brew Title' type='text' value={this.props.title} onChange={this.handleChange} />
<div className={cx('charCount', {'max' : this.props.title.length >= MAX_TITLE_LENGTH})}> <div className={cx('charCount', { 'max': this.props.title.length >= MAX_TITLE_LENGTH })}>
{this.props.title.length}/{MAX_TITLE_LENGTH} {this.props.title.length}/{MAX_TITLE_LENGTH}
</div> </div>
</Nav.item> </Nav.item>;
}, },
}); });

View File

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

View File

@@ -1,25 +1,26 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = React.createClass({ const Navbar = createClass({
getInitialState: function() { getInitialState : function() {
return { return {
//showNonChromeWarning : false, //showNonChromeWarning : false,
ver : '0.0.0' ver : '0.0.0'
}; };
}, },
componentDidMount: function() { componentDidMount : function() {
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); //const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
this.setState({ this.setState({
//showNonChromeWarning : !isChrome, //showNonChromeWarning : !isChrome,
ver : window.version ver : window.version
}) });
}, },
/* /*
renderChromeWarning : function(){ renderChromeWarning : function(){
if(!this.state.showNonChromeWarning) return; if(!this.state.showNonChromeWarning) return;
return <Nav.item className='warning' icon='fa-exclamation-triangle'> return <Nav.item className='warning' icon='fa-exclamation-triangle'>
@@ -42,7 +43,7 @@ const Navbar = React.createClass({
{/*this.renderChromeWarning()*/} {/*this.renderChromeWarning()*/}
</Nav.section> </Nav.section>
{this.props.children} {this.props.children}
</Nav.base> </Nav.base>;
} }
}); });

View File

@@ -1,5 +1,6 @@
var React = require('react'); const React = require('react');
var Nav = require('naturalcrit/nav/nav.jsx'); const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){ module.exports = function(props){
return <Nav.item return <Nav.item
@@ -9,5 +10,5 @@ module.exports = function(props){
color='green' color='green'
icon='fa-heart'> icon='fa-heart'>
help out help out
</Nav.item> </Nav.item>;
}; };

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require("superagent"); const request = require('superagent');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
@@ -20,40 +21,39 @@ const Markdown = require('naturalcrit/markdown.js');
const SAVE_TIMEOUT = 3000; const SAVE_TIMEOUT = 3000;
const EditPage = React.createClass({ const EditPage = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
brew : { brew : {
text : '', text : '',
shareId : null, shareId : null,
editId : null, editId : null,
createdAt : null, createdAt : null,
updatedAt : null, updatedAt : null,
title : '', title : '',
description : '', description : '',
tags : '', tags : '',
published : false, published : false,
authors : [], authors : [],
systems : [] systems : []
} }
}; };
}, },
getInitialState: function() { getInitialState : function() {
return { return {
brew : this.props.brew, brew : this.props.brew,
isSaving : false, isSaving : false,
isPending : false, isPending : false,
errors : null, errors : null,
htmlErrors : Markdown.validate(this.props.brew.text), htmlErrors : Markdown.validate(this.props.brew.text),
lastUpdated : this.props.brew.updatedAt
}; };
}, },
savedBrew : null, savedBrew : null,
componentDidMount: function(){ componentDidMount : function(){
this.trySave(); this.trySave();
window.onbeforeunload = ()=>{ window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){ if(this.state.isSaving || this.state.isPending){
@@ -61,13 +61,13 @@ const EditPage = React.createClass({
} }
}; };
this.setState({ this.setState((prevState)=>({
htmlErrors : Markdown.validate(this.state.brew.text) htmlErrors : Markdown.validate(prevState.brew.text)
}) }));
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
window.onbeforeunload = function(){}; window.onbeforeunload = function(){};
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
@@ -90,44 +90,36 @@ const EditPage = React.createClass({
}, },
handleMetadataChange : function(metadata){ handleMetadataChange : function(metadata){
this.setState({ this.setState((prevState)=>({
brew : _.merge({}, this.state.brew, metadata), brew : _.merge({}, prevState.brew, metadata),
isPending : true, isPending : true,
}, ()=>{ }), ()=>this.trySave());
this.trySave();
});
}, },
handleTextChange : function(text){ handleTextChange : function(text){
//If there are errors, run the validator on everychange to give quick feedback //If there are errors, run the validator on everychange to give quick feedback
var htmlErrors = this.state.htmlErrors; let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text); if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState({ this.setState((prevState)=>({
brew : _.merge({}, this.state.brew, {text : text}), brew : _.merge({}, prevState.brew, { text: text }),
isPending : true, isPending : true,
htmlErrors : htmlErrors htmlErrors : htmlErrors
}); }), ()=>this.trySave());
this.trySave();
}, },
hasChanges : function(){ hasChanges : function(){
if(this.savedBrew){ const savedBrew = this.savedBrew ? this.savedBrew : this.props.brew;
return !_.isEqual(this.state.brew, this.savedBrew) return !_.isEqual(this.state.brew, savedBrew);
}else{
return !_.isEqual(this.state.brew, this.props.brew)
}
return false;
}, },
trySave : function(){ trySave : function(){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT); if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){ if(this.hasChanges()){
this.debounceSave(); this.debounceSave();
}else{ } else {
this.debounceSave.cancel(); this.debounceSave.cancel();
} }
}, },
@@ -135,58 +127,58 @@ const EditPage = React.createClass({
save : function(){ save : function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel(); if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
this.setState({ this.setState((prevState)=>({
isSaving : true, isSaving : true,
errors : null, errors : null,
htmlErrors : Markdown.validate(this.state.brew.text) htmlErrors : Markdown.validate(prevState.brew.text)
}); }));
request request
.put('/api/update/' + this.props.brew.editId) .put(`/api/update/${this.props.brew.editId}`)
.send(this.state.brew) .send(this.state.brew)
.end((err, res) => { .end((err, res)=>{
if(err){ if(err){
this.setState({ this.setState({
errors : err, errors : err,
}) });
}else{ } else {
this.savedBrew = res.body; this.savedBrew = res.body;
this.setState({ this.setState({
isPending : false, isPending : false,
isSaving : false, isSaving : false,
lastUpdated : res.body.updatedAt });
})
} }
}) });
}, },
renderSaveButton : function(){ renderSaveButton : function(){
if(this.state.errors){ if(this.state.errors){
var errMsg = ''; let errMsg = '';
try{ try {
errMsg += this.state.errors.toString() + '\n\n'; errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```'; errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
}catch(e){} } catch (e){}
return <Nav.item className='save error' icon="fa-warning"> return <Nav.item className='save error' icon='fa-warning'>
Oops! Oops!
<div className='errorContainer'> <div className='errorContainer'>
Looks like there was a problem saving. <br /> Looks like there was a problem saving. <br />
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}> Report the issue <a target='_blank' rel='noopener noreferrer'
href={`https://github.com/stolksdorf/naturalcrit/issues/new?body=${encodeURIComponent(errMsg)}`}>
here here
</a>. </a>.
</div> </div>
</Nav.item> </Nav.item>;
} }
if(this.state.isSaving){ if(this.state.isSaving){
return <Nav.item className='save' icon="fa-spinner fa-spin">saving...</Nav.item> return <Nav.item className='save' icon='fa-spinner fa-spin'>saving...</Nav.item>;
} }
if(this.state.isPending && this.hasChanges()){ if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item> return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>;
} }
if(!this.state.isPending && !this.state.isSaving){ if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item> return <Nav.item className='save saved'>saved.</Nav.item>;
} }
}, },
renderNavbar : function(){ renderNavbar : function(){
@@ -198,13 +190,13 @@ const EditPage = React.createClass({
{this.renderSaveButton()} {this.renderSaveButton()}
{/*<RecentlyEdited brew={this.props.brew} />*/} {/*<RecentlyEdited brew={this.props.brew} />*/}
<ReportIssue /> <ReportIssue />
<Nav.item newTab={true} href={'/share/' + this.props.brew.shareId} color='teal' icon='fa-share-alt'> <Nav.item newTab={true} href={`/share/${this.props.brew.shareId}`} color='teal' icon='fa-share-alt'>
Share Share
</Nav.item> </Nav.item>
<PrintLink shareId={this.props.brew.shareId} /> <PrintLink shareId={this.props.brew.shareId} />
<Account /> <Account />
</Nav.section> </Nav.section>
</Navbar> </Navbar>;
}, },
render : function(){ render : function(){
@@ -223,7 +215,7 @@ const EditPage = React.createClass({
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} /> <BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} />
</SplitPane> </SplitPane>
</div> </div>
</div> </div>;
} }
}); });

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require("superagent"); const request = require('superagent');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx'); const Navbar = require('../../navbar/navbar.jsx');
@@ -17,16 +18,18 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const HomePage = React.createClass({ const HomePage = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
welcomeText : '', welcomeText : '',
ver : '0.0.0' ver : '0.0.0'
}; };
}, },
getInitialState: function() { getInitialState : function() {
return { return {
text: this.props.welcomeText text : this.props.welcomeText
}; };
}, },
handleSave : function(){ handleSave : function(){
@@ -36,8 +39,8 @@ const HomePage = React.createClass({
}) })
.end((err, res)=>{ .end((err, res)=>{
if(err) return; if(err) return;
var brew = res.body; const brew = res.body;
window.location = '/edit/' + brew.editId; window.location = `/edit/${brew.editId}`;
}); });
}, },
handleSplitMove : function(){ handleSplitMove : function(){
@@ -64,7 +67,7 @@ const HomePage = React.createClass({
</Nav.item> </Nav.item>
*/} */}
</Nav.section> </Nav.section>
</Navbar> </Navbar>;
}, },
render : function(){ render : function(){
@@ -78,14 +81,14 @@ const HomePage = React.createClass({
</SplitPane> </SplitPane>
</div> </div>
<div className={cx('floatingSaveButton', {show : this.props.welcomeText != this.state.text})} onClick={this.handleSave}> <div className={cx('floatingSaveButton', { show: this.props.welcomeText != this.state.text })} onClick={this.handleSave}>
Save current <i className='fa fa-save' /> Save current <i className='fa fa-save' />
</div> </div>
<a href='/new' className='floatingNewButton'> <a href='/new' className='floatingNewButton'>
Create your own <i className='fa fa-magic' /> Create your own <i className='fa fa-magic' />
</a> </a>
</div> </div>;
} }
}); });

View File

@@ -55,7 +55,7 @@ If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](
<img src='http://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' /> <img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
<div class='pageNumber'>1</div> <div class='pageNumber'>1</div>
<div class='footnote'>PART 1 | FANCINESS</div> <div class='footnote'>PART 1 | FANCINESS</div>

View File

@@ -1,7 +1,8 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require("superagent"); const request = require('superagent');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('naturalcrit/markdown.js');
@@ -17,33 +18,33 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const KEY = 'homebrewery-new'; const KEY = 'homebrewery-new';
const NewPage = React.createClass({ const NewPage = createClass({
getInitialState: function() { getInitialState : function() {
return { return {
metadata : { metadata : {
title : '', title : '',
description : '', description : '',
tags : '', tags : '',
published : false, published : false,
authors : [], authors : [],
systems : [] systems : []
}, },
text: '', text : '',
isSaving : false, isSaving : false,
errors : [] errors : []
}; };
}, },
componentDidMount: function() { componentDidMount : function() {
const storage = localStorage.getItem(KEY); const storage = localStorage.getItem(KEY);
if(storage){ if(storage){
this.setState({ this.setState({
text : storage text : storage
}) });
} }
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
@@ -71,7 +72,7 @@ const NewPage = React.createClass({
handleTextChange : function(text){ handleTextChange : function(text){
this.setState({ this.setState({
text : text, text : text,
errors : Markdown.validate(text) errors : Markdown.validate(text)
}); });
localStorage.setItem(KEY, text); localStorage.setItem(KEY, text);
@@ -96,31 +97,31 @@ const NewPage = React.createClass({
window.onbeforeunload = function(){}; window.onbeforeunload = function(){};
const brew = res.body; const brew = res.body;
localStorage.removeItem(KEY); localStorage.removeItem(KEY);
window.location = '/edit/' + brew.editId; window.location = `/edit/${brew.editId}`;
}) });
}, },
renderSaveButton : function(){ renderSaveButton : function(){
if(this.state.isSaving){ if(this.state.isSaving){
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'> return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
save... save...
</Nav.item> </Nav.item>;
}else{ } else {
return <Nav.item icon='fa-save' className='saveButton' onClick={this.save}> return <Nav.item icon='fa-save' className='saveButton' onClick={this.save}>
save save
</Nav.item> </Nav.item>;
} }
}, },
print : function(){ print : function(){
localStorage.setItem('print', this.state.text); localStorage.setItem('print', this.state.text);
window.open('/print?dialog=true&local=print','_blank'); window.open('/print?dialog=true&local=print', '_blank');
}, },
renderLocalPrintButton : function(){ renderLocalPrintButton : function(){
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}> return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}>
get PDF get PDF
</Nav.item> </Nav.item>;
}, },
renderNavbar : function(){ renderNavbar : function(){
@@ -136,7 +137,7 @@ const NewPage = React.createClass({
<IssueNavItem /> <IssueNavItem />
<AccountNavItem /> <AccountNavItem />
</Nav.section> </Nav.section>
</Navbar> </Navbar>;
}, },
render : function(){ render : function(){
@@ -154,7 +155,7 @@ const NewPage = React.createClass({
<BrewRenderer text={this.state.text} errors={this.state.errors} /> <BrewRenderer text={this.state.text} errors={this.state.errors} />
</SplitPane> </SplitPane>
</div> </div>
</div> </div>;
} }
}); });

View File

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

View File

@@ -1,4 +1,5 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
@@ -13,24 +14,24 @@ const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const SharePage = React.createClass({ const SharePage = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
brew : { brew : {
title : '', title : '',
text : '', text : '',
shareId : null, shareId : null,
createdAt : null, createdAt : null,
updatedAt : null, updatedAt : null,
views : 0 views : 0
} }
}; };
}, },
componentDidMount: function() { componentDidMount : function() {
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
handleControlKeys : function(e){ handleControlKeys : function(e){
@@ -54,7 +55,7 @@ const SharePage = React.createClass({
<ReportIssue /> <ReportIssue />
{/*<RecentlyViewed brew={this.props.brew} />*/} {/*<RecentlyViewed brew={this.props.brew} />*/}
<PrintLink shareId={this.props.brew.shareId} /> <PrintLink shareId={this.props.brew.shareId} />
<Nav.item href={'/source/' + this.props.brew.shareId} color='teal' icon='fa-code'> <Nav.item href={`/source/${this.props.brew.shareId}`} color='teal' icon='fa-code'>
source source
</Nav.item> </Nav.item>
<Account /> <Account />
@@ -64,7 +65,7 @@ const SharePage = React.createClass({
<div className='content'> <div className='content'>
<BrewRenderer text={this.props.brew.text} /> <BrewRenderer text={this.props.brew.text} />
</div> </div>
</div> </div>;
} }
}); });

View File

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

View File

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

View File

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

View File

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

45
contributing.md Normal file
View File

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

17
docker-compose.yml Normal file
View File

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

8118
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
const _ = require('lodash'); const _ = require('lodash');
const jwt = require('jwt-simple'); const jwt = require('jwt-simple');
const express = require("express"); const express = require('express');
const app = express(); const app = express();
app.use(express.static(__dirname + '/build'));'' app.use(express.static(`${__dirname}/build`));
app.use(require('body-parser').json({limit: '25mb'})); app.use(require('body-parser').json({ limit: '25mb' }));
app.use(require('cookie-parser')()); app.use(require('cookie-parser')());
const config = require('nconf') const config = require('nconf')
@@ -14,20 +14,21 @@ const config = require('nconf')
.file('defaults', { file: 'config/default.json' }); .file('defaults', { file: 'config/default.json' });
//DB //DB
require('mongoose') const mongoose = require('mongoose');
.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/naturalcrit') mongoose.connect(config.get('mongodb_uri') || config.get('mongolab_uri') || 'mongodb://localhost/naturalcrit');
.connection.on('error', () => { mongoose.connection.on('error', ()=>{
console.log('Error : Could not connect to a Mongo Database.'); console.log('Error : Could not connect to a Mongo Database.');
console.log(' If you are running locally, make sure mongodb.exe is running.'); console.log(' If you are running locally, make sure mongodb.exe is running.');
}); throw 'Can not connect to Mongo';
});
//Account MIddleware //Account MIddleware
app.use((req, res, next) => { app.use((req, res, next)=>{
if(req.cookies && req.cookies.nc_session){ if(req.cookies && req.cookies.nc_session){
try{ try {
req.account = jwt.decode(req.cookies.nc_session, config.get('secret')); req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
}catch(e){} } catch (e){}
} }
return next(); return next();
}); });
@@ -43,35 +44,35 @@ const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
//Source page //Source page
String.prototype.replaceAll = function(s,r){return this.split(s).join(r)} String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
app.get('/source/:id', (req, res)=>{ app.get('/source/:id', (req, res)=>{
HomebrewModel.get({shareId : req.params.id}) HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{ .then((brew)=>{
const text = brew.text.replaceAll('<', '&lt;').replaceAll('>', '&gt;'); const text = brew.text.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
return res.send(`<code><pre>${text}</pre></code>`); return res.send(`<code><pre style="white-space: pre-wrap;">${text}</pre></code>`);
}) })
.catch((err)=>{ .catch((err)=>{
console.log(err); console.log(err);
return res.status(404).send('Could not find Homebrew with that id'); return res.status(404).send('Could not find Homebrew with that id');
}) });
}); });
app.get('/user/:username', (req, res, next) => { app.get('/user/:username', (req, res, next)=>{
const fullAccess = req.account && (req.account.username == req.params.username); const fullAccess = req.account && (req.account.username == req.params.username);
HomebrewModel.getByUser(req.params.username, fullAccess) HomebrewModel.getByUser(req.params.username, fullAccess)
.then((brews) => { .then((brews)=>{
req.brews = brews; req.brews = brews;
return next(); return next();
}) })
.catch((err) => { .catch((err)=>{
console.log(err); console.log(err);
}) });
}) });
app.get('/edit/:id', (req, res, next)=>{ app.get('/edit/:id', (req, res, next)=>{
HomebrewModel.get({editId : req.params.id}) HomebrewModel.get({ editId: req.params.id })
.then((brew)=>{ .then((brew)=>{
req.brew = brew.sanatize(); req.brew = brew.sanatize();
return next(); return next();
@@ -84,7 +85,7 @@ app.get('/edit/:id', (req, res, next)=>{
//Share Page //Share Page
app.get('/share/:id', (req, res, next)=>{ app.get('/share/:id', (req, res, next)=>{
HomebrewModel.get({shareId : req.params.id}) HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{ .then((brew)=>{
return brew.increaseView(); return brew.increaseView();
}) })
@@ -100,7 +101,7 @@ app.get('/share/:id', (req, res, next)=>{
//Print Page //Print Page
app.get('/print/:id', (req, res, next)=>{ app.get('/print/:id', (req, res, next)=>{
HomebrewModel.get({shareId : req.params.id}) HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{ .then((brew)=>{
req.brew = brew.sanatize(true); req.brew = brew.sanatize(true);
return next(); return next();
@@ -115,20 +116,20 @@ app.get('/print/:id', (req, res, next)=>{
//Render Page //Render Page
const render = require('vitreum/steps/render'); const render = require('vitreum/steps/render');
const templateFn = require('./client/template.js'); const templateFn = require('./client/template.js');
app.use((req, res) => { app.use((req, res)=>{
render('homebrew', templateFn, { render('homebrew', templateFn, {
version : require('./package.json').version, version : require('./package.json').version,
url: req.originalUrl, url : req.originalUrl,
welcomeText : welcomeText, welcomeText : welcomeText,
changelog : changelogText, changelog : changelogText,
brew : req.brew, brew : req.brew,
brews : req.brews, brews : req.brews,
account : req.account account : req.account
})
.then((page)=>{
return res.send(page);
}) })
.then((page) => { .catch((err)=>{
return res.send(page)
})
.catch((err) => {
console.log(err); console.log(err);
return res.sendStatus(500); return res.sendStatus(500);
}); });

View File

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

View File

@@ -1,33 +1,19 @@
const _ = require('lodash'); const _ = require('lodash');
const Moment = require('moment');
const HomebrewModel = require('./homebrew.model.js').model; const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router(); const router = require('express').Router();
// const getTopBrews = (cb)=>{
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
// cb(brews);
// });
// };
const getGoodBrewTitle = (text)=>{
//TODO: Possiblity remove
let homebrewTotal = 0;
const refreshCount = ()=>{
HomebrewModel.count({}, (err, total)=>{
homebrewTotal = total;
});
};
refreshCount();
const getTopBrews = (cb)=>{
HomebrewModel.find().sort({views: -1}).limit(5).exec(function(err, brews) {
cb(brews);
});
}
const getGoodBrewTitle = (text) => {
const titlePos = text.indexOf('# '); const titlePos = text.indexOf('# ');
if(titlePos !== -1){ if(titlePos !== -1){
const ending = text.indexOf('\n', titlePos); const ending = text.indexOf('\n', titlePos);
return text.substring(titlePos + 2, ending); return text.substring(titlePos + 2, ending);
}else{ } else {
return _.find(text.split('\n'), (line)=>{ return _.find(text.split('\n'), (line)=>{
return line; return line;
}); });
@@ -43,7 +29,7 @@ router.post('/api', (req, res)=>{
const newHomebrew = new HomebrewModel(_.merge({}, const newHomebrew = new HomebrewModel(_.merge({},
req.body, req.body,
{authors : authors} { authors: authors }
)); ));
if(!newHomebrew.title){ if(!newHomebrew.title){
newHomebrew.title = getGoodBrewTitle(newHomebrew.text); newHomebrew.title = getGoodBrewTitle(newHomebrew.text);
@@ -54,11 +40,11 @@ router.post('/api', (req, res)=>{
return res.status(500).send(`Error while creating new brew, ${err.toString()}`); return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
} }
return res.json(obj); return res.json(obj);
}) });
}); });
router.put('/api/update/:id', (req, res)=>{ router.put('/api/update/:id', (req, res)=>{
HomebrewModel.get({editId : req.params.id}) HomebrewModel.get({ editId: req.params.id })
.then((brew)=>{ .then((brew)=>{
brew = _.merge(brew, req.body); brew = _.merge(brew, req.body);
brew.updatedAt = new Date(); brew.updatedAt = new Date();
@@ -70,22 +56,22 @@ router.put('/api/update/:id', (req, res)=>{
brew.save((err, obj)=>{ brew.save((err, obj)=>{
if(err) throw err; if(err) throw err;
return res.status(200).send(obj); return res.status(200).send(obj);
}) });
}) })
.catch((err)=>{ .catch((err)=>{
console.log(err); console.log(err);
return res.status(500).send("Error while saving"); return res.status(500).send('Error while saving');
}); });
}); });
router.get('/api/remove/:id', (req, res)=>{ router.get('/api/remove/:id', (req, res)=>{
HomebrewModel.find({editId : req.params.id}, (err, objs)=>{ HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{
if(!objs.length || err) return res.status(404).send("Can not find homebrew with that id"); if(!objs.length || err) return res.status(404).send('Can not find homebrew with that id');
var resEntry = objs[0]; const resEntry = objs[0];
resEntry.remove((err)=>{ resEntry.remove((err)=>{
if(err) return res.status(500).send("Error while removing"); if(err) return res.status(500).send('Error while removing');
return res.status(200).send(); return res.status(200).send();
}) });
}); });
}); });

View File

@@ -1,24 +1,24 @@
var mongoose = require('mongoose'); const mongoose = require('mongoose');
var shortid = require('shortid'); const shortid = require('shortid');
var _ = require('lodash'); const _ = require('lodash');
var HomebrewSchema = mongoose.Schema({ const HomebrewSchema = mongoose.Schema({
shareId : {type : String, default: shortid.generate, index: { unique: true }}, shareId : { type: String, default: shortid.generate, index: { unique: true } },
editId : {type : String, default: shortid.generate, index: { unique: true }}, editId : { type: String, default: shortid.generate, index: { unique: true } },
title : {type : String, default : ""}, title : { type: String, default: '' },
text : {type : String, default : ""}, text : { type: String, default: '' },
description : {type : String, default : ""}, description : { type: String, default: '' },
tags : {type : String, default : ""}, tags : { type: String, default: '' },
systems : [String], systems : [String],
authors : [String], authors : [String],
published : {type : Boolean, default : false}, published : { type: Boolean, default: false },
createdAt : { type: Date, default: Date.now }, createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now}, updatedAt : { type: Date, default: Date.now },
lastViewed : { type: Date, default: Date.now}, lastViewed : { type: Date, default: Date.now },
views : {type:Number, default:0}, views : { type: Number, default: 0 },
version : {type: Number, default:1} version : { type: Number, default: 1 }
}, { versionKey: false }); }, { versionKey: false });
@@ -35,10 +35,10 @@ HomebrewSchema.methods.sanatize = function(full=false){
HomebrewSchema.methods.increaseView = function(){ HomebrewSchema.methods.increaseView = function(){
return new Promise((resolve, reject) => { return new Promise((resolve, reject)=>{
this.lastViewed = new Date(); this.lastViewed = new Date();
this.views = this.views + 1; this.views = this.views + 1;
this.save((err) => { this.save((err)=>{
if(err) return reject(err); if(err) return reject(err);
return resolve(this); return resolve(this);
}); });
@@ -48,7 +48,7 @@ HomebrewSchema.methods.increaseView = function(){
HomebrewSchema.statics.get = function(query){ HomebrewSchema.statics.get = function(query){
return new Promise((resolve, reject) => { return new Promise((resolve, reject)=>{
Homebrew.find(query, (err, brews)=>{ Homebrew.find(query, (err, brews)=>{
if(err || !brews.length) return reject('Can not find brew'); if(err || !brews.length) return reject('Can not find brew');
return resolve(brews[0]); return resolve(brews[0]);
@@ -57,8 +57,8 @@ HomebrewSchema.statics.get = function(query){
}; };
HomebrewSchema.statics.getByUser = function(username, allowAccess=false){ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
return new Promise((resolve, reject) => { return new Promise((resolve, reject)=>{
let query = {authors : username, published : true}; const query = { authors: username, published: true };
if(allowAccess){ if(allowAccess){
delete query.published; delete query.published;
} }
@@ -73,9 +73,9 @@ HomebrewSchema.statics.getByUser = function(username, allowAccess=false){
var Homebrew = mongoose.model('Homebrew', HomebrewSchema); const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
module.exports = { module.exports = {
schema : HomebrewSchema, schema : HomebrewSchema,
model : Homebrew, model : Homebrew,
} };

View File

@@ -1,21 +1,22 @@
const React = require('react'); const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const DISMISS_KEY = 'dismiss_render_warning'; const DISMISS_KEY = 'dismiss_render_warning';
const RenderWarnings = React.createClass({ const RenderWarnings = createClass({
getInitialState: function() { getInitialState : function() {
return { return {
warnings: {} warnings : {}
}; };
}, },
componentDidMount: function() { componentDidMount : function() {
this.checkWarnings(); this.checkWarnings();
window.addEventListener('resize', this.checkWarnings); window.addEventListener('resize', this.checkWarnings);
}, },
componentWillUnmount: function() { componentWillUnmount : function() {
window.removeEventListener('resize', this.checkWarnings); window.removeEventListener('resize', this.checkWarnings);
}, },
warnings : { warnings : {
@@ -31,33 +32,24 @@ const RenderWarnings = React.createClass({
</li>; </li>;
} }
}, },
zoom : function(){
return false;
if(window.devicePixelRatio !== 1){
return <li key='zoom'>
<em>Your browser is zoomed. </em> <br />
This can cause content to jump columns.
</li>;
}
}
}, },
checkWarnings : function(){ checkWarnings : function(){
const hideDismiss = localStorage.getItem(DISMISS_KEY); const hideDismiss = localStorage.getItem(DISMISS_KEY);
if(hideDismiss) return this.setState({warnings : {}}); if(hideDismiss) return this.setState({ warnings: {} });
this.setState({ this.setState({
warnings : _.reduce(this.warnings, (r, fn, type) => { warnings : _.reduce(this.warnings, (r, fn, type)=>{
const element = fn(); const element = fn();
if(element) r[type] = element; if(element) r[type] = element;
return r; return r;
}, {}) }, {})
}) });
}, },
dismiss : function(){ dismiss : function(){
localStorage.setItem(DISMISS_KEY, true); localStorage.setItem(DISMISS_KEY, true);
this.checkWarnings(); this.checkWarnings();
}, },
render: function(){ render : function(){
if(_.isEmpty(this.state.warnings)) return null; if(_.isEmpty(this.state.warnings)) return null;
return <div className='renderWarnings'> return <div className='renderWarnings'>
@@ -66,7 +58,7 @@ const RenderWarnings = React.createClass({
<h3>Render Warnings</h3> <h3>Render Warnings</h3>
<small>If this homebrew is rendering badly if might be because of the following:</small> <small>If this homebrew is rendering badly if might be because of the following:</small>
<ul>{_.values(this.state.warnings)}</ul> <ul>{_.values(this.state.warnings)}</ul>
</div> </div>;
} }
}); });

View File

@@ -1,11 +1,12 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
const cx = require('classnames');
var CodeMirror; let CodeMirror;
if(typeof navigator !== 'undefined'){ if(typeof navigator !== 'undefined'){
var CodeMirror = require('codemirror'); CodeMirror = require('codemirror');
//Language Modes //Language Modes
require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown require('codemirror/mode/gfm/gfm.js'); //Github flavoured markdown
@@ -13,23 +14,27 @@ if(typeof navigator !== 'undefined'){
} }
var CodeEditor = React.createClass({ const CodeEditor = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
language : '', language : '',
value : '', value : '',
wrap : false, wrap : false,
onChange : function(){}, onChange : function(){},
onCursorActivity : function(){}, onCursorActivity : function(){},
}; };
}, },
componentDidMount: function() { componentDidMount : function() {
this.codeMirror = CodeMirror(this.refs.editor,{ this.codeMirror = CodeMirror(this.refs.editor, {
value : this.props.value, value : this.props.value,
lineNumbers: true, lineNumbers : true,
lineWrapping : this.props.wrap, lineWrapping : this.props.wrap,
mode : this.props.language mode : this.props.language,
extraKeys : {
'Ctrl-B' : this.makeBold,
'Ctrl-I' : this.makeItalic
}
}); });
this.codeMirror.on('change', this.handleChange); this.codeMirror.on('change', this.handleChange);
@@ -37,13 +42,23 @@ var CodeEditor = React.createClass({
this.updateSize(); this.updateSize();
}, },
componentWillReceiveProps: function(nextProps){ makeBold : function() {
const selection = this.codeMirror.getSelection();
this.codeMirror.replaceSelection(`**${selection}**`, 'around');
},
makeItalic : function() {
const selection = this.codeMirror.getSelection();
this.codeMirror.replaceSelection(`*${selection}*`, 'around');
},
componentWillReceiveProps : function(nextProps){
if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) { if(this.codeMirror && nextProps.value !== undefined && this.codeMirror.getValue() != nextProps.value) {
this.codeMirror.setValue(nextProps.value); this.codeMirror.setValue(nextProps.value);
} }
}, },
shouldComponentUpdate: function(nextProps, nextState) { shouldComponentUpdate : function(nextProps, nextState) {
return false; return false;
}, },
@@ -66,7 +81,7 @@ var CodeEditor = React.createClass({
}, },
render : function(){ render : function(){
return <div className='codeEditor' ref='editor' /> return <div className='codeEditor' ref='editor' />;
} }
}); });

View File

@@ -1,11 +1,11 @@
var _ = require('lodash'); const _ = require('lodash');
var Markdown = require('marked'); const Markdown = require('marked');
var renderer = new Markdown.Renderer(); const renderer = new Markdown.Renderer();
//Processes the markdown within an HTML block if it's just a class-wrapper //Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) { renderer.html = function (html) {
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){ if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
var openTag = html.substring(0, html.indexOf('>')+1); const openTag = html.substring(0, html.indexOf('>')+1);
html = html.substring(html.indexOf('>')+1); html = html.substring(html.indexOf('>')+1);
html = html.substring(0, html.lastIndexOf('</div>')); html = html.substring(0, html.lastIndexOf('</div>'));
return `${openTag} ${Markdown(html)} </div>`; return `${openTag} ${Markdown(html)} </div>`;
@@ -13,25 +13,33 @@ renderer.html = function (html) {
return html; return html;
}; };
const sanatizeScriptTags = (content)=>{
return content
.replace(/<script/g, '&lt;script')
.replace(/<\/script>/g, '&lt;/script&gt;');
};
const tagTypes = ['div', 'span', 'a']; const tagTypes = ['div', 'span', 'a'];
const tagRegex = new RegExp('(' + const tagRegex = new RegExp(`(${
_.map(tagTypes, (type)=>{ _.map(tagTypes, (type)=>{
return `\\<${type}|\\</${type}>`; return `\\<${type}|\\</${type}>`;
}).join('|') + ')', 'g'); }).join('|')})`, 'g');
module.exports = { module.exports = {
marked : Markdown, marked : Markdown,
render : (rawBrewText)=>{ render : (rawBrewText)=>{
return Markdown(rawBrewText, {renderer : renderer}) return Markdown(
sanatizeScriptTags(rawBrewText),
{ renderer: renderer }
);
}, },
validate : (rawBrewText) => { validate : (rawBrewText)=>{
var errors = []; const errors = [];
var leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber) => { const leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber)=>{
var lineNumber = _lineNumber + 1; const lineNumber = _lineNumber + 1;
var matches = line.match(tagRegex); const matches = line.match(tagRegex);
if(!matches || !matches.length) return acc; if(!matches || !matches.length) return acc;
_.each(matches, (match)=>{ _.each(matches, (match)=>{
@@ -48,16 +56,16 @@ module.exports = {
line : lineNumber, line : lineNumber,
type : type, type : type,
text : 'Unmatched closing tag', text : 'Unmatched closing tag',
id : 'CLOSE' id : 'CLOSE'
}); });
}else if(_.last(acc).type == type){ } else if(_.last(acc).type == type){
acc.pop(); acc.pop();
}else{ } else {
errors.push({ errors.push({
line : _.last(acc).line + ' to ' + lineNumber, line : `${_.last(acc).line} to ${lineNumber}`,
type : type, type : type,
text : 'Type mismatch on closing tag', text : 'Type mismatch on closing tag',
id : 'MISMATCH' id : 'MISMATCH'
}); });
acc.pop(); acc.pop();
} }
@@ -71,9 +79,9 @@ module.exports = {
errors.push({ errors.push({
line : unmatched.line, line : unmatched.line,
type : unmatched.type, type : unmatched.type,
text : "Unmatched opening tag", text : 'Unmatched opening tag',
id : 'OPEN' id : 'OPEN'
}) });
}); });
return errors; return errors;

View File

@@ -1,21 +1,22 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
const cx = require('classnames');
var NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx'); const NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
var Nav = { const Nav = {
base : React.createClass({ base : createClass({
render : function(){ render : function(){
return <nav> return <nav>
<div className='navContent'> <div className='navContent'>
{this.props.children} {this.props.children}
</div> </div>
</nav> </nav>;
} }
}), }),
logo : function(){ logo : function(){
return <a className='navLogo' href="http://naturalcrit.com"> return <a className='navLogo' href='http://naturalcrit.com'>
<NaturalCritIcon /> <NaturalCritIcon />
<span className='name'> <span className='name'>
Natural<span className='crit'>Crit</span> Natural<span className='crit'>Crit</span>
@@ -23,32 +24,32 @@ var Nav = {
</a>; </a>;
}, },
section : React.createClass({ section : createClass({
render : function(){ render : function(){
return <div className='navSection'> return <div className='navSection'>
{this.props.children} {this.props.children}
</div> </div>;
} }
}), }),
item : React.createClass({ item : createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
icon : null, icon : null,
href : null, href : null,
newTab : false, newTab : false,
onClick : function(){}, onClick : function(){},
color : null color : null
}; };
}, },
handleClick : function(){ handleClick : function(){
this.props.onClick(); this.props.onClick();
}, },
render : function(){ render : function(){
var classes = cx('navItem', this.props.color, this.props.className); const classes = cx('navItem', this.props.color, this.props.className);
var icon; let icon;
if(this.props.icon) icon = <i className={'fa ' + this.props.icon} />; if(this.props.icon) icon = <i className={`fa ${this.props.icon}`} />;
const props = _.omit(this.props, ['newTab']); const props = _.omit(this.props, ['newTab']);
@@ -56,12 +57,12 @@ var Nav = {
return <a {...props} className={classes} target={this.props.newTab ? '_blank' : '_self'} > return <a {...props} className={classes} target={this.props.newTab ? '_blank' : '_self'} >
{this.props.children} {this.props.children}
{icon} {icon}
</a> </a>;
}else{ } else {
return <div {...props} className={classes} onClick={this.handleClick} > return <div {...props} className={classes} onClick={this.handleClick} >
{this.props.children} {this.props.children}
{icon} {icon}
</div> </div>;
} }
} }
}), }),

View File

@@ -1,27 +1,28 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const createClass = require('create-react-class');
var cx = require('classnames'); const _ = require('lodash');
const cx = require('classnames');
var SplitPane = React.createClass({ const SplitPane = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
storageKey : 'naturalcrit-pane-split', storageKey : 'naturalcrit-pane-split',
onDragFinish : function(){} //fires when dragging onDragFinish : function(){} //fires when dragging
}; };
}, },
getInitialState: function() { getInitialState : function() {
return { return {
size : null, size : null,
isDragging : false isDragging : false
}; };
}, },
componentDidMount: function() { componentDidMount : function() {
var paneSize = window.localStorage.getItem(this.props.storageKey); const paneSize = window.localStorage.getItem(this.props.storageKey);
if(paneSize){ if(paneSize){
this.setState({ this.setState({
size : paneSize size : paneSize
}) });
} }
}, },
@@ -30,10 +31,10 @@ var SplitPane = React.createClass({
this.props.onDragFinish(this.state.size); this.props.onDragFinish(this.state.size);
window.localStorage.setItem(this.props.storageKey, this.state.size); window.localStorage.setItem(this.props.storageKey, this.state.size);
} }
this.setState({ isDragging : false }); this.setState({ isDragging: false });
}, },
handleDown : function(){ handleDown : function(){
this.setState({ isDragging : true }); this.setState({ isDragging: true });
//this.unFocus() //this.unFocus()
}, },
handleMove : function(e){ handleMove : function(e){
@@ -42,7 +43,7 @@ var SplitPane = React.createClass({
size : e.pageX size : e.pageX
}); });
}, },
/* /*
unFocus : function() { unFocus : function() {
if(document.selection){ if(document.selection){
document.selection.empty(); document.selection.empty();
@@ -58,7 +59,7 @@ var SplitPane = React.createClass({
<i className='fa fa-circle' /> <i className='fa fa-circle' />
<i className='fa fa-circle' /> <i className='fa fa-circle' />
</div> </div>
</div> </div>;
}, },
render : function(){ render : function(){
@@ -66,7 +67,7 @@ var SplitPane = React.createClass({
<Pane ref='pane1' width={this.state.size}>{this.props.children[0]}</Pane> <Pane ref='pane1' width={this.state.size}>{this.props.children[0]}</Pane>
{this.renderDivider()} {this.renderDivider()}
<Pane ref='pane2'>{this.props.children[1]}</Pane> <Pane ref='pane2'>{this.props.children[1]}</Pane>
</div> </div>;
} }
}); });
@@ -75,23 +76,23 @@ var SplitPane = React.createClass({
var Pane = React.createClass({ const Pane = createClass({
getDefaultProps: function() { getDefaultProps : function() {
return { return {
width : null width : null
}; };
}, },
render : function(){ render : function(){
var styles = {}; let styles = {};
if(this.props.width){ if(this.props.width){
styles = { styles = {
flex : 'none', flex : 'none',
width : this.props.width + 'px' width : `${this.props.width}px`
} };
} }
return <div className={cx('pane', this.props.className)} style={styles}> return <div className={cx('pane', this.props.className)} style={styles}>
{this.props.children} {this.props.children}
</div> </div>;
} }
}); });

View File

@@ -7,6 +7,7 @@
flex-direction : row; flex-direction : row;
.pane{ .pane{
overflow-x : hidden; overflow-x : hidden;
overflow-y : hidden;
flex : 1; flex : 1;
} }
.divider{ .divider{

View File

@@ -1,3 +1,3 @@
module.exports = function(props){ module.exports = function(props){
return <svg version="1.1" x="0px" y="0px" viewBox="0 0 80 100" enableBackground="new 0 0 80 80"><g><g><polygon fill="#000000" points="12.9,71.4 7.6,66.1 19.3,54.4 20.7,55.8 10.4,66.1 12.9,68.6 23.2,58.3 24.6,59.7 "/></g><g><path fill="#000000" d="M29,61.6c-1.7,0-3.4-0.7-4.6-1.9l-5.1-5.1c-2.5-2.5-2.5-6.6,0-9.2l0.7-0.7L34.3,59l-0.7,0.7 C32.4,60.9,30.8,61.6,29,61.6z M20.1,47.6c-1.1,1.7-0.9,4.1,0.6,5.6l5.1,5.1c0.8,0.8,2,1.3,3.2,1.3c0.9,0,1.7-0.2,2.4-0.7 L20.1,47.6z"/></g><g><path fill="#000000" d="M12.3,74.8c-0.8,0-1.5-0.3-2-0.8l-5.2-5.2c-0.5-0.5-0.8-1.2-0.8-2c0-0.8,0.3-1.5,0.8-2 c1.1-1.1,2.9-1.1,4,0l5.2,5.2c1.1,1.1,1.1,2.9,0,4C13.8,74.5,13.1,74.8,12.3,74.8z M7.1,65.9c-0.2,0-0.4,0.1-0.6,0.2 c-0.2,0.2-0.2,0.4-0.2,0.6s0.1,0.4,0.2,0.6l5.2,5.2c0.3,0.3,0.9,0.3,1.2,0c0.3-0.3,0.3-0.8,0-1.2l-5.2-5.2 C7.5,66,7.3,65.9,7.1,65.9z"/></g><g><polygon fill="#000000" points="31.7,58.7 30.3,57.3 70,17.6 70,9 62.4,9 23.3,49.4 21.9,48 61.6,7 72,7 72,18.4 "/></g><g><rect x="46" y="6.7" transform="matrix(0.7168 0.6973 -0.6973 0.7168 35.9716 -23.568)" fill="#000000" width="2" height="51.6"/></g><g><rect x="13" y="61" fill="#000000" width="2" height="7"/></g><g><rect x="17" y="57" fill="#000000" width="2" height="7"/></g></g><g><g><polygon fill="#000000" points="68.4,71.4 56.7,59.7 58.1,58.3 68.4,68.6 70.8,66.1 60.5,55.8 61.9,54.4 73.6,66.1 "/></g><g><path fill="#000000" d="M52.2,61.6c-1.7,0-3.4-0.7-4.6-1.9L46.9,59l14.3-14.3l0.7,0.7c2.5,2.5,2.5,6.6,0,9.2l-5.1,5.1 C55.6,60.9,53.9,61.6,52.2,61.6z M49.8,58.9c0.7,0.4,1.5,0.7,2.4,0.7c1.2,0,2.3-0.5,3.2-1.3l5.1-5.1c1.5-1.5,1.7-3.8,0.6-5.6 L49.8,58.9z"/></g><g><path fill="#000000" d="M68.9,74.8c-0.8,0-1.5-0.3-2-0.8c-1.1-1.1-1.1-2.9,0-4l5.2-5.2c1.1-1.1,2.9-1.1,4,0c0.5,0.5,0.8,1.2,0.8,2 c0,0.8-0.3,1.5-0.8,2L70.9,74C70.4,74.5,69.7,74.8,68.9,74.8z M74.2,65.9c-0.2,0-0.4,0.1-0.6,0.2l-5.2,5.2c-0.3,0.3-0.3,0.8,0,1.2 c0.3,0.3,0.9,0.3,1.2,0l5.2-5.2c0.2-0.2,0.2-0.4,0.2-0.6s-0.1-0.4-0.2-0.6C74.6,66,74.4,65.9,74.2,65.9z"/></g><g><rect x="38.6" y="52.3" transform="matrix(0.7082 0.706 -0.706 0.7082 50.8397 -16.4875)" fill="#000000" width="13.4" height="2"/></g><g><polygon fill="#000000" points="30.6,39.9 9,18.4 9,7 19.7,7 41.1,29.1 39.7,30.5 18.8,9 11,9 11,17.6 32,38.5 "/></g><g><rect x="47.8" y="43.1" transform="matrix(0.6959 0.7181 -0.7181 0.6959 48.1381 -25.5246)" fill="#000000" width="12.8" height="2"/></g><g><rect x="12" y="23.1" transform="matrix(0.6974 0.7167 -0.7167 0.6974 25.1384 -11.3825)" fill="#000000" width="28.1" height="2"/></g><g><rect x="43.8" y="46.4" transform="matrix(0.6974 0.7167 -0.7167 0.6974 48.7492 -20.5985)" fill="#000000" width="10" height="2"/></g><g><rect x="66" y="61" fill="#000000" width="2" height="7"/></g><g><rect x="62" y="57" fill="#000000" width="2" height="7"/></g></g></svg>; return <svg version='1.1' x='0px' y='0px' viewBox='0 0 80 100' enableBackground='new 0 0 80 80'><g><g><polygon fill='#000000' points='12.9,71.4 7.6,66.1 19.3,54.4 20.7,55.8 10.4,66.1 12.9,68.6 23.2,58.3 24.6,59.7 '/></g><g><path fill='#000000' d='M29,61.6c-1.7,0-3.4-0.7-4.6-1.9l-5.1-5.1c-2.5-2.5-2.5-6.6,0-9.2l0.7-0.7L34.3,59l-0.7,0.7 C32.4,60.9,30.8,61.6,29,61.6z M20.1,47.6c-1.1,1.7-0.9,4.1,0.6,5.6l5.1,5.1c0.8,0.8,2,1.3,3.2,1.3c0.9,0,1.7-0.2,2.4-0.7 L20.1,47.6z'/></g><g><path fill='#000000' d='M12.3,74.8c-0.8,0-1.5-0.3-2-0.8l-5.2-5.2c-0.5-0.5-0.8-1.2-0.8-2c0-0.8,0.3-1.5,0.8-2 c1.1-1.1,2.9-1.1,4,0l5.2,5.2c1.1,1.1,1.1,2.9,0,4C13.8,74.5,13.1,74.8,12.3,74.8z M7.1,65.9c-0.2,0-0.4,0.1-0.6,0.2 c-0.2,0.2-0.2,0.4-0.2,0.6s0.1,0.4,0.2,0.6l5.2,5.2c0.3,0.3,0.9,0.3,1.2,0c0.3-0.3,0.3-0.8,0-1.2l-5.2-5.2 C7.5,66,7.3,65.9,7.1,65.9z'/></g><g><polygon fill='#000000' points='31.7,58.7 30.3,57.3 70,17.6 70,9 62.4,9 23.3,49.4 21.9,48 61.6,7 72,7 72,18.4 '/></g><g><rect x='46' y='6.7' transform='matrix(0.7168 0.6973 -0.6973 0.7168 35.9716 -23.568)' fill='#000000' width='2' height='51.6'/></g><g><rect x='13' y='61' fill='#000000' width='2' height='7'/></g><g><rect x='17' y='57' fill='#000000' width='2' height='7'/></g></g><g><g><polygon fill='#000000' points='68.4,71.4 56.7,59.7 58.1,58.3 68.4,68.6 70.8,66.1 60.5,55.8 61.9,54.4 73.6,66.1 '/></g><g><path fill='#000000' d='M52.2,61.6c-1.7,0-3.4-0.7-4.6-1.9L46.9,59l14.3-14.3l0.7,0.7c2.5,2.5,2.5,6.6,0,9.2l-5.1,5.1 C55.6,60.9,53.9,61.6,52.2,61.6z M49.8,58.9c0.7,0.4,1.5,0.7,2.4,0.7c1.2,0,2.3-0.5,3.2-1.3l5.1-5.1c1.5-1.5,1.7-3.8,0.6-5.6 L49.8,58.9z'/></g><g><path fill='#000000' d='M68.9,74.8c-0.8,0-1.5-0.3-2-0.8c-1.1-1.1-1.1-2.9,0-4l5.2-5.2c1.1-1.1,2.9-1.1,4,0c0.5,0.5,0.8,1.2,0.8,2 c0,0.8-0.3,1.5-0.8,2L70.9,74C70.4,74.5,69.7,74.8,68.9,74.8z M74.2,65.9c-0.2,0-0.4,0.1-0.6,0.2l-5.2,5.2c-0.3,0.3-0.3,0.8,0,1.2 c0.3,0.3,0.9,0.3,1.2,0l5.2-5.2c0.2-0.2,0.2-0.4,0.2-0.6s-0.1-0.4-0.2-0.6C74.6,66,74.4,65.9,74.2,65.9z'/></g><g><rect x='38.6' y='52.3' transform='matrix(0.7082 0.706 -0.706 0.7082 50.8397 -16.4875)' fill='#000000' width='13.4' height='2'/></g><g><polygon fill='#000000' points='30.6,39.9 9,18.4 9,7 19.7,7 41.1,29.1 39.7,30.5 18.8,9 11,9 11,17.6 32,38.5 '/></g><g><rect x='47.8' y='43.1' transform='matrix(0.6959 0.7181 -0.7181 0.6959 48.1381 -25.5246)' fill='#000000' width='12.8' height='2'/></g><g><rect x='12' y='23.1' transform='matrix(0.6974 0.7167 -0.7167 0.6974 25.1384 -11.3825)' fill='#000000' width='28.1' height='2'/></g><g><rect x='43.8' y='46.4' transform='matrix(0.6974 0.7167 -0.7167 0.6974 48.7492 -20.5985)' fill='#000000' width='10' height='2'/></g><g><rect x='66' y='61' fill='#000000' width='2' height='7'/></g><g><rect x='62' y='57' fill='#000000' width='2' height='7'/></g></g></svg>;
} };

View File

@@ -1,9 +1,10 @@
var React = require('react'); const React = require('react');
const createClass = require('create-react-class');
module.exports = function(props){ module.exports = function(props){
return <svg version="1.1" x="0px" y="0px" viewBox="0 0 90 112.5" enableBackground="new 0 0 90 90" > return <svg version='1.1' x='0px' y='0px' viewBox='0 0 90 112.5' enableBackground='new 0 0 90 90' >
<path d="M25.363,25.54c0,1.906,8.793,3.454,19.636,3.454c10.848,0,19.638-1.547,19.638-3.454c0-1.12-3.056-2.117-7.774-2.75 c-1.418,1.891-3.659,3.133-6.208,3.133c-2.85,0-5.315-1.547-6.67-3.833C33.617,22.185,25.363,23.692,25.363,25.54z"/><path d="M84.075,54.142c0-8.68-2.868-17.005-8.144-23.829c1.106-1.399,1.41-2.771,1.41-3.854c0-6.574-10.245-9.358-19.264-10.533 c0.209,0.706,0.359,1.439,0.359,2.215c0,0.09-0.022,0.17-0.028,0.26l0,0c-0.028,0.853-0.195,1.667-0.479,2.429 c9.106,1.282,14.508,3.754,14.508,5.63c0,2.644-10.688,6.486-27.439,6.486c-16.748,0-27.438-3.842-27.438-6.486 c0-2.542,9.904-6.183,25.559-6.459c-0.098-0.396-0.159-0.807-0.2-1.223c0.006,0,0.013,0,0.017,0 c-0.017-0.213-0.063-0.417-0.063-0.636c0-1.084,0.226-2.119,0.628-3.058c-6.788,0.129-30.846,1.299-30.846,11.376 c0,1.083,0.305,2.455,1.411,3.854c-5.276,6.823-8.145,15.149-8.145,23.829c0,11.548,5.187,20.107,14.693,25.115 c-0.902,3.146-1.391,7.056,1.111,8.181c2.626,1.178,5.364-2.139,7.111-5.005c4.73,1.261,10.13,1.923,16.161,1.923 c6.034,0,11.428-0.661,16.158-1.922c1.75,2.865,4.493,6.18,7.112,5.004c2.504-1.123,2.014-5.035,1.113-8.179 C78.889,74.249,84.075,65.689,84.075,54.142z M70.39,31.392c5.43,6.046,8.78,14,8.78,22.75c0,20.919-18.582,25.309-34.171,25.309 c-15.587,0-34.17-4.39-34.17-25.309c0-8.75,3.35-16.7,8.781-22.753c5.561,2.643,15.502,4.009,25.389,4.009 C54.886,35.397,64.829,34.031,70.39,31.392z"/><path d="M50.654,23.374c2.892,0,5.234-2.341,5.234-5.233c0-2.887-2.343-5.23-5.234-5.23c-2.887,0-5.231,2.343-5.231,5.23 C45.423,21.032,47.768,23.374,50.654,23.374z"/> <path d='M25.363,25.54c0,1.906,8.793,3.454,19.636,3.454c10.848,0,19.638-1.547,19.638-3.454c0-1.12-3.056-2.117-7.774-2.75 c-1.418,1.891-3.659,3.133-6.208,3.133c-2.85,0-5.315-1.547-6.67-3.833C33.617,22.185,25.363,23.692,25.363,25.54z'/><path d='M84.075,54.142c0-8.68-2.868-17.005-8.144-23.829c1.106-1.399,1.41-2.771,1.41-3.854c0-6.574-10.245-9.358-19.264-10.533 c0.209,0.706,0.359,1.439,0.359,2.215c0,0.09-0.022,0.17-0.028,0.26l0,0c-0.028,0.853-0.195,1.667-0.479,2.429 c9.106,1.282,14.508,3.754,14.508,5.63c0,2.644-10.688,6.486-27.439,6.486c-16.748,0-27.438-3.842-27.438-6.486 c0-2.542,9.904-6.183,25.559-6.459c-0.098-0.396-0.159-0.807-0.2-1.223c0.006,0,0.013,0,0.017,0 c-0.017-0.213-0.063-0.417-0.063-0.636c0-1.084,0.226-2.119,0.628-3.058c-6.788,0.129-30.846,1.299-30.846,11.376 c0,1.083,0.305,2.455,1.411,3.854c-5.276,6.823-8.145,15.149-8.145,23.829c0,11.548,5.187,20.107,14.693,25.115 c-0.902,3.146-1.391,7.056,1.111,8.181c2.626,1.178,5.364-2.139,7.111-5.005c4.73,1.261,10.13,1.923,16.161,1.923 c6.034,0,11.428-0.661,16.158-1.922c1.75,2.865,4.493,6.18,7.112,5.004c2.504-1.123,2.014-5.035,1.113-8.179 C78.889,74.249,84.075,65.689,84.075,54.142z M70.39,31.392c5.43,6.046,8.78,14,8.78,22.75c0,20.919-18.582,25.309-34.171,25.309 c-15.587,0-34.17-4.39-34.17-25.309c0-8.75,3.35-16.7,8.781-22.753c5.561,2.643,15.502,4.009,25.389,4.009 C54.886,35.397,64.829,34.031,70.39,31.392z'/><path d='M50.654,23.374c2.892,0,5.234-2.341,5.234-5.233c0-2.887-2.343-5.23-5.234-5.23c-2.887,0-5.231,2.343-5.231,5.23 C45.423,21.032,47.768,23.374,50.654,23.374z'/>
<circle cx="62.905" cy="10.089" r="3.595"/> <circle cx='62.905' cy='10.089' r='3.595'/>
<circle cx="52.616" cy="5.048" r="2.73"/> <circle cx='52.616' cy='5.048' r='2.73'/>
</svg>; </svg>;
}; };

View File

@@ -1,5 +1,6 @@
var React = require('react'); const React = require('react');
const createClass = require('create-react-class');
module.exports = function(props){ module.exports = function(props){
return <svg version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enableBackground="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>; return <svg version='1.1' x='0px' y='0px' viewBox='0 0 100 100' enableBackground='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>;
}; };

7
tests/basic.test.js Normal file
View File

@@ -0,0 +1,7 @@
const test = require('pico-check');
test('Just setting up a spot for future tests', (t)=>{
t.pass();
});
module.exports = test;

1
tests/test.init.js Normal file
View File

@@ -0,0 +1 @@
//Set up configs and DB connectiosna nd what not in here