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

Compare commits

..

96 Commits
v2.13.2 ... v3

Author SHA1 Message Date
Scott Tolksdorf
b39f9041c2 Updating DB to handle newer versions of mongo 2017-05-28 11:24:50 -04:00
Scott Tolksdorf
2023ae4f6a Merge branch 'borderShadows' into v3 2017-03-26 15:18:06 -04:00
Scott Tolksdorf
d5f04ca2b6 Shadows now being drawn as a :after element. Beauty 2017-03-26 15:10:36 -04:00
Scott Tolksdorf
f99bcabad0 Updating the build scripts 2017-03-26 12:20:17 -04:00
Scott Tolksdorf
32317bfa6f Merge branch 'collaspeNav' into snippets 2017-03-21 00:15:34 -04:00
Scott Tolksdorf
1946a50ce0 Converted a few nav items over 2017-03-21 00:15:24 -04:00
Scott Tolksdorf
ee1827eab0 Trying to get it working 2017-03-21 00:09:37 -04:00
Scott Tolksdorf
20371a8b3d Adding the brew title to the print page title, so downloads are named properly 2017-03-19 20:23:19 -04:00
Scott Tolksdorf
28a3f31caa Work on the Table of contents snippet 2017-03-19 17:26:55 -04:00
Scott Tolksdorf
c647bdf5ee Added in stlying for blockquotes and clean up logic in footer 2017-03-19 15:07:00 -04:00
Scott Tolksdorf
eb1827cedb Merge branch 'borderTest' into snippets 2017-03-19 13:51:24 -04:00
Scott Tolksdorf
a3251dfa19 Pseudo borders are now working 2017-03-19 13:50:45 -04:00
Scott Tolksdorf
30d3fcf168 Pseudo element borders are working, holy shit 2017-03-19 13:17:33 -04:00
Scott Tolksdorf
393df1b181 Updating the faqw 2017-03-19 12:28:45 -04:00
Scott Tolksdorf
94a3a96960 Adding to faq 2017-03-03 19:20:27 -05:00
Scott Tolksdorf
bfb2cea48e Working on onster block 2017-02-28 23:21:41 -05:00
Scott Tolksdorf
00f2703d0b Moved files into statics, finally fixed the brew editor breaking on resizze 2017-02-28 21:07:37 -05:00
Scott Tolksdorf
4c874149fb added an internal nested div on block elements 2017-02-26 20:18:44 -05:00
Scott Tolksdorf
0705e08381 Added in stlying for code blocks 2017-02-26 19:49:36 -05:00
Scott Tolksdorf
e112808706 Column split now a key word 2017-02-26 19:38:42 -05:00
Scott Tolksdorf
234d216d64 Adding in notes and adding more to blocks 2017-02-26 17:42:21 -05:00
Scott Tolksdorf
ef0265f4fa moving old snippets into the depricated folder 2017-02-26 13:03:12 -05:00
Scott Tolksdorf
fbc18a017c Creating new stlying for the snippet blocks 2017-02-26 13:01:48 -05:00
Scott Tolksdorf
9d4d337bb9 Snippet bar has been replaced and new style of snippets being worked on 2017-02-26 11:53:40 -05:00
Scott Tolksdorf
0d0ce101f3 Starting to set up the snippets 2017-02-24 00:49:21 -05:00
Scott Tolksdorf
540c00cb0c Merge branch 'noHtml' into v3 2017-02-23 10:07:32 -05:00
Scott Tolksdorf
446ae9cbcf Merge branch 'dualRenderer' into noHtml 2017-02-23 10:07:05 -05:00
Scott Tolksdorf
dc486cfba9 Added nested markdown parsering within blocks 2017-02-23 10:03:04 -05:00
Scott Tolksdorf
a6a1f41e77 Merge branch 'newStyle' into dualRenderer 2017-02-23 09:58:38 -05:00
Scott Tolksdorf
fd567352a4 Moved imgs and fonts into the new style folder 2017-02-23 09:58:10 -05:00
Scott Tolksdorf
a33b1d845d Styling is finally split, oh boy 2017-02-23 08:33:13 -05:00
Scott Tolksdorf
b20f4ffb46 PHB style should be fully scoped 2017-02-23 08:11:48 -05:00
Scott Tolksdorf
2f69ef3fe8 Removing the old server files 2017-02-23 07:41:55 -05:00
Scott Tolksdorf
1da1f90a35 Backing up the todo 2017-02-18 14:29:16 -05:00
Scott Tolksdorf
bd08858745 Split off the old stlying in a separate file 2017-02-13 00:45:17 -05:00
Scott Tolksdorf
304cd0ffcd Getting both renderers to play nice 2017-02-12 23:35:19 -05:00
Scott Tolksdorf
b40e5bc4c4 simplifying the issue template, because no one ever actually uses it 2017-02-12 10:21:02 -05:00
Scott Tolksdorf
0663737e1c moved the old parser and renderer into a depreciated folder 2017-02-04 03:27:10 -05:00
Scott Tolksdorf
307dd2d9ba Adding newlines to div injection for blocks 2017-02-01 23:54:20 -05:00
Scott Tolksdorf
95c91b6ba8 Merge branch 'styleEditor' into noHtml 2017-01-30 10:48:34 -05:00
Scott Tolksdorf
c8c46725a2 Making the error looks better 2017-01-30 10:48:05 -05:00
Scott Tolksdorf
7001b71d91 Lots of progress with the new editor 2017-01-28 22:19:14 -05:00
Scott Tolksdorf
cbab4f4959 added todo 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
22d9982888 Added support for title description and thumbnail images 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
76ced9ca49 Added comma parsing to the block code 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
b1db8040a4 Added a todo for generic line styling 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
c8b089f7fb Added new lexer for handling the new block syntax 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
97c0443c76 Fixed bug where new page was storing null brews 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
c470bed591 'Created 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
4593099914 Merge branch 'newAdmin' into v3 2017-01-28 16:36:25 -05:00
Scott Tolksdorf
6030134de2 New brew search finished on admin page 2017-01-28 16:35:48 -05:00
Scott Tolksdorf
7e6f42f062 Merge branch 'search' into newAdmin 2017-01-28 15:39:05 -05:00
Scott Tolksdorf
75111acefb All tests should be done, phew 2017-01-28 12:25:26 -05:00
Scott Tolksdorf
26bcb3395a Fixing edge cases in the search tests 2017-01-27 19:47:45 -05:00
Scott Tolksdorf
a826aaffd9 created a brew generator and chai plugin for easier testing 2017-01-27 18:38:51 -05:00
Scott Tolksdorf
8018442f25 Upgrading the brew generation for testing 2017-01-27 10:47:38 -05:00
Scott Tolksdorf
8e58e5aca9 MOved the brews search to its own file, writing out more tests 2017-01-27 10:36:07 -05:00
Scott Tolksdorf
2f82d3875e Adding a new script from populating the DB with a bunch of random brews 2017-01-27 09:56:44 -05:00
Scott Tolksdorf
efee8ff05c Basic search is working 2017-01-23 00:35:30 -05:00
Scott Tolksdorf
a405c7cfb2 Stubbing out tests for searching 2017-01-22 13:42:40 -05:00
Scott Tolksdorf
dfcb04fd09 Setting up search tests 2017-01-22 13:42:32 -05:00
Scott Tolksdorf
728277f861 Removing invalid brews is working 2017-01-22 13:41:16 -05:00
Scott Tolksdorf
0878439750 Fixed issue with arrays not being saved 2017-01-22 13:41:08 -05:00
Scott Tolksdorf
e77532acef Added tests for admin routes, lookup is working 2017-01-22 13:41:00 -05:00
Scott Tolksdorf
cd2eb5fdce Adding in lookup route 2017-01-22 13:40:39 -05:00
Scott Tolksdorf
37de888f03 Creating a brand new admin page 2017-01-22 13:40:29 -05:00
Scott Tolksdorf
1aa79b32d9 Remove chrome warnings from after rebase 2017-01-22 12:55:00 -05:00
Scott Tolksdorf
894e345a44 Fixed bug where new page loaded null brews sometimes 2017-01-22 12:49:35 -05:00
Scott Tolksdorf
07f249b23e Local login now working great 2017-01-22 12:49:35 -05:00
Scott Tolksdorf
baaa82ed34 Added in a logout to the user page 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
0d0f0d8eb0 Adding in env configs and aextra protection on dev routes 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
d77fa0a3dc Backend of local login working 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
a26c4e2092 Cleaned up the admin routes 2017-01-22 12:48:48 -05:00
Scott Tolksdorf
ca40ec5a2d Added in full test coverage of current spec 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
987363ed41 Fixing the pathing to the build folder 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
7b38bccec1 config file for tests 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
174c2973f7 Split off app into own file 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
66ca09b36d Both types of tests are now working 2017-01-22 12:46:16 -05:00
Scott Tolksdorf
5820564894 added nodemon'd test npm task 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
3dc4c13178 Something is up 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
537a75b2ab Triyng to setup api tests 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
a0bc4fddf8 moving the db setup out 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
25e0a1607a whatever 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
68ecf749ea Stubbing out test files 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
10f4759471 adding in some api tests 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
5ba3f98696 Finally testing, things should be working a bit better now 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
95c09ba7ad moved welcome message and adding in egads errors 2017-01-22 12:45:16 -05:00
Scott Tolksdorf
1173af5803 'Created 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
924b398768 Added new navitems 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
41303e6918 'Share 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
f75f60aa1e Edit page finally converted over 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
f4cf288f27 newPage is now working, working on editpage 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
8abf6abf99 Updating pico-flux and vitreum to latest 2017-01-22 12:39:42 -05:00
Scott Tolksdorf
95aa803c61 Trying to fix prod builds breaking 2017-01-22 12:39:27 -05:00
Scott Tolksdorf
47396e5c7e Added smart componenets, page line number highlighting 2017-01-22 12:39:06 -05:00
Scott Tolksdorf
7581d155a6 Updating libs and adding basic flux 2017-01-22 12:36:45 -05:00
269 changed files with 8490 additions and 26243 deletions

View File

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

View File

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

View File

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

View File

@@ -1,78 +0,0 @@
module.exports = {
root : true,
parserOptions : {
ecmaVersion : 2021,
sourceType : 'module',
ecmaFeatures : {
jsx : true
}
},
env : {
browser : true,
node : 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', { 'MemberExpression': 'off' }],
'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' : 'off',
'no-trailing-spaces' : 'warn',
'no-whitespace-before-property' : 'warn',
'object-curly-spacing' : ['warn', 'always'],
'react/jsx-indent-props' : ['warn', 'tab'],
'space-in-parens' : ['warn', 'never'],
'template-curly-spacing' : ['warn', 'never'],
}
};

1
.gitattributes vendored
View File

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

View File

@@ -1,69 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 99
ignore:
- dependency-name: eslint
versions:
- 7.19.0
- 7.22.0
- 7.23.0
- 7.24.0
- dependency-name: "@babel/core"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.13
- 7.13.14
- 7.13.15
- dependency-name: googleapis
versions:
- 68.0.0
- 70.0.0
- 71.0.0
- dependency-name: "@babel/preset-env"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.0
- 7.13.12
- 7.13.8
- dependency-name: mongoose
versions:
- 5.11.14
- 5.11.15
- 5.11.16
- 5.11.17
- 5.11.18
- 5.11.19
- 5.12.1
- 5.12.2
- 5.12.3
- dependency-name: eslint-plugin-react
versions:
- 7.23.0
- 7.23.1
- dependency-name: query-string
versions:
- 7.0.0
- dependency-name: nanoid
versions:
- 3.1.22
- dependency-name: "@babel/preset-react"
versions:
- 7.13.13
- dependency-name: codemirror
versions:
- 5.59.3
- 5.60.0
- dependency-name: classnames
versions:
- 2.3.0
- dependency-name: marked
versions:
- 1.2.8

View File

@@ -1,28 +1,5 @@
<!-- 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.
Share link to issue brew: http://homebrewery.naturalcrit.com/share/XXXXXXX
*Delete the above section and the instructions in the sections below before submitting*
## Description
If this is a *feature request*, explain why it should be added. Specific use-cases are best.
For *bug reports*, please provide as much *relevant* info as possible.
**Share Link** :
or
**Brew code to reproduce** : <details><summary>Click to expand</summary><code><pre>
PASTE BREW CODE HERE
</pre></code></details>

18
.gitignore vendored
View File

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

View File

@@ -1,19 +0,0 @@
FROM node:14.15
ENV NODE_ENV=docker
# Create app directory
WORKDIR /usr/src/app
# Copy package.json into the image, then run yarn install
# This improves caching so we don't have to download the dependencies every time the code changes
COPY package.json ./
# --ignore-scripts tells yarn not to run postbuild. We run it explicitly later
RUN yarn install --ignore-scripts
# Bundle app source and build application
COPY . .
RUN yarn build
EXPOSE 8000
CMD [ "yarn", "start" ]

View File

@@ -1,12 +0,0 @@
# Running Homebrewery via Docker
The repo includes a Dockerfile and a docker-compose.yml file.
To run the application via docker-compose.yml:
`docker-compose up -d`
To stop the application:
`docker-compose down`
To stop the application and remove all data:
`docker-compose down -v`

View File

@@ -1,35 +0,0 @@
# FreeBSD/FreeNAS Installation Instructions
## Before Installing
These instructions assume that you are installing to a completely new, fresh FreeBSD/FreeNAS jail. As such, some steps will not be necessary if you are installing to an existing FreeBSD/FreeNAS install.
## Installation instructions
1. Create a new jail, with the appropriate network settings to access the internet.
2. Install wget (`pkg install -y wget`). On a fresh jail, you will be prompted to press 'Y' to set up `pkg`.
3. Download the installation script (`wget --no-check-certificate https://raw.githubusercontent.com/naturalcrit/homebrewery/master/freebsd/install.sh`). The parameter `--no-check-certificate` is required as we haven't set up any trusted certificates/authorities yet.
4. Make the downloaded file executable (`chmod +x install.sh`).
5. Run the script (`./install.sh`). This will automatically download all of the required packages, install both them and HomeBrewery, configure the system and finally start HomeBrewery.
**NOTE:** At this time, the script **ONLY** installs HomeBrewery. It does **NOT** install the NaturalCrit login system, as that is currently a completely separate project.
---
### Testing
These installation instructions have been tested on the following FreeBSD/FreeNAS platforms:
* FreeNAS-11.3-U5; Jail 11.4-RELEASE-p2
## Final Notes
While this installation process works successfully at the time of writing (December 28, 2020), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function under FreeBSD. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation under FreeBSD may fail without warning at some point in the future.
Regards,
G
December 28, 2020

128
README.md
View File

@@ -1,127 +1,33 @@
# 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).
[![Homebrewery](https://circleci.com/gh/naturalcrit/homebrewery/tree/master.svg?style=svg)](https://app.circleci.com/pipelines/github/naturalcrit/homebrewery?branch=master)
The Homebrewery is a tool for making authentic looking [D&D content][dnd-content-url]
using [Markdown][markdown-url]. It is distributed under the terms of the [MIT License](./license).
### issues, suggestions, bugs
If you run into any issues using The Homebrewery, please submit an issues [here](/issues)
[dnd-content-url]: https://dnd.wizards.com/products/tabletop-games/rpg-products/rpg_playershandbook
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
## Quick Start
The easiest way to get started using the Homebrewery is to use it
[on our website][homebrewery-url]. The code is open source, so feel free to
clone it, tinker with it. If you want to make changes to the code, you can run
your own local version for testing by following the installation instructions
below.
[homebrewery-url]: https://homebrewery.naturalcrit.com
### Installation
First, install three programs that the Homebrewery requires to run and retrieve
updates:
### local dev
Homebrewery is open source, so feel free to clone it, tinker with it, or run your own local version.
#### pre-reqs
1. install [node](https://nodejs.org/en/)
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
For easiest installation, follow these steps:
1. In the installer, uncheck the option to run as a service
1. You can install MongoDB Compass if you want a GUI to view your database documents
1. Go to the C drive and create a folder called "data"
1. Inside the "data" folder, create a new folder called "db"
1. Open a command prompt or other terminal and navigate to your mongodb install folder (c:program files\mongo\server\4.4\bin)
1. In the command prompt, run "mongod", which will start up your local database server
1. While MongoD is running, open a second command prompt and navigate to the mongodb install folder
1. In the second command prompt, run "mongo", which allows you to edit the database
1. Type `use homebrewery` to create the homebrewery database. You should see `switched to db homebrewery`
1. Type `db.brews.insert({"title":"test"})` to create a blank document. You should see `WriteResult({ "nInserted" : 1 })`
1. Search in Windows for "Advanced system settings" and open it
1. Click "Environment variables", find the "path" variable, and double-click to open it
1. Click "New" and paste in the path to the mongodb "bin" folder
1. Click "OK", "OK", "OK" to close all the windows
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt)
Checkout the repo ([documentation][github-clone-repo-docs-url]):
```
git clone https://github.com/naturalcrit/homebrewery.git
```
[github-clone-repo-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/cloning-a-repository
Second, you will need to add the environment variable `NODE_ENV=local` to allow
the project to run locally.
You can set this temporarily in your shell of choice:
* Windows Powershell: `$env:NODE_ENV="local"`
* Windows CMD: `set NODE_ENV=local`
* Linux / OSX: `export NODE_ENV=local`
Third, you will need to install the Node dependencies, compile the app, and run
it using the two commands:
1. install [mongodb](https://www.mongodb.com/)
#### getting started
1. clone it
1. `npm install`
1. `npm build`
1. `npm start`
You should now be able to go to [http://localhost:8000](http://localhost:8000)
in your browser and use the Homebrewery offline.
#### 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)
### Running the application via Docker
If you are developing locally and would like to generate your own, follow the above steps and then run `npm run phb`.
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
### changelog
### Running the application on FreeBSD or FreeNAS
You can check out the changelog [here](https://github.com/stolksdorf/homebrewery/blob/master/changelog.md)
Please see the docs here: [README.FreeBSD.md](./README.FREEBSD.md)
### license
### Standalone PHB Stylesheet
If you just want the stylesheet that is generated to make pages look like they
are from the Player's Handbook, you will find it in the
[phb.standalone.css](./phb.standalone.css) file.
If you are developing locally and would like to generate your own, follow the
above steps and then run `npm run phb`.
## Issues, Suggestions, and Bugs
If you run into any issues using The Homebrewery or have suggestions for
improvement, please submit an issue [on GitHub][repo-issues-url].
You can also get help for issues on the subreddit [r/homebrewery][subreddit-url]
[repo-issues-url]: https://github.com/naturalcrit/homebrewery/issues
[subreddit-url]: https://www.reddit.com/r/homebrewery
## Changelog
You can check out the [changelog](./changelog.md).
## License
This project is licensed under the [MIT license](./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.
## Contributing
You are welcome to contribute to the development and maintenance of the
project! There are several ways of doing that:
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some
of them are outdated, duplicates or doesn't contain any useful info. In order
to help you can [mark duplicates][github-mark-duplicate-url], try to
reproduce some complex or weird issues, try with finding a workaround for a
reported bug or just mention issue managers team to let them know about
outdated issue via `@naturalcrit/issue-managers`.
- Our [subreddit][subreddit-url] is constantly growing and there are number of
bug reports: any help with sorting them out is very welcome.
- And of course you can contribute by fixing a bug or implementing a new
feature by yourself, we are waiting for your
[pull requests][github-pr-docs-url]!
Anyway, if you would like to get in touch with the team and discuss/coordinate
your contribution to the project, please join our [gitter chat][gitter-url].
[github-mark-duplicate-url]: https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/about-duplicate-issues-and-pull-requests
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
[gitter-url]: https://gitter.im/naturalcrit/Lobby
This project is licensed under [MIT](./license)

View File

@@ -1,38 +1,42 @@
require('./admin.less');
const React = require('react');
const createClass = require('create-react-class');
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
const BrewLookup = require('./brewLookup/brewLookup.jsx');
const BrewCompress = require ('./brewCompress/brewCompress.jsx');
const Stats = require('./stats/stats.jsx');
const Admin = createClass({
getDefaultProps : function() {
return {};
},
render : function(){
return <div className='admin'>
<header>
<div className='container'>
<i className='fas fa-rocket' />
homebrewery admin
</div>
</header>
<div className='container'>
<Stats />
<hr />
<BrewLookup />
<hr />
<BrewCleanup />
<hr />
<BrewCompress />
</div>
</div>;
}
});
module.exports = Admin;
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const BrewLookup = require('./brewLookup/brewLookup.jsx');
const AdminSearch = require('./adminSearch/adminSearch.jsx');
const InvalidBrew = require('./invalidBrew/invalidBrew.jsx');
const Admin = React.createClass({
getDefaultProps: function() {
return {
admin_key : '',
};
},
renderNavbar : function(){
return <Nav.base>
<Nav.section>
<Nav.item icon='fa-magic' className='homebreweryLogo'>
Homebrewery Admin
</Nav.item>
</Nav.section>
</Nav.base>
},
render : function(){
return <div className='admin'>
{this.renderNavbar()}
<main className='content'>
<BrewLookup adminKey={this.props.admin_key} />
<AdminSearch adminKey={this.props.admin_key} />
<div className='dangerZone'>Danger Zone</div>
<InvalidBrew adminKey={this.props.admin_key} />
</main>
</div>
}
});
module.exports = Admin;

View File

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

View File

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

View File

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

View File

@@ -1,75 +0,0 @@
require('./brewCleanup.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const request = require('superagent');
const BrewCleanup = createClass({
displayName : 'BrewCleanup',
getDefaultProps(){
return {};
},
getInitialState() {
return {
count : 0,
pending : false,
primed : false,
err : null
};
},
prime(){
this.setState({ pending: true });
request.get('/admin/cleanup')
.then((res)=>this.setState({ count: res.body.count, primed: true }))
.catch((err)=>this.setState({ error: err }))
.finally(()=>this.setState({ pending: false }));
},
cleanup(){
this.setState({ pending: true });
request.post('/admin/cleanup')
.then((res)=>this.setState({ count: res.body.count }))
.catch((err)=>this.setState({ error: err }))
.finally(()=>this.setState({ pending: false, primed: false }));
},
renderPrimed(){
if(!this.state.primed) return;
if(!this.state.count){
return <div className='removeBox'>No Matching Brews found.</div>;
}
return <div className='removeBox'>
<button onClick={this.cleanup} className='remove'>
{this.state.pending
? <i className='fas fa-spin fa-spinner' />
: <span><i className='fas fa-times' /> Remove</span>
}
</button>
<span>Found {this.state.count} Brews that could be removed. </span>
</div>;
},
render(){
return <div className='BrewCleanup'>
<h2> Brew Cleanup </h2>
<p>Removes very short brews to tidy up the database</p>
<button onClick={this.prime} className='query'>
{this.state.pending
? <i className='fas fa-spin fa-spinner' />
: 'Query Brews'
}
</button>
{this.renderPrimed()}
{this.state.error
&& <div className='error'>{this.state.error.toString()}</div>
}
</div>;
}
});
module.exports = BrewCleanup;

View File

@@ -1,10 +0,0 @@
.BrewCleanup{
.removeBox{
margin-top: 20px;
button{
background-color: @red;
margin-right: 10px;
}
}
}

View File

@@ -1,92 +0,0 @@
require('./brewCompress.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const request = require('superagent');
const BrewCompress = createClass({
displayName : 'BrewCompress',
getDefaultProps(){
return {};
},
getInitialState() {
return {
count : 0,
batchRange : 0,
pending : false,
primed : false,
err : null,
ids : null
};
},
prime(){
this.setState({ pending: true });
request.get('/admin/finduncompressed')
.then((res)=>this.setState({ count: res.body.count, primed: true, ids: res.body.ids }))
.catch((err)=>this.setState({ error: err }))
.finally(()=>this.setState({ pending: false }));
},
cleanup(){
const brews = this.state.ids;
const compressBatches = ()=>{
if(brews.length == 0){
this.setState({ pending: false, primed: false });
return;
}
const batch = brews.splice(0, 1000); // Process brews in batches of 1000
this.setState({ batchRange: this.state.count - brews.length });
batch.forEach((id, idx)=>{
request.put(`/admin/compress/${id}`)
.catch((err)=>this.setState({ error: err }));
});
setTimeout(compressBatches, 10000); //Wait 10 seconds between batches
};
this.setState({ pending: true });
compressBatches();
},
renderPrimed(){
if(!this.state.primed) return;
if(!this.state.count){
return <div className='removeBox'>No Matching Brews found.</div>;
}
return <div className='removeBox'>
<button onClick={this.cleanup} className='remove'>
{this.state.pending
? <i className='fas fa-spin fa-spinner' />
: <span><i className='fas fa-compress' /> compress </span>
}
</button>
{this.state.pending
? <span>Compressing {this.state.batchRange} brews. </span>
: <span>Found {this.state.count} Brews that could be compressed. </span>
}
</div>;
},
render(){
return <div className='BrewCompress'>
<h2> Brew Compression </h2>
<p>Compresses the text in brews to binary</p>
<button onClick={this.prime} className='query'>
{this.state.pending
? <i className='fas fa-spin fa-spinner' />
: 'Query Brews'
}
</button>
{this.renderPrimed()}
{this.state.error
&& <div className='error'>{this.state.error.toString()}</div>
}
</div>;
}
});
module.exports = BrewCompress;

View File

@@ -1,10 +0,0 @@
.BrewCompress{
.removeBox{
margin-top: 20px;
button{
background-color: @red;
margin-right: 10px;
}
}
}

View File

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

View File

@@ -1,30 +1,13 @@
.brewLookup{
height : 200px;
input{
height : 33px;
margin-bottom : 20px;
padding : 0px 10px;
font-family : monospace;
}
button{
vertical-align : middle;
height : 37px;
}
dl{
@maxItemWidth : 132px;
dt{
float : left;
clear : left;
width : @maxItemWidth;
text-align : right;
&::after {
content: " : ";
}
}
dd{
height : 1em;
margin-left : @maxItemWidth + 6px;
padding : 0 0 0.5em 0;
}
.error{
font-weight : 800;
color : @red;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,46 +0,0 @@
require('./stats.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const request = require('superagent');
const Stats = createClass({
displayName : 'Stats',
getDefaultProps(){
return {};
},
getInitialState(){
return {
stats : {
totalBrews : 0
},
fetching : false
};
},
componentDidMount(){
this.fetchStats();
},
fetchStats(){
this.setState({ fetching: true });
request.get('/admin/stats')
.then((res)=>this.setState({ stats: res.body }))
.finally(()=>this.setState({ fetching: false }));
},
render(){
return <div className='Stats'>
<h2> Stats </h2>
<dl>
<dt>Total Brew Count</dt>
<dd>{this.state.stats.totalBrews}</dd>
</dl>
{this.state.fetching
&& <div className='pending'><i className='fas fa-spin fa-spinner' /></div>
}
</div>;
}
});
module.exports = Stats;

View File

@@ -1,28 +0,0 @@
.Stats{
position : relative;
.pending{
position : absolute;
top : 0px;
left : 0px;
height : 100%;
width : 100%;
background-color : rgba(238,238,238, 0.5);
}
dl{
@maxItemWidth : 132px;
dt{
float : left;
clear : left;
width : @maxItemWidth;
text-align : right;
&::after {
content: " : ";
}
}
dd{
margin : 0 0 0 @maxItemWidth + 10px;
padding : 0 0 0.5em 0;
}
}
}

View File

@@ -1,217 +0,0 @@
require('./brewRenderer.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx');
//TODO: move to the brew renderer
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
const Frame = require('react-frame-component').default;
const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50;
const BrewRenderer = createClass({
getDefaultProps : function() {
return {
text : '',
style : '',
renderer : 'legacy',
errors : []
};
},
getInitialState : function() {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page/gm);
}
return {
viewablePageNumber : 0,
height : 0,
isMounted : false,
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD,
visibility : 'hidden',
initialContent : `<!DOCTYPE html><html><head>
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`
};
},
height : 0,
lastRender : <div></div>,
componentWillUnmount : function() {
window.removeEventListener('resize', this.updateSize);
},
componentDidUpdate : function(prevProps) {
if(prevProps.text !== this.props.text) {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page/gm);
}
this.setState({
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD
});
}
},
updateSize : function() {
this.setState({
height : this.refs.main.parentNode.clientHeight,
});
},
handleScroll : function(e){
const target = e.target;
this.setState((prevState)=>({
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
}));
},
shouldRender : function(pageText, index){
if(!this.state.isMounted) return false;
const viewIndex = this.state.viewablePageNumber;
if(index == viewIndex - 3) return true;
if(index == viewIndex - 2) return true;
if(index == viewIndex - 1) return true;
if(index == viewIndex) return true;
if(index == viewIndex + 1) return true;
if(index == viewIndex + 2) return true;
if(index == viewIndex + 3) return true;
//Check for style tages
if(pageText.indexOf('<style>') !== -1) return true;
return false;
},
renderPageInfo : function(){
return <div className='pageInfo' ref='main'>
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div>;
},
renderPPRmsg : function(){
if(!this.state.usePPR) return;
return <div className='ppr_msg'>
Partial Page Renderer enabled, because your brew is so large. May effect rendering.
</div>;
},
renderDummyPage : function(index){
return <div className='phb' id={`p${index + 1}`} key={index}>
<i className='fas fa-spinner fa-spin' />
</div>;
},
renderStyle : function() {
if(!this.props.style) return;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />;
},
renderPage : function(pageText, index){
if(this.props.renderer == 'legacy')
return <div className='phb page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }} key={index} />;
else
return <div className='phb3 page' id={`p${index + 1}`} dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} key={index} />;
},
renderPages : function(){
if(this.state.usePPR){
return _.map(this.state.pages, (page, index)=>{
if(this.shouldRender(page, index) && typeof window !== 'undefined'){
return this.renderPage(page, index);
} else {
return this.renderDummyPage(index);
}
});
}
if(this.props.errors && this.props.errors.length) return this.lastRender;
this.lastRender = _.map(this.state.pages, (page, index)=>{
if(typeof window !== 'undefined') {
return this.renderPage(page, index);
} else {
return this.renderDummyPage(index);
}
});
return this.lastRender;
},
frameDidMount : function(){ //This triggers when iFrame finishes internal "componentDidMount"
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
this.updateSize();
window.addEventListener('resize', this.updateSize);
this.renderPages(); //Make sure page is renderable before showing
this.setState({
isMounted : true,
visibility : 'visible'
});
}, 100);
},
render : function(){
//render in iFrame so broken code doesn't crash the site.
//Also render dummy page while iframe is mounting.
return (
<React.Fragment>
{!this.state.isMounted
? <div className='brewRenderer' onScroll={this.handleScroll}>
<div className='pages' ref='pages'>
{this.renderDummyPage(1)}
</div>
</div>
: null}
<Frame initialContent={this.state.initialContent}
head = <link href={`${this.props.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
style={{ width: '100%', height: '100%', visibility: this.state.visibility }}
contentDidMount={this.frameDidMount}>
<div className={'brewRenderer'}
onScroll={this.handleScroll}
style={{ height: this.state.height }}>
<ErrorBar errors={this.props.errors} />
<div className='popups'>
<RenderWarnings />
<NotificationPopup />
</div>
<div className='pages' ref='pages'>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{this.state.isMounted
&&
<>
{this.renderStyle()}
{this.renderPages()}
</>
}
</div>
</div>
</Frame>
{this.renderPageInfo()}
{this.renderPPRmsg()}
</React.Fragment>
);
}
});
module.exports = BrewRenderer;

View File

@@ -1,40 +0,0 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer{
will-change : transform;
overflow-y : scroll;
.pages{
margin : 30px 0px;
&>.page{
margin-right : auto;
margin-bottom : 30px;
margin-left : auto;
box-shadow : 1px 4px 14px #000;
}
}
}
.pane{
position : relative;
}
.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,72 +0,0 @@
require('./notificationPopup.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames'); //Unused variable
const DISMISS_KEY = 'dismiss_notification7-10-20';
const NotificationPopup = createClass({
getInitialState : function() {
return {
notifications : {}
};
},
componentDidMount : function() {
this.checkNotifications();
window.addEventListener('resize', this.checkNotifications);
},
componentWillUnmount : function() {
window.removeEventListener('resize', this.checkNotifications);
},
notifications : {
psa : function(){
return <li key='psa'>
<em>Google Drive Integration!</em> <br />
We have added Google Drive integration to the Homebrewery! <a target='_blank' href='https://www.naturalcrit.com/login'>Sign in</a> with
your Google account to link it with your Homebrewery profile. A new button in the Edit page will let you transfer your file to your personal
Google Drive storage, and Google will keep a backup of each version! No more lost work surprises!
<br /><br />
However, we are aware that there may be uncaught bugs. We encourage you to copy your brew into a text document before transferring to Google
Drive just in case any issues arise as this update is rolled out.
<br /><br />
<b>Note:</b> Transferring an existing brew to Google Drive will change the edit and share links of your document. If you have shared your
document online, remember to update the links there as well.
</li>;
},
faq : function(){
return <li key='faq'>
<em>Protect your work! </em> <br />
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews! &nbsp;
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
See the FAQ
</a> to learn how to avoid losing your work!
</li>;
},
},
checkNotifications : function(){
const hideDismiss = localStorage.getItem(DISMISS_KEY);
if(hideDismiss) return this.setState({ notifications: {} });
this.setState({
notifications : _.mapValues(this.notifications, (fn)=>{ return fn(); }) //Convert notification functions into their return text value
});
},
dismiss : function(){
localStorage.setItem(DISMISS_KEY, true);
this.checkNotifications();
},
render : function(){
if(_.isEmpty(this.state.notifications)) return null;
return <div className='notificationPopup'>
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
<i className='fas fa-info-circle info' />
<h3>Notice</h3>
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
<ul>{_.values(this.state.notifications)}</ul>
</div>;
}
});
module.exports = NotificationPopup;

View File

@@ -1,62 +0,0 @@
.popups{
position : fixed;
top : @navbarHeight;
right : 15px;
z-index : 10001;
width : 350px;
}
.notificationPopup{
position : relative;
float : right;
display : inline-block;
width : 350px;
padding : 15px;
padding-bottom : 10px;
padding-left : 55px;
background-color : @blue;
color : white;
a{
color : @steel;
font-weight : 800;
}
i.info{
position : absolute;
top : 12px;
left : 12px;
opacity : 0.8;
font-size : 2.5em;
}
i.dismiss{
position : absolute;
top : 10px;
right : 10px;
cursor : pointer;
opacity : 0.6;
&:hover{
opacity : 1;
}
}
small{
opacity : 0.7;
font-size : 0.6em;
}
h3{
font-size : 1.1em;
font-weight : 800;
}
ul{
margin-top : 15px;
font-size : 0.8em;
list-style-position : outside;
list-style-type : disc;
li{
font-size : 0.8em;
line-height : 1.4em;
margin-top : 1.4em;
em{
font-weight : 800;
}
}
}
}

View File

@@ -1,216 +0,0 @@
require('./editor.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const dedent = require('dedent-tabs').default;
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const SNIPPETBAR_HEIGHT = 25;
const DEFAULT_STYLE_TEXT = dedent`
/*=======--- Example CSS styling ---=======*/
/* Any CSS here will apply to your document! */
.myExampleClass {
color: black;
}`;
const splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index);
};
const Editor = createClass({
getDefaultProps : function() {
return {
brew : {
text : '',
style : ''
},
onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
renderer : 'legacy'
};
},
getInitialState : function() {
return {
view : 'text' //'text', 'style', 'meta'
};
},
isText : function() {return this.state.view == 'text';},
isStyle : function() {return this.state.view == 'style';},
isMeta : function() {return this.state.view == 'meta';},
componentDidMount : function() {
this.updateEditorSize();
this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize);
},
componentWillUnmount : function() {
window.removeEventListener('resize', this.updateEditorSize);
},
updateEditorSize : function() {
if(this.refs.codeEditor) {
let paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
}
},
handleInject : function(injectText){
const text = (this.isText() ? this.props.brew.text : this.props.brew.style);
const lines = text.split('\n');
const cursorPos = this.refs.codeEditor.getCursorPosition();
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectText.split('\n').length, cursorPos.ch + injectText.length);
if(this.isText()) this.props.onTextChange(lines.join('\n'));
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
},
handleViewChange : function(newView){
this.setState({
view : newView
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
},
getCurrentPage : function(){
const lines = this.props.brew.text.split('\n').slice(0, this.cursorPosition.line + 1);
return _.reduce(lines, (r, line)=>{
if(line.indexOf('\\page') !== -1) r++;
return r;
}, 1);
},
highlightCustomMarkdown : function(){
if(!this.refs.codeEditor) return;
if(this.state.view === 'text') {
const codeMirror = this.refs.codeEditor.codeMirror;
//reset custom text styles
const customHighlights = codeMirror.getAllMarks();
for (let i=0;i<customHighlights.length;i++) customHighlights[i].clear();
const lineNumbers = _.reduce(this.props.brew.text.split('\n'), (r, line, lineNumber)=>{
//reset custom line styles
codeMirror.removeLineClass(lineNumber, 'background');
codeMirror.removeLineClass(lineNumber, 'text');
// Legacy Codemirror styling
if(this.props.renderer == 'legacy') {
if(line.includes('\\page')){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
}
// New Codemirror styling for V3 renderer
if(this.props.renderer == 'V3') {
if(line.startsWith('\\page')){
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
r.push(lineNumber);
}
if(line.match(/^\\column$/)){
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
r.push(lineNumber);
}
// Highlight inline spans {{content}}
if(line.includes('{{') && line.includes('}}')){
const regex = /{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])*\s*|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
if(match[0].startsWith('{')) {
blockCount += 1;
} else {
blockCount -= 1;
}
if(blockCount < 0) {
blockCount = 0;
continue;
}
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'inline-block' });
}
} else if(line.trimLeft().startsWith('{{') || line.trimLeft().startsWith('}}')){
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?::(?:"[\w,\-()#%. ]*"|[\w\,\-()#%.]*)|[^"'{}\s])* *$|^ *}}$/);
if(match)
endCh = match.index+match[0].length;
codeMirror.markText({ line: lineNumber, ch: 0 }, { line: lineNumber, ch: endCh }, { className: 'block' });
}
}
return r;
}, []);
return lineNumbers;
}
},
brewJump : function(){
const currentPage = this.getCurrentPage();
window.location.hash = `p${currentPage}`;
},
//Called when there are changes to the editor's dimensions
update : function(){
this.refs.codeEditor?.updateSize();
},
renderEditor : function(){
if(this.isText()){
return <CodeEditor key='text'
ref='codeEditor'
language='gfm'
value={this.props.brew.text}
onChange={this.props.onTextChange} />;
}
if(this.isStyle()){
return <CodeEditor key='style'
ref='codeEditor'
language='css'
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange} />;
}
if(this.isMeta()){
return <MetadataEditor
metadata={this.props.brew}
onChange={this.props.onMetaChange} />;
}
},
render : function(){
this.highlightCustomMarkdown();
return (
<div className='editor' ref='main'>
<SnippetBar
brew={this.props.brew}
view={this.state.view}
onViewChange={this.handleViewChange}
onInject={this.handleInject}
showEditButtons={this.props.showEditButtons}
renderer={this.props.renderer} />
{this.renderEditor()}
</div>
);
}
});
module.exports = Editor;

View File

@@ -1,222 +0,0 @@
require('./metadataEditor.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
const MetadataEditor = createClass({
getDefaultProps : function() {
return {
metadata : {
editId : null,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : [],
renderer : 'legacy'
},
onChange : ()=>{}
};
},
handleFieldChange : function(name, e){
this.props.onChange(_.merge({}, this.props.metadata, {
[name] : e.target.value
}));
},
handleSystem : function(system, e){
if(e.target.checked){
this.props.metadata.systems.push(system);
} else {
this.props.metadata.systems = _.without(this.props.metadata.systems, system);
}
this.props.onChange(this.props.metadata);
},
handleRenderer : function(renderer, e){
if(e.target.checked){
this.props.metadata.renderer = renderer;
}
this.props.onChange(this.props.metadata);
},
handlePublish : function(val){
this.props.onChange(_.merge({}, this.props.metadata, {
published : val
}));
},
handleDelete : function(){
if(this.props.metadata.authors && this.props.metadata.authors.length <= 1){
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
} else {
if(!confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
}
request.delete(`/api/${this.props.metadata.editId}`)
.send()
.end(function(err, res){
window.location.href = '/';
});
},
getRedditLink : function(){
const meta = this.props.metadata;
const shareLink = (meta.googleId || '') + meta.shareId;
const title = `${meta.title} [${meta.systems.join(' ')}]`;
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](https://homebrewery.naturalcrit.com/share/${shareLink})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
},
renderSystems : function(){
return _.map(SYSTEMS, (val)=>{
return <label key={val}>
<input
type='checkbox'
checked={_.includes(this.props.metadata.systems, val)}
onChange={(e)=>this.handleSystem(val, e)} />
{val}
</label>;
});
},
renderPublish : function(){
if(this.props.metadata.published){
return <button className='unpublish' onClick={()=>this.handlePublish(false)}>
<i className='fas fa-ban' /> unpublish
</button>;
} else {
return <button className='publish' onClick={()=>this.handlePublish(true)}>
<i className='fas fa-globe' /> publish
</button>;
}
},
renderDelete : function(){
if(!this.props.metadata.editId) return;
return <div className='field delete'>
<label>delete</label>
<div className='value'>
<button className='publish' onClick={this.handleDelete}>
<i className='fas fa-trash-alt' /> delete brew
</button>
</div>
</div>;
},
renderAuthors : function(){
let text = 'None.';
if(this.props.metadata.authors && this.props.metadata.authors.length){
text = this.props.metadata.authors.join(', ');
}
return <div className='field authors'>
<label>authors</label>
<div className='value'>
{text}
</div>
</div>;
},
renderShareToReddit : function(){
if(!this.props.metadata.shareId) return;
return <div className='field reddit'>
<label>reddit</label>
<div className='value'>
<a href={this.getRedditLink()} target='_blank' rel='noopener noreferrer'>
<button className='publish'>
<i className='fab fa-reddit-alien' /> share to reddit
</button>
</a>
</div>
</div>;
},
renderRenderOptions : function(){
if(!global.enable_v3) return;
return <div className='field systems'>
<label>Renderer</label>
<div className='value'>
<label key='legacy'>
<input
type='radio'
value = 'legacy'
name = 'renderer'
checked={this.props.metadata.renderer === 'legacy'}
onChange={(e)=>this.handleRenderer('legacy', e)} />
Legacy
</label>
<label key='V3'>
<input
type='radio'
value = 'V3'
name = 'renderer'
checked={this.props.metadata.renderer === 'V3'}
onChange={(e)=>this.handleRenderer('V3', e)} />
V3
</label>
</div>
</div>;
},
render : function(){
return <div className='metadataEditor'>
<div className='field title'>
<label>title</label>
<input type='text' className='value'
value={this.props.metadata.title}
onChange={(e)=>this.handleFieldChange('title', e)} />
</div>
<div className='field description'>
<label>description</label>
<textarea value={this.props.metadata.description} className='value'
onChange={(e)=>this.handleFieldChange('description', e)} />
</div>
{/*}
<div className='field tags'>
<label>tags</label>
<textarea value={this.props.metadata.tags}
onChange={(e)=>this.handleFieldChange('tags', e)} />
</div>
*/}
{this.renderAuthors()}
<div className='field systems'>
<label>systems</label>
<div className='value'>
{this.renderSystems()}
</div>
</div>
{this.renderRenderOptions()}
<div className='field publish'>
<label>publish</label>
<div className='value'>
{this.renderPublish()}
<small>Published homebrews will be publicly viewable and searchable (eventually...)</small>
</div>
</div>
{this.renderShareToReddit()}
{this.renderDelete()}
</div>;
}
});
module.exports = MetadataEditor;

View File

@@ -1,129 +0,0 @@
require('./snippetbar.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const SnippetsLegacy = require('./snippetsLegacy/snippets.js');
const SnippetsV3 = require('./snippets/snippets.js');
const execute = function(val, brew){
if(_.isFunction(val)) return val(brew);
return val;
};
const Snippetbar = createClass({
getDefaultProps : function() {
return {
brew : {},
view : 'text',
onViewChange : ()=>{},
onInject : ()=>{},
onToggle : ()=>{},
showEditButtons : true,
renderer : 'legacy'
};
},
getInitialState : function() {
return {
renderer : this.props.renderer
};
},
handleSnippetClick : function(injectedText){
this.props.onInject(injectedText);
},
renderSnippetGroups : function(){
let snippets = [];
if(this.props.view === 'text') {
if(this.props.renderer === 'V3')
snippets = SnippetsV3;
else
snippets = SnippetsLegacy;
}
return _.map(snippets, (snippetGroup)=>{
return <SnippetGroup
brew={this.props.brew}
groupName={snippetGroup.groupName}
icon={snippetGroup.icon}
snippets={snippetGroup.snippets}
key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick}
/>;
});
},
renderEditorButtons : function(){
if(!this.props.showEditButtons) return;
return <div className='editors'>
<div className={cx('text', { selected: this.props.view === 'text' })}
onClick={()=>this.props.onViewChange('text')}>
<i className='fa fa-beer' />
</div>
<div className={cx('style', { selected: this.props.view === 'style' })}
onClick={()=>this.props.onViewChange('style')}>
<i className='fa fa-paint-brush' />
</div>
<div className={cx('meta', { selected: this.props.view === 'meta' })}
onClick={()=>this.props.onViewChange('meta')}>
<i className='fas fa-info-circle' />
</div>
</div>;
},
render : function(){
return <div className='snippetBar'>
{this.renderSnippetGroups()}
{this.renderEditorButtons()}
</div>;
}
});
module.exports = Snippetbar;
const SnippetGroup = createClass({
getDefaultProps : function() {
return {
brew : {},
groupName : '',
icon : 'fas fa-rocket',
snippets : [],
onSnippetClick : function(){},
};
},
handleSnippetClick : function(snippet){
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
},
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
<i className={snippet.icon} />
{snippet.name}
</div>;
});
},
render : function(){
return <div className='snippetGroup snippetBarButton'>
<div className='text'>
<i className={this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets()}
</div>
</div>;
},
});

View File

@@ -1,90 +0,0 @@
.snippetBar{
@menuHeight : 25px;
position : relative;
height : @menuHeight;
background-color : #ddd;
.editors{
position : absolute;
display : flex;
top : 0px;
right : 0px;
height : @menuHeight;
width : 90px;
justify-content : space-between;
&>div{
height : @menuHeight;
width : @menuHeight;
cursor : pointer;
line-height : @menuHeight;
text-align : center;
&:hover,&.selected{
background-color : #999;
}
&.text{
.tooltipLeft('Brew Editor');
}
&.style{
.tooltipLeft('Style Editor');
}
&.meta{
.tooltipLeft('Properties');
}
}
}
.snippetBarButton{
height : @menuHeight;
line-height : @menuHeight;
display : inline-block;
padding : 0px 5px;
font-weight : 800;
font-size : 0.625em;
text-transform : uppercase;
cursor : pointer;
&:hover, &.selected{
background-color : #999;
}
i{
vertical-align : middle;
margin-right : 3px;
font-size : 1.4em;
}
}
.toggleMeta{
position : absolute;
top : 0px;
right : 0px;
border-left : 1px solid black;
.tooltipLeft("Edit Brew Properties");
}
.snippetGroup{
border-right : 1px solid black;
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown{
position : absolute;
top : 100%;
visibility : hidden;
z-index : 1000;
margin-left : -5px;
padding : 0px;
background-color : #ddd;
.snippet{
.animate(background-color);
padding : 5px;
cursor : pointer;
font-size : 10px;
i{
margin-right : 8px;
font-size : 1.2em;
}
&:hover{
background-color : #999;
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,170 +0,0 @@
const _ = require('lodash');
const dedent = require('dedent-tabs').default;
const genList = function(list, max){
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
};
const getMonsterName = function(){
return _.sample([
'All-devouring Baseball Imp',
'All-devouring Gumdrop Wraith',
'Chocolate Hydra',
'Devouring Peacock',
'Economy-sized Colossus of the Lemonade Stand',
'Ghost Pigeon',
'Gibbering Duck',
'Sparklemuffin Peacock Spider',
'Gum Elemental',
'Illiterate Construct of the Candy Store',
'Ineffable Chihuahua',
'Irritating Death Hamster',
'Irritating Gold Mouse',
'Juggernaut Snail',
'Juggernaut of the Sock Drawer',
'Koala of the Cosmos',
'Mad Koala of the West',
'Milk Djinni of the Lemonade Stand',
'Mind Ferret',
'Mystic Salt Spider',
'Necrotic Halitosis Angel',
'Pinstriped Famine Sheep',
'Ritalin Leech',
'Shocker Kangaroo',
'Stellar Tennis Juggernaut',
'Wailing Quail of the Sun',
'Angel Pigeon',
'Anime Sphinx',
'Bored Avalanche Sheep of the Wasteland',
'Devouring Nougat Sphinx of the Sock Drawer',
'Djinni of the Footlocker',
'Ectoplasmic Jazz Devil',
'Flatuent Angel',
'Gelatinous Duck of the Dream-Lands',
'Gelatinous Mouse',
'Golem of the Footlocker',
'Lich Wombat',
'Mechanical Sloth of the Past',
'Milkshake Succubus',
'Puffy Bone Peacock of the East',
'Rainbow Manatee',
'Rune Parrot',
'Sand Cow',
'Sinister Vanilla Dragon',
'Snail of the North',
'Spider of the Sewer',
'Stellar Sawdust Leech',
'Storm Anteater of Hell',
'Stupid Spirit of the Brewery',
'Time Kangaroo',
'Tomb Poodle',
]);
};
const getType = function(){
return `${_.sample(['Tiny', 'Small', 'Medium', 'Large', 'Gargantuan', 'Stupidly vast'])} ${_.sample(['beast', 'fiend', 'annoyance', 'guy', 'cutie'])}`;
};
const getAlignment = function(){
return _.sample([
'annoying evil',
'chaotic gossipy',
'chaotic sloppy',
'depressed neutral',
'lawful bogus',
'lawful coy',
'manic-depressive evil',
'narrow-minded neutral',
'neutral annoying',
'neutral ignorant',
'oedpipal neutral',
'silly neutral',
'unoriginal neutral',
'weird neutral',
'wordy evil',
'unaligned'
]);
};
const getStats = function(){
return `|${_.times(6, function(){
const num = _.random(1, 20);
const mod = Math.ceil(num/2 - 5);
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
}).join('|')}|`;
};
const genAbilities = function(){
return _.sample([
'***Pack Tactics.*** These guys work together like peanut butter and jelly.',
'***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
'***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
'***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
'***Sassiness.*** When questioned, this creature will talk back instead of answering.',
'***Big Jerk.*** Whenever this creature makes an attack, it starts telling you how much cooler it is than you.',
]);
};
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 = {
monster : function(classes, genLines){
return dedent`
{{${classes}
## ${getMonsterName()}
*${getType()}, ${getAlignment()}*
___
**Armor Class** :: ${_.random(10, 20)} (chain mail, shield)
**Hit Points** :: ${_.random(1, 150)}(1d4 + 5)
**Speed** :: ${_.random(0, 50)}ft.
___
| STR | DEX | CON | INT | WIS | CHA |
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
${getStats()}
___
**Condition Immunities** :: ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
**Senses** :: darkvision 60 ft., passive Perception ${_.random(3, 20)}
**Languages** :: ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
**Challenge** :: ${_.random(0, 15)} (${_.random(10, 10000)} XP)
___
:
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n\t\t\t\n\t\t\t')}
:
### Actions
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n\t\t\t\n\t\t\t')}
}}
\n`;
}
};

View File

@@ -1,312 +0,0 @@
/* eslint-disable max-lines */
const MagicGen = require('./magic.gen.js');
const ClassTableGen = require('./classtable.gen.js');
const MonsterBlockGen = require('./monsterblock.gen.js');
const ClassFeatureGen = require('./classfeature.gen.js');
const CoverPageGen = require('./coverpage.gen.js');
const TableOfContentsGen = require('./tableOfContents.gen.js');
const dedent = require('dedent-tabs').default;
module.exports = [
{
groupName : 'Editor',
icon : 'fas fa-pencil-alt',
snippets : [
{
name : 'Column Break',
icon : 'fas fa-columns',
gen : '\n\\column\n'
},
{
name : 'New Page',
icon : 'fas fa-file-alt',
gen : '\n\\page\n'
},
{
name : 'Vertical Spacing',
icon : 'fas fa-arrows-alt-v',
gen : '\n::::\n'
},
{
name : 'Horizontal Spacing',
icon : 'fas fa-arrows-alt-h',
gen : ' {{width:100px}} '
},
{
name : 'Wide Block',
icon : 'fas fa-window-maximize',
gen : dedent`\n
{{wide
Everything in here will be extra wide. Tables, text, everything!
Beware though, CSS columns can behave a bit weird sometimes. You may
have to rely on the automatic column-break rather than \`\column\` if
you mix columns and wide blocks on the same page.
}}
\n`
},
{
name : 'Image',
icon : 'fas fa-image',
gen : dedent`
![cat warrior](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {width:325px}
Credit: Kyounghwan Kim`
},
{
name : 'Background Image',
icon : 'fas fa-tree',
gen : `![homebrew mug](http://i.imgur.com/hMna6G0.png) {position:absolute,top:50px,right:30px,width:280px}`
},
{
name : 'QR Code',
icon : 'fas fa-qrcode',
gen : (brew)=>{
return `![]` +
`(https://api.qrserver.com/v1/create-qr-code/?data=` +
`https://homebrewery.naturalcrit.com/share/${brew.shareId}` +
`&amp;size=100x100) {width:100px;mix-blend-mode:multiply}`;
}
},
{
name : 'Page Number',
icon : 'fas fa-bookmark',
gen : '{{pageNumber 1}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
},
{
name : 'Auto-incrementing Page Number',
icon : 'fas fa-sort-numeric-down',
gen : '{{pageNumber,auto}}\n{{footnote PART 1 | SECTION NAME}}\n\n'
},
{
name : 'Link to page',
icon : 'fas fa-link',
gen : '[Click here](#p3) to go to page 3\n'
},
{
name : 'Table of Contents',
icon : 'fas fa-book',
gen : TableOfContentsGen
},
{
name : 'Remove Drop Cap',
icon : 'fas fa-remove-format',
gen : '<style>\n' +
' .phb3 h1+p:first-letter {\n' +
' all: unset;\n' +
' }\n' +
'</style>'
},
{
name : 'Tweak Drop Cap',
icon : 'fas fa-sliders-h',
gen : '<style>\n' +
' /* Drop Cap settings */\n' +
' .phb3 h1 + p::first-letter {\n' +
' float: left;\n' +
' font-family: SolberaImitationRemake;\n' +
' font-size: 3.5cm;\n' +
' color: #222;\n' +
' line-height: .8em;\n' +
' }\n' +
'</style>'
},
]
},
/************************* PHB ********************/
{
groupName : 'PHB',
icon : 'fas fa-book',
snippets : [
{
name : 'Spell',
icon : 'fas fa-magic',
gen : MagicGen.spell,
},
{
name : 'Spell List',
icon : 'fas fa-scroll',
gen : MagicGen.spellList,
},
{
name : 'Class Feature',
icon : 'fas fa-mask',
gen : ClassFeatureGen,
},
{
name : 'Note',
icon : 'fas fa-sticky-note',
gen : function(){
return dedent`
{{note
##### Time to Drop Knowledge
Use notes to point out some interesting information.
**Tables and lists** both work within a note.
}}
\n`;
},
},
{
name : 'Descriptive Text Box',
icon : 'fas fa-comment-alt',
gen : function(){
return dedent`
{{descriptive
##### Time to Drop Knowledge
Use descriptive boxes to highlight text that should be read aloud.
**Tables and lists** both work within a descriptive box.
}}
\n`;
},
},
{
name : 'Monster Stat Block (unframed)',
icon : 'fas fa-paw',
gen : MonsterBlockGen.monster('monster', 2),
},
{
name : 'Monster Stat Block',
icon : 'fas fa-spider',
gen : MonsterBlockGen.monster('monster,frame', 2),
},
{
name : 'Wide Monster Stat Block',
icon : 'fas fa-dragon',
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
},
{
name : 'Cover Page',
icon : 'fas fa-file-word',
gen : CoverPageGen,
},
{
name : 'Magic Item',
icon : 'fas fa-hat-wizard',
gen : MagicGen.item,
},
]
},
/********************* TABLES *********************/
{
groupName : 'Tables',
icon : 'fas fa-table',
snippets : [
{
name : 'Class Table',
icon : 'fas fa-table',
gen : ClassTableGen.full,
},
{
name : 'Half Class Table',
icon : 'fas fa-list-alt',
gen : ClassTableGen.half,
},
{
name : 'Table',
icon : 'fas fa-th-list',
gen : function(){
return dedent`
##### Character Advancement
| Experience Points | Level | Proficiency Bonus |
|:------------------|:-----:|:-----------------:|
| 0 | 1 | +2 |
| 300 | 2 | +2 |
| 900 | 3 | +2 |
| 2,700 | 4 | +2 |
| 6,500 | 5 | +3 |
| 14,000 | 6 | +3 |
\n`;
}
},
{
name : 'Wide Table',
icon : 'fas fa-list',
gen : function(){
return dedent`
{{wide
##### Weapons
| Name | Cost | Damage | Weight | Properties |
|:------------------------|:-----:|:----------------|--------:|:-----------|
| *Simple Melee Weapons* | | | | |
| &emsp; Club | 1 sp | 1d4 bludgeoning | 2 lb. | Light |
| &emsp; Dagger | 2 gp | 1d4 piercing | 1 lb. | Finesse |
| &emsp; Spear | 1 gp | 1d6 piercing | 3 lb. | Thrown |
| *Simple Ranged Weapons* | | | | |
| &emsp; Dart | 5 cp | 1d4 piercig | 1/4 lb. | Finesse |
| &emsp; Shortbow | 25 gp | 1d6 piercing | 2 lb. | Ammunition |
| &emsp; Sling | 1 sp | 1d4 bludgeoning | &mdash; | Ammunition |
}}
\n`;
}
},
{
name : 'Split Table',
icon : 'fas fa-th-large',
gen : function(){
return dedent`
##### Typical Difficulty Classes
{{column-count:2
| Task Difficulty | DC |
|:----------------|:--:|
| Very easy | 5 |
| Easy | 10 |
| Medium | 15 |
| Task Difficulty | DC |
|:------------------|:--:|
| Hard | 20 |
| Very hard | 25 |
| Nearly impossible | 30 |
}}
\n`;
}
}
]
},
/**************** PRINT *************/
{
groupName : 'Print',
icon : 'fas fa-print',
snippets : [
{
name : 'A4 PageSize',
icon : 'far fa-file',
gen : ['<style>',
' .phb{',
' width : 210mm;',
' height : 296.8mm;',
' }',
'</style>'
].join('\n')
},
{
name : 'Ink Friendly',
icon : 'fas fa-tint',
gen : ['<style>',
' .phb{ background : white;}',
' .phb img{ display : none;}',
' .phb hr+blockquote{background : white;}',
'</style>',
''
].join('\n')
},
]
},
];

View File

@@ -1,84 +0,0 @@
const _ = require('lodash');
const dedent = require('dedent-tabs').default;
const getTOC = (pages)=>{
const add1 = (title, page)=>{
res.push({
title : title,
page : page + 1,
children : []
});
};
const add2 = (title, page)=>{
if(!_.last(res)) add1(null, page);
_.last(res).children.push({
title : title,
page : page + 1,
children : []
});
};
const add3 = (title, page)=>{
if(!_.last(res)) add1(null, page);
if(!_.last(_.last(res).children)) add2(null, page);
_.last(_.last(res).children).children.push({
title : title,
page : page + 1,
children : []
});
};
const res = [];
_.each(pages, (page, pageNum)=>{
const lines = page.split('\n');
_.each(lines, (line)=>{
if(_.startsWith(line, '# ')){
const title = line.replace('# ', '');
add1(title, pageNum);
}
if(_.startsWith(line, '## ')){
const title = line.replace('## ', '');
add2(title, pageNum);
}
if(_.startsWith(line, '### ')){
const title = line.replace('### ', '');
add3(title, pageNum);
}
});
});
return res;
};
module.exports = function(brew){
const pages = brew.text.split('\\page');
const TOC = getTOC(pages);
const markdown = _.reduce(TOC, (r, g1, idx1)=>{
if(g1.title !== null) {
r.push(`\t\t- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
}
if(g1.children.length){
_.each(g1.children, (g2, idx2)=>{
if(g2.title !== null) {
r.push(`\t\t - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
}
if(g2.children.length){
_.each(g2.children, (g3, idx3)=>{
if(g2.title !== null) {
r.push(`\t\t - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
} else { // Don't over-indent if no level-2 parent entry
r.push(`\t\t - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
}
});
}
});
}
return r;
}, []).join('\n');
return dedent`
{{toc,wide
# Table Of Contents
${markdown}
}}
\n`;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,71 +1,73 @@
require('./homebrew.less');
const React = require('react');
const createClass = require('create-react-class');
const { StaticRouter:Router, Switch, Route } = require('react-router-dom');
const queryString = require('query-string');
const HomePage = require('./pages/homePage/homePage.jsx');
const EditPage = require('./pages/editPage/editPage.jsx');
const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx');
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx');
const Homebrew = createClass({
getDefaultProps : function() {
return {
url : '',
welcomeText : '',
changelog : '',
version : '0.0.0',
account : null,
enable_v3 : false,
brew : {
title : '',
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
}
};
},
componentWillMount : function() {
global.account = this.props.account;
global.version = this.props.version;
global.enable_v3 = this.props.enable_v3;
},
render : function (){
return (
<Router location={this.props.url}>
<div className='homebrew'>
<Switch>
<Route path='/edit/:id' component={(routeProps)=><EditPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new' exact component={(routeProps)=><NewPage />}/>
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/>
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>
<Route path='/' component={()=><HomePage brew={this.props.brew} />}/>
</Switch>
</div>
</Router>
);
}
});
module.exports = Homebrew;
//TODO: Nicer Error page instead of just "cant get that"
// '/share/:id' : (args)=>{
// if(!this.props.brew.shareId){
// return <ErrorPage errorId={args.id}/>;
// }
//
// return <SharePage
// id={args.id}
// brew={this.props.brew} />;
// },
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const CreateRouter = require('pico-router').createRouter;
const BrewActions = require('homebrewery/brew.actions.js');
const AccountActions = require('homebrewery/account.actions.js');
const HomePage = require('./pages/homePage/homePage.jsx');
const EditPage = require('./pages/editPage/editPage.jsx');
const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx');
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx');
let Router;
const Homebrew = React.createClass({
getDefaultProps: function() {
return {
url : '',
version : '0.0.0',
loginPath : '',
user : undefined,
brew : undefined,
brews : []
};
},
componentWillMount: function() {
BrewActions.init({
version : this.props.version,
brew : this.props.brew
});
AccountActions.init({
user : this.props.user,
loginPath : this.props.loginPath
});
Router = CreateRouter({
'/edit/:id' : <EditPage />,
'/share/:id' : <SharePage />,
'/user/:username' : (args) => {
return <UserPage
username={args.username}
brews={this.props.brews}
/>
},
'/print/:id' : (args, query) => {
return <PrintPage brew={this.props.brew} query={query}/>;
},
'/print' : (args, query) => {
return <PrintPage query={query}/>;
},
'/new' : <NewPage />,
'/changelog' : <SharePage />,
'/test' : <SharePage />,
'/test_old' : <SharePage />,
'*' : <HomePage />,
});
},
render : function(){
return <div className='homebrew'>
<Router initialUrl={this.props.url}/>
</div>
}
});
module.exports = Homebrew;

View File

@@ -1,15 +1,16 @@
@import 'naturalcrit/styles/core.less';
.homebrew{
height : 100%;
.sitePage{
.page{
display : flex;
height : 100%;
background-color : @steel;
flex-direction : column;
.content{
position : relative;
height : calc(~"100% - 29px"); //Navbar height
flex : auto;
position : relative;
height : calc(~"100% - 29px"); //Navbar height
flex : auto;
}
}
}
}

View File

@@ -1,34 +1,23 @@
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const Account = createClass({
getInitialState : function() {
return {
url : ''
};
},
componentDidMount : function(){
if(typeof window !== 'undefined'){
this.setState({
url : window.location.href
});
}
},
render : function(){
if(global.account){
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'>
{global.account.username}
</Nav.item>;
}
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>
login
</Nav.item>;
}
});
module.exports = Account;
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/account.store.js');
const Actions = require('homebrewery/account.actions.js');
module.exports = function(props){
const user = Store.getUser();
if(user && user == props.userPage){
return <Nav.item onClick={Actions.logout} color='yellow' icon='fa-user-times'>
logout
</Nav.item>
}
if(user){
return <Nav.item href={`/user/${user}`} color='yellow' icon='fa-user'>
{user}
</Nav.item>
}
return <Nav.item onClick={Actions.login} color='teal' icon='fa-sign-in'>
login
</Nav.item>
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,51 +1,22 @@
require('./navbar.less');
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const PatreonNavItem = require('./patreon.navitem.jsx');
const Navbar = createClass({
getInitialState : function() {
return {
//showNonChromeWarning : false,
ver : '0.0.0'
};
},
componentDidMount : function() {
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
this.setState({
//showNonChromeWarning : !isChrome,
ver : window.version
});
},
/*
renderChromeWarning : function(){
if(!this.state.showNonChromeWarning) return;
return <Nav.item className='warning' icon='fa-exclamation-triangle'>
Optimized for Chrome
<div className='dropdown'>
If you are experiencing rendering issues, use Chrome instead
</div>
</Nav.item>
},
*/
render : function(){
return <Nav.base>
<Nav.section>
<Nav.logo />
<Nav.item href='/' className='homebrewLogo'>
<div>The Homebrewery</div>
</Nav.item>
<Nav.item>{`v${this.state.ver}`}</Nav.item>
<PatreonNavItem />
{/*this.renderChromeWarning()*/}
</Nav.section>
{this.props.children}
</Nav.base>;
}
});
module.exports = Navbar;
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Navbar = React.createClass({
render : function(){
return <Nav.base>
<Nav.section>
<Nav.logo />
<Nav.item href='/' className='homebrewLogo'>
<div>The Homebrewery</div>
</Nav.item>
<Nav.item>{`v${Store.getVersion()}`}</Nav.item>
</Nav.section>
{this.props.children}
</Nav.base>
}
});
module.exports = Navbar;

View File

@@ -1,12 +1,4 @@
@navbarHeight : 28px;
@keyframes coloring {
//from {color: white;}
//to {color: red;}
0% {color: pink;}
50% {color: pink;}
75% {color: red;}
100% {color: pink;}
}
.homebrew nav{
.homebrewLogo{
.animate(color);
@@ -21,32 +13,6 @@
color : @blue;
}
}
.editTitle.navItem{
padding : 2px 12px;
input{
width : 250px;
margin : 0;
padding : 2px;
background-color : #444;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
border : 1px solid @blue;
outline : none;
}
.charCount{
display : inline-block;
vertical-align : bottom;
margin-left : 8px;
color : #666;
text-align : right;
&.max{
color : @red;
}
}
}
.brewTitle.navItem{
font-size : 12px;
font-weight : 800;
@@ -55,16 +21,11 @@
text-transform : initial;
}
.patreon.navItem{
border-left : 1px solid #666;
border-right : 1px solid #666;
&:hover i {
color: red;
}
i{
.animate(color);
animation-name: coloring;
animation-duration: 2s;
color: pink;
&:hover{
color : @red;
}
}
}
.recent.navItem{
@@ -138,4 +99,34 @@
text-align : center;
}
}
}
.staticSave.navItem{
background-color : @orange;
&:hover{
background-color : @green;
}
}
.continousSave.navItem{
width : 105px;
text-align : center;
&.saved{
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
.errorContainer{
position : absolute;
top : 29px;
left : -20px;
z-index : 1000;
width : 170px;
padding : 8px;
background-color : #333;
a{
color : @teal;
}
}
}
}
}

View File

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

View File

@@ -1,11 +0,0 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.item
href='/new'
color='purple'
icon='fas fa-plus-square'>
new
</Nav.item>;
};

View File

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

View File

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

View File

@@ -1,186 +1,204 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx');
const EDIT_KEY = 'homebrewery-recently-edited';
const VIEW_KEY = 'homebrewery-recently-viewed';
const RecentItems = createClass({
getDefaultProps : function() {
return {
storageKey : '',
showEdit : false,
showView : false
};
},
getInitialState : function() {
return {
showDropdown : false,
edit : [],
view : []
};
},
componentDidMount : function() {
//== Load recent items list ==//
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
//== Add current brew to appropriate recent items list (depending on storageKey) ==//
if(this.props.storageKey == 'edit'){
let editId = this.props.brew.editId;
if(this.props.brew.googleId){
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
}
edited = _.filter(edited, (brew)=>{
return brew.id !== editId;
});
edited.unshift({
id : editId,
title : this.props.brew.title,
url : `/edit/${editId}`,
ts : Date.now()
});
}
if(this.props.storageKey == 'view'){
let shareId = this.props.brew.shareId;
if(this.props.brew.googleId){
shareId = `${this.props.brew.googleId}${this.props.brew.shareId}`;
}
viewed = _.filter(viewed, (brew)=>{
return brew.id !== shareId;
});
viewed.unshift({
id : shareId,
title : this.props.brew.title,
url : `/share/${shareId}`,
ts : Date.now()
});
}
//== Store the updated lists (up to 8 items each) ==//
edited = _.slice(edited, 0, 8);
viewed = _.slice(viewed, 0, 8);
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
this.setState({
edit : edited,
view : viewed
});
},
componentDidUpdate : function(prevProps) {
if(prevProps.brew && this.props.brew.editId !== prevProps.brew.editId) {
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
if(this.props.storageKey == 'edit') {
let prevEditId = prevProps.brew.editId;
if(prevProps.brew.googleId){
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
}
edited = _.filter(this.state.edit, (brew)=>{
return brew.id !== prevEditId;
});
let editId = this.props.brew.editId;
if(this.props.brew.googleId){
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
}
edited.unshift({
id : editId,
title : this.props.brew.title,
url : `/edit/${editId}`,
ts : Date.now()
});
}
//== Store the updated lists (up to 8 items each) ==//
edited = _.slice(edited, 0, 8);
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
this.setState({
edit : edited
});
}
},
handleDropdown : function(show){
this.setState({
showDropdown : show
});
},
renderDropdown : function(){
if(!this.state.showDropdown) return null;
const makeItems = (brews)=>{
return _.map(brews, (brew)=>{
return <a href={brew.url} className='item' key={brew.id} target='_blank' rel='noopener noreferrer'>
<span className='title'>{brew.title || '[ no title ]'}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</span>
</a>;
});
};
return <div className='dropdown'>
{(this.props.showEdit && this.props.showView) ?
<h4>edited</h4> : null }
{this.props.showEdit ?
makeItems(this.state.edit) : null }
{(this.props.showEdit && this.props.showView) ?
<h4>viewed</h4> : null }
{this.props.showView ?
makeItems(this.state.view) : null }
</div>;
},
render : function(){
return <Nav.item icon='fas fa-history' color='grey' className='recent'
onMouseEnter={()=>this.handleDropdown(true)}
onMouseLeave={()=>this.handleDropdown(false)}>
{this.props.text}
{this.renderDropdown()}
</Nav.item>;
}
});
module.exports = {
edited : (props)=>{
return <RecentItems
brew={props.brew}
storageKey={props.storageKey}
text='recently edited'
showEdit={true}
/>;
},
viewed : (props)=>{
return <RecentItems
brew={props.brew}
storageKey={props.storageKey}
text='recently viewed'
showView={true}
/>;
},
both : (props)=>{
return <RecentItems
brew={props.brew}
storageKey={props.storageKey}
text='recent brews'
showEdit={true}
showView={true}
/>;
}
};
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var Moment = require('moment');
var Nav = require('naturalcrit/nav/nav.jsx');
const VIEW_KEY = 'homebrewery-recently-viewed';
const EDIT_KEY = 'homebrewery-recently-edited';
//DEPRICATED
var BaseItem = React.createClass({
getDefaultProps: function() {
return {
storageKey : '',
text : '',
currentBrew:{
title : '',
id : '',
url : ''
}
};
},
getInitialState: function() {
return {
showDropdown: false,
brews : []
};
},
componentDidMount: function() {
console.log('Recent nav item is depricated');
var brews = JSON.parse(localStorage.getItem(this.props.storageKey) || '[]');
brews = _.filter(brews, (brew)=>{
return brew.id !== this.props.currentBrew.id;
});
if(this.props.currentBrew.id){
brews.unshift({
id : this.props.currentBrew.id,
url : this.props.currentBrew.url,
title : this.props.currentBrew.title,
ts : Date.now()
});
}
brews = _.slice(brews, 0, 8);
localStorage.setItem(this.props.storageKey, JSON.stringify(brews));
this.setState({
brews : brews
});
},
handleDropdown : function(show){
this.setState({
showDropdown : show
})
},
renderDropdown : function(){
if(!this.state.showDropdown) return null;
var items = _.map(this.state.brews, (brew)=>{
return <a href={brew.url} className='item' key={brew.id} target='_blank'>
<span className='title'>{brew.title}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</span>
</a>
});
return <div className='dropdown'>{items}</div>
},
render : function(){
return <Nav.item icon='fa-clock-o' color='grey' className='recent'
onMouseEnter={this.handleDropdown.bind(null, true)}
onMouseLeave={this.handleDropdown.bind(null, false)}>
{this.props.text}
{this.renderDropdown()}
</Nav.item>
},
});
module.exports = {
viewed : React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
shareId : ''
}
};
},
render : function(){
return <BaseItem text='recently viewed' storageKey={VIEW_KEY}
currentBrew={{
id : this.props.brew.shareId,
title : this.props.brew.title,
url : `/share/${this.props.brew.shareId}`
}}
/>
},
}),
edited : React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
editId : ''
}
};
},
render : function(){
return <BaseItem text='recently edited' storageKey={EDIT_KEY}
currentBrew={{
id : this.props.brew.editId,
title : this.props.brew.title,
url : `/edit/${this.props.brew.editId}`
}}
/>
},
}),
both : React.createClass({
getDefaultProps: function() {
return {
errorId : null
};
},
getInitialState: function() {
return {
showDropdown: false,
edit : [],
view : []
};
},
componentDidMount: function() {
var edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
var viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
if(this.props.errorId){
edited = _.filter(edited, (edit) => {
return edit.id !== this.props.errorId;
});
viewed = _.filter(viewed, (view) => {
return view.id !== this.props.errorId;
});
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
}
this.setState({
edit : edited,
view : viewed
});
},
handleDropdown : function(show){
this.setState({
showDropdown : show
})
},
renderDropdown : function(){
if(!this.state.showDropdown) return null;
var makeItems = (brews) => {
return _.map(brews, (brew)=>{
return <a href={brew.url} className='item' key={brew.id} target='_blank'>
<span className='title'>{brew.title}</span>
<span className='time'>{Moment(brew.ts).fromNow()}</span>
</a>
});
};
return <div className='dropdown'>
<h4>edited</h4>
{makeItems(this.state.edit)}
<h4>viewed</h4>
{makeItems(this.state.view)}
</div>
},
render : function(){
return <Nav.item icon='fa-clock-o' color='grey' className='recent'
onMouseEnter={this.handleDropdown.bind(null, true)}
onMouseLeave={this.handleDropdown.bind(null, false)}>
Recent brews
{this.renderDropdown()}
</Nav.item>
}
})
}

View File

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

View File

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

View File

@@ -1,430 +1,63 @@
/* eslint-disable max-lines */
require('./editPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const request = require('superagent');
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Markdown = require('naturalcrit/markdown.js');
const googleDriveActive = require('../../googleDrive.png');
const googleDriveInactive = require('../../googleDriveMono.png');
const SAVE_TIMEOUT = 3000;
const EditPage = createClass({
getDefaultProps : function() {
return {
brew : {
text : '',
style : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
gDrive : false,
trashed : false,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : [],
renderer : 'legacy'
}
};
},
getInitialState : function() {
return {
brew : this.props.brew,
isSaving : false,
isPending : false,
alertTrashedGoogleBrew : this.props.brew.trashed,
alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
errors : null,
htmlErrors : Markdown.validate(this.props.brew.text),
url : ''
};
},
savedBrew : null,
componentDidMount : function(){
this.setState({
url : window.location.href
});
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
this.trySave();
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){
return 'You have unsaved changes!';
}
};
this.setState((prevState)=>({
htmlErrors : Markdown.validate(prevState.brew.text)
}));
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount : function() {
window.onbeforeunload = function(){};
document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save();
if(e.keyCode == P_KEY) window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { text: text }),
isPending : true,
htmlErrors : htmlErrors
}), ()=>this.trySave());
},
handleStyleChange : function(style){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { style: style }),
isPending : true
}), ()=>this.trySave());
},
handleMetaChange : function(metadata){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata),
isPending : true,
}), ()=>this.trySave());
},
hasChanges : function(){
return !_.isEqual(this.state.brew, this.savedBrew);
},
trySave : function(){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){
this.debounceSave();
} else {
this.debounceSave.cancel();
}
},
handleGoogleClick : function(){
if(!global.account?.googleId) {
this.setState({
alertLoginToTransfer : true
});
return;
}
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}));
this.clearErrors();
},
closeAlerts : function(event){
event.stopPropagation(); //Only handle click once so alert doesn't reopen
this.setState({
alertTrashedGoogleBrew : false,
alertLoginToTransfer : false,
confirmGoogleTransfer : false
});
},
toggleGoogleStorage : function(){
this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
isSaving : false,
errors : null
}), ()=>this.save());
},
clearErrors : function(){
this.setState({
errors : null,
isSaving : false
});
},
save : async function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
this.setState((prevState)=>({
isSaving : true,
errors : null,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
if(this.state.saveGoogle) {
if(transfer) {
const res = await request
.post('/api/newGoogle/')
.send(this.state.brew)
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
: 'Error Transferring to Google!');
this.setState({ errors: err, saveGoogle: false });
});
if(!res) { return; }
console.log('Deleting Local Copy');
await request.delete(`/api/${this.state.brew.editId}`)
.send()
.catch((err)=>{
console.log('Error deleting Local Copy');
});
this.savedBrew = res.body;
history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
} else {
const res = await request
.put(`/api/updateGoogle/${this.state.brew.editId}`)
.send(this.state.brew)
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
: 'Error Saving to Google!');
this.setState({ errors: err });
return;
});
this.savedBrew = res.body;
}
} else {
if(transfer) {
const res = await request.post('/api')
.send(this.state.brew)
.catch((err)=>{
console.log('Error creating Local Copy');
this.setState({ errors: err });
return;
});
await request.get(`/api/removeGoogle/${this.state.brew.googleId}${this.state.brew.editId}`)
.send()
.catch((err)=>{
console.log('Error Deleting Google Brew');
});
this.savedBrew = res.body;
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
} else {
const res = await request
.put(`/api/update/${this.state.brew.editId}`)
.send(this.state.brew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ errors: err });
return;
});
this.savedBrew = res.body;
}
}
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, {
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
editId : this.savedBrew.editId,
shareId : this.savedBrew.shareId
}),
isPending : false,
isSaving : false,
}));
},
renderGoogleDriveIcon : function(){
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
{this.state.saveGoogle
? <img src={googleDriveActive} alt='googleDriveActive'/>
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
}
{this.state.confirmGoogleTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
{ this.state.saveGoogle
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
}
<br />
<div className='confirm' onClick={this.toggleGoogleStorage}>
Yes
</div>
<div className='deny'>
No
</div>
</div>
}
{this.state.alertLoginToTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
You must be signed in to a Google account to transfer
between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
}
</Nav.item>;
},
renderSaveButton : function(){
if(this.state.errors){
let errMsg = '';
try {
errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
} catch (e){}
if(this.state.errors.status == '401'){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={this.clearErrors}>
You must be signed in to a Google account
to save this to<br />Google Drive!<br />
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' rel='noopener noreferrer'
href={`https://github.com/naturalcrit/homebrewery/issues/new?body=${encodeURIComponent(errMsg)}`}>
here
</a>.
</div>
</Nav.item>;
}
if(this.state.isSaving){
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
}
if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
}
if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item>;
}
},
processShareId : function() {
return this.state.brew.googleId ?
this.state.brew.googleId + this.state.brew.shareId :
this.state.brew.shareId;
},
renderNavbar : function(){
return <Navbar>
{this.state.alertTrashedGoogleBrew &&
<div className='errorContainer' onClick={this.closeAlerts}>
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
<div className='confirm'>
OK
</div>
</div>
}
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderGoogleDriveIcon()}
{this.renderSaveButton()}
<NewBrew />
<ReportIssue />
<Nav.item newTab={true} href={`/share/${this.processShareId()}`} color='teal' icon='fas fa-share-alt'>
Share
</Nav.item>
<PrintLink shareId={this.processShareId()} />
<RecentNavItem brew={this.state.brew} storageKey='edit' />
<Account />
</Nav.section>
</Navbar>;
},
render : function(){
return <div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
brew={this.state.brew}
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
/>
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors} />
</SplitPane>
</div>
</div>;
}
});
module.exports = EditPage;
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const Items = require('../../navbar/navitems.js');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Utils = require('homebrewery/utils.js');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const EditPage = React.createClass({
componentDidMount: function(){
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount: function() {
document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : Utils.controlKeys({
s : Actions.save,
p : Actions.print
}),
render : function(){
return <div className='editPage page'>
<SmartNav />
<div className='content'>
<BrewInterface />
</div>
</div>
}
});
const SmartNav = Store.createSmartComponent(React.createClass({
getDefaultProps: function() {
return {
brew : {}
};
},
render : function(){
return <Navbar>
<Nav.section>
<Items.BrewTitle />
</Nav.section>
<Nav.section>
<Items.ContinousSave />
<Items.Issue />
<Nav.item newTab={true} href={'/share/' + Store.getBrew().shareId} color='teal' icon='fa-share-alt'>
Share
</Nav.item>
<Items.Print />
<Items.Account />
</Nav.section>
</Navbar>
}
}), ()=>{
return {brew : Store.getBrew()}
});
module.exports = EditPage;

View File

@@ -1,99 +1,4 @@
@keyframes glideDown {
0% {transform : translate(-50% + 3px, 0px);
opacity : 0;}
100% {transform : translate(-50% + 3px, 10px);
opacity : 1;}
}
.editPage{
.navItem.save{
width : 106px;
text-align : center;
position : relative;
&.saved{
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
}
}
.googleDriveStorage {
position : relative;
}
.googleDriveStorage img{
height : 20px;
padding : 0px;
margin : -5px;
}
.errorContainer{
animation-name: glideDown;
animation-duration: 0.4s;
position : absolute;
top : 100%;
left : 50%;
z-index : 100000;
width : 140px;
padding : 3px;
color : white;
background-color : #333;
border : 3px solid #444;
border-radius : 5px;
transform : translate(-50% + 3px, 10px);
text-align : center;
font-size : 10px;
font-weight : 800;
text-transform : uppercase;
a{
color : @teal;
}
&:before {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #444;
left: 53px;
top: -23px;
}
&:after {
content: "";
width: 0px;
height: 0px;
position: absolute;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 10px solid transparent;
border-bottom: 10px solid #333;
left: 53px;
top: -19px;
}
.deny {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
border-left : 1px solid #666;
.animate(background-color);
&:hover{
background-color : red;
}
}
.confirm {
width : 48%;
margin : 1px;
padding : 5px;
background-color : #333;
display : inline-block;
color : white;
.animate(background-color);
&:hover{
background-color : teal;
}
}
}
}
}

View File

@@ -1,29 +1,27 @@
require('./errorPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
var Nav = require('naturalcrit/nav/nav.jsx');
var Navbar = require('../../navbar/navbar.jsx');
var PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
var IssueNavItem = require('../../navbar/issue.navitem.jsx');
var RecentNavItem = require('../../navbar/recent.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const ErrorPage = createClass({
getDefaultProps : function() {
var ErrorPage = React.createClass({
getDefaultProps: function() {
return {
ver : '0.0.0',
errorId : ''
ver : '0.0.0',
errorId: ''
};
},
text : '# Oops \n We could not find a brew with that id. **Sorry!**',
render : function(){
return <div className='errorPage sitePage'>
return <div className='errorPage page'>
<Navbar ver={this.props.ver}>
<Nav.section>
<Nav.item className='errorTitle'>
@@ -34,14 +32,14 @@ const ErrorPage = createClass({
<Nav.section>
<PatreonNavItem />
<IssueNavItem />
<RecentNavItem />
<RecentNavItem.both errorId={this.props.errorId} />
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer text={this.text} />
</div>
</div>;
</div>
}
});

View File

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

View File

@@ -1,99 +1,63 @@
require('./homePage.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const request = require('superagent');
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const HomePage = createClass({
getDefaultProps : function() {
return {
brew : {
text : '',
},
ver : '0.0.0'
};
},
getInitialState : function() {
return {
brew : this.props.brew,
welcomeText : this.props.brew.text
};
},
handleSave : function(){
request.post('/api')
.send({
text : this.state.brew.text
})
.end((err, res)=>{
if(err) return;
const brew = res.body;
window.location = `/edit/${brew.editId}`;
});
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleTextChange : function(text){
this.setState({
brew : { text: text }
});
},
renderNavbar : function(){
return <Navbar ver={this.props.ver}>
<Nav.section>
<NewBrewItem />
<IssueNavItem />
<Nav.item newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
Changelog
</Nav.item>
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
},
render : function(){
return <div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
brew={this.state.brew}
onTextChange={this.handleTextChange}
renderer={this.state.brew.renderer}
showEditButtons={false}
/>
<BrewRenderer text={this.state.brew.text} />
</SplitPane>
</div>
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
Save current <i className='fas fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fas fa-magic' />
</a>
</div>;
}
});
module.exports = HomePage;
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Actions = require('homebrewery/brew.actions.js');
//
const HomePage = React.createClass({
handleSave : function(){
Actions.saveNew();
},
renderNavbar : function(){
return <Navbar>
<Nav.section>
<PatreonNavItem collaspe={true} />
<IssueNavItem collaspe={true} />
<Nav.item newTab={true} href='/changelog' color='purple' icon='fa-star' collaspe={true}>
What's new
</Nav.item>
<RecentNavItem.both />
<AccountNavItem />
{/*}
<Nav.item href='/new' color='green' icon='fa-external-link'>
New Brew
</Nav.item>
*/}
</Nav.section>
</Navbar>
},
render : function(){
return <div className='homePage page'>
{this.renderNavbar()}
<div className='content'>
<BrewInterface />
</div>
<div className={cx('floatingSaveButton', {
//show : Store.getBrewText() !== this.props.welcomeText
})} onClick={this.handleSave}>
Save current <i className='fa fa-save' />
</div>
<a href='/new' className='floatingNewButton'>
Create your own <i className='fa fa-magic' />
</a>
</div>
}
});
module.exports = HomePage;

View File

@@ -1,242 +1,62 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./newPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const request = require('superagent');
const Markdown = require('naturalcrit/markdown.js');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const NewPage = createClass({
getDefaultProps : function() {
return {
brew : {
text : '',
style : undefined,
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
gDrive : false,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
}
};
},
getInitialState : function() {
return {
brew : {
text : this.props.brew.text || '',
style : this.props.brew.style || undefined,
gDrive : false,
title : this.props.brew.title || '',
description : this.props.brew.description || '',
tags : this.props.brew.tags || '',
published : false,
authors : [],
systems : this.props.brew.systems || [],
renderer : this.props.brew.renderer || 'legacy'
},
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
errors : [],
htmlErrors : Markdown.validate(this.props.brew.text)
};
},
componentDidMount : function() {
const brewStorage = localStorage.getItem(BREWKEY);
const styleStorage = localStorage.getItem(STYLEKEY);
const brew = this.state.brew;
if(!this.props.brew.text || !this.props.brew.style){
brew.text = this.props.brew.text || (brewStorage ?? '');
brew.style = this.props.brew.style || (styleStorage ?? undefined);
}
this.setState((prevState)=>({
brew : brew,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save();
if(e.keyCode == P_KEY) this.print();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { text: text }),
htmlErrors : htmlErrors
}));
localStorage.setItem(BREWKEY, text);
},
handleStyleChange : function(style){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, { style: style }),
}));
localStorage.setItem(STYLEKEY, style);
},
handleMetaChange : function(metadata){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata),
}));
},
save : async function(){
this.setState({
isSaving : true
});
console.log('saving new brew');
let brew = this.state.brew;
// Split out CSS to Style if CSS codefence exists
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
const index = brew.text.indexOf('```\n\n');
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
brew.text = brew.text.slice(index + 5);
};
if(this.state.saveGoogle) {
const res = await request
.post('/api/newGoogle/')
.send(brew)
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
: 'Error Creating New Google Brew!');
this.setState({ isSaving: false });
return;
});
brew = res.body;
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
window.location = `/edit/${brew.googleId}${brew.editId}`;
} else {
request.post('/api')
.send(brew)
.end((err, res)=>{
if(err){
this.setState({
isSaving : false
});
return;
}
window.onbeforeunload = function(){};
brew = res.body;
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
window.location = `/edit/${brew.editId}`;
});
}
},
renderSaveButton : function(){
if(this.state.isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='saveButton'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='saveButton' onClick={this.save}>
save
</Nav.item>;
}
},
print : function(){
localStorage.setItem('print', `<style>\n${this.state.brew.style}\n</style>\n\n${this.state.brew.text}`);
window.open('/print?dialog=true&local=print', '_blank');
},
renderLocalPrintButton : function(){
return <Nav.item color='purple' icon='far fa-file-pdf' onClick={this.print}>
get PDF
</Nav.item>;
},
renderNavbar : function(){
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
{this.renderLocalPrintButton()}
<IssueNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>;
},
render : function(){
return <div className='newPage sitePage'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref='editor'
brew={this.state.brew}
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
renderer={this.state.brew.renderer}
/>
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} errors={this.state.htmlErrors}/>
</SplitPane>
</div>
</div>;
}
});
module.exports = NewPage;
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const Items = require('../../navbar/navitems.js');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Utils = require('homebrewery/utils.js');
const KEY = 'homebrewery-new';
const NewPage = React.createClass({
componentDidMount: function() {
try{
const storedBrew = JSON.parse(localStorage.getItem(KEY));
if(storedBrew && storedBrew.text) Actions.setBrew(storedBrew);
}catch(e){}
Store.updateEmitter.on('change', this.saveToLocal);
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount: function() {
Store.updateEmitter.removeListener('change', this.saveToLocal);
document.removeEventListener('keydown', this.handleControlKeys);
},
saveToLocal : function(){
localStorage.setItem(KEY, JSON.stringify(Store.getBrew()));
},
handleControlKeys : Utils.controlKeys({
s : Actions.saveNew,
p : Actions.localPrint
}),
render : function(){
return <div className='newPage page'>
<Navbar>
<Nav.section>
<Items.BrewTitle />
</Nav.section>
<Nav.section>
<Items.StaticSave />
<Nav.item color='purple' icon='fa-file-pdf-o' onClick={Actions.localPrint}>
get PDF
</Nav.item>
<Items.Issue collaspe={true} />
<Items.Account />
</Nav.section>
</Navbar>
<div className='content'>
<BrewInterface />
</div>
</div>
}
});
module.exports = NewPage;

View File

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

View File

@@ -1,69 +1,66 @@
require('./printPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const { Meta } = require('vitreum/headtags');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js');
const Markdown = require('homebrewery/markdown.js');
const PrintPage = createClass({
getDefaultProps : function() {
const Headtags = require('vitreum/headtags');
const PrintPage = React.createClass({
getDefaultProps: function() {
return {
query : {},
brew : {
text : '',
style : '',
renderer : 'legacy'
brew : {
text : '',
style : ''
}
};
},
getInitialState : function() {
getInitialState: function() {
return {
brewText : this.props.brew.text
brew: this.props.brew
};
},
componentDidMount : function() {
componentDidMount: function() {
if(this.props.query.local){
this.setState((prevState, prevProps)=>({
brewText : localStorage.getItem(prevProps.query.local)
}));
try{
this.setState({
brew : JSON.parse(
localStorage.getItem(this.props.query.local)
)
});
}catch(e){}
}
if(this.props.query.dialog) window.print();
},
//TODO: Print page shouldn't replicate functionality in brew renderer
renderStyle : function(){
if(!this.state.brew.style) return;
return <style>{this.state.brew.style.replace(/;/g, ' !important;')}</style>
},
renderPages : function(){
if(this.props.brew.renderer == 'legacy') {
return _.map(this.state.brewText.split('\\page'), (page, index)=>{
return <div
className='phb page'
id={`p${index + 1}`}
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(page) }}
key={index} />;
});
} else {
return _.map(this.state.brewText.split(/^\\page/gm), (page, index)=>{
return <div
className='phb3 page'
id={`p${index + 1}`}
dangerouslySetInnerHTML={{ __html: Markdown.render(page) }}
key={index} />;
});
}
return _.map(this.state.brew.text.split('\\page'), (page, index) => {
return <div
className='phb v2'
id={`p${index + 1}`}
dangerouslySetInnerHTML={{__html:Markdown.render(page)}}
key={index} />;
});
},
renderPrintInstructions : function(){
return <div className='printInstructions'>
Hey, I'm really cool instructions!!!!!
</div>
},
render : function(){
return <div>
<Meta name='robots' content='noindex, nofollow' />
<link href={`${this.props.brew.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
{/* Apply CSS from Style tab */}
<div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.brew.style} </style>` }} />
return <div className='printPage'>
<Headtags.title>{this.state.brew.title}</Headtags.title>
{this.renderPrintInstructions()}
{this.renderStyle()}
{this.renderPages()}
</div>;
</div>
}
});

View File

@@ -1,3 +1,17 @@
.printPage{
.printPage{
position : relative;
@media print{
.printInstructions{
display : none;
}
}
.printInstructions{
position : absolute;
top : 0px;
right : 0px;
z-index : 100000;
padding : 30px;
background-color : @blue;
}
}

View File

@@ -1,112 +1,74 @@
require('./sharePage.less');
const React = require('react');
const createClass = require('create-react-class');
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const SharePage = createClass({
getDefaultProps : function() {
return {
brew : {
title : '',
text : '',
style : '',
shareId : null,
createdAt : null,
updatedAt : null,
views : 0,
renderer : ''
}
};
},
getInitialState : function() {
return {
showDropdown : false
};
},
componentDidMount : function() {
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return;
const P_KEY = 80;
if(e.keyCode == P_KEY){
window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
e.stopPropagation();
e.preventDefault();
}
},
processShareId : function() {
return this.props.brew.googleId ?
this.props.brew.googleId + this.props.brew.shareId :
this.props.brew.shareId;
},
handleDropdown : function(show){
this.setState({
showDropdown : show
});
},
renderDropdown : function(){
if(!this.state.showDropdown) return null;
return <div className='dropdown'>
<a href={`/source/${this.processShareId()}`} className='item'>
view
</a>
<a href={`/download/${this.processShareId()}`} className='item'>
download
</a>
<a href={`/new/${this.processShareId()}`} className='item'>
clone to new
</a>
</div>;
},
render : function(){
return <div className='sharePage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.props.brew.shareId && <>
<PrintLink shareId={this.processShareId()} />
<Nav.item icon='fas fa-code' color='red' className='source'
onMouseEnter={()=>this.handleDropdown(true)}
onMouseLeave={()=>this.handleDropdown(false)}>
source
{this.renderDropdown()}
</Nav.item>
</>}
<RecentNavItem brew={this.props.brew} storageKey='view' />
<Account />
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} />
</div>
</div>;
}
});
module.exports = SharePage;
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const ReportIssue = require('../../navbar/issue.navitem.jsx');
//const RecentlyViewed = require('../../navbar/recent.navitem.jsx').viewed;
const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('homebrewery/brewRenderer/brewRenderer.jsx');
const Utils = require('homebrewery/utils.js');
const Actions = require('homebrewery/brew.actions.js');
const Store = require('homebrewery/brew.store.js');
const Headtags = require('vitreum/headtags');
const SharePage = React.createClass({
componentDidMount: function() {
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount: function() {
document.removeEventListener('keydown', this.handleControlKeys);
},
handleControlKeys : Utils.controlKeys({
p : Actions.print
}),
renderMetatags : function(brew){
let metatags = [
<Headtags.meta key='site_name' property='og:site_name' content='Homebrewery'/>,
<Headtags.meta key='type' property='og:type' content='article' />
];
if(brew.title){
metatags.push(<Headtags.meta key='title' property='og:title' content={brew.title} />);
}
if(brew.description){
metatags.push(<Headtags.meta key='description' name='description' content={brew.description} />);
}
if(brew.thumbnail){
metatags.push(<Headtags.meta key='image' property='og:image' content={brew.thumbnail} />);
}
return metatags;
},
render : function(){
const brew = Store.getBrew();
return <div className='sharePage page'>
{this.renderMetatags(brew)}
<Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{brew.title}</Nav.item>
</Nav.section>
<Nav.section>
<ReportIssue />
<PrintLink shareId={brew.shareId} />
<Nav.item href={'/source/' + brew.shareId} color='teal' icon='fa-code'>
source
</Nav.item>
<Account />
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer brew={brew} />
</div>
</div>
}
});
module.exports = SharePage;

View File

@@ -1,50 +1,3 @@
.sharePage{
.content{
overflow-y : hidden;
}
.source.navItem{
position : relative;
.dropdown{
position : absolute;
top : 28px;
left : 0px;
z-index : 10000;
width : 100%;
h4{
display : block;
box-sizing : border-box;
padding : 5px 0px;
background-color : #333;
font-size : 0.8em;
color : #bbb;
text-align : center;
border-top : 1px solid #888;
&:nth-of-type(1){ background-color: darken(@teal, 20%); }
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
}
.item{
.animate(background-color);
position : relative;
display : block;
width : 100%;
vertical-align : middle;
padding : 13px 5px;
box-sizing : border-box;
background-color : #333;
color : white;
text-decoration : none;
border-top : 1px solid #888;
&:hover{
background-color : @blue;
}
.title{
display : inline-block;
overflow : hidden;
width : 100%;
text-overflow : ellipsis;
white-space : nowrap;
}
}
}
}
}

View File

@@ -1,18 +1,13 @@
require('./brewItem.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const moment = require('moment');
const request = require('superagent');
const googleDriveIcon = require('../../../googleDrive.png');
const BrewItem = createClass({
getDefaultProps : function() {
const BrewItem = React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
title : '',
description : '',
authors : []
@@ -20,83 +15,12 @@ const BrewItem = createClass({
};
},
deleteBrew : function(){
if(this.props.brew.authors.length <= 1){
if(!confirm('Are you sure you want to delete this brew? Because you are the only owner of this brew, the document will be deleted permanently.')) return;
if(!confirm('Are you REALLY sure? You will not be able to recover the document.')) return;
} else {
if(!confirm('Are you sure you want to remove this brew from your collection? This will remove you as an editor, but other owners will still be able to access the document.')) return;
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
}
if(this.props.brew.googleId) {
request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`)
.send()
.end(function(err, res){
location.reload();
});
} else {
request.delete(`/api/${this.props.brew.editId}`)
.send()
.end(function(err, res){
location.reload();
});
}
},
renderDeleteBrewLink : function(){
renderEditLink: function(){
if(!this.props.brew.editId) return;
return <a onClick={this.deleteBrew}>
<i className='fas fa-trash-alt' />
</a>;
},
renderEditLink : function(){
if(!this.props.brew.editId) return;
let editLink = this.props.brew.editId;
if(this.props.brew.googleId) {
editLink = this.props.brew.googleId + editLink;
}
return <a href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fas fa-pencil-alt' />
</a>;
},
renderShareLink : function(){
if(!this.props.brew.shareId) return;
let shareLink = this.props.brew.shareId;
if(this.props.brew.googleId) {
shareLink = this.props.brew.googleId + shareLink;
}
return <a href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
<i className='fas fa-share-alt' />
</a>;
},
renderDownloadLink : function(){
if(!this.props.brew.shareId) return;
let shareLink = this.props.brew.shareId;
if(this.props.brew.googleId) {
shareLink = this.props.brew.googleId + shareLink;
}
return <a href={`/download/${shareLink}`}>
<i className='fas fa-download' />
</a>;
},
renderGoogleDriveIcon : function(){
if(!this.props.brew.gDrive) return;
return <span>
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
</span>;
return <a href={`/edit/${this.props.brew.editId}`} target='_blank'>
<i className='fa fa-pencil' />
</a>
},
render : function(){
@@ -108,24 +32,23 @@ const BrewItem = createClass({
<div className='info'>
<span>
<i className='fas fa-user' /> {brew.authors.join(', ')}
<i className='fa fa-user' /> {brew.authors.join(', ')}
</span>
<span>
<i className='fas fa-eye' /> {brew.views}
<i className='fa fa-eye' /> {brew.views}
</span>
<span>
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
<i className='fa fa-refresh' /> {moment(brew.updatedAt).fromNow()}
</span>
{this.renderGoogleDriveIcon()}
</div>
<div className='links'>
{this.renderShareLink()}
<a href={`/share/${brew.shareId}`} target='_blank'>
<i className='fa fa-share-alt' />
</a>
{this.renderEditLink()}
{this.renderDownloadLink()}
{this.renderDeleteBrewLink()}
</div>
</div>;
</div>
}
});

View File

@@ -7,7 +7,6 @@
box-sizing : border-box;
overflow : hidden;
width : 48%;
min-height : 105px;
margin-right : 15px;
margin-bottom : 15px;
padding : 5px 15px 5px 8px;
@@ -22,13 +21,10 @@
font-size : 2.2em;
}
.info{
position: absolute;
bottom: 0px;
margin-bottom: 4px;
font-family : ScalySans;
font-size : 1.2em;
&>span{
margin-right : 12px;
margin-right : 15px;
}
}
&:hover{
@@ -59,14 +55,6 @@
&:hover{
opacity : 1;
}
i{
cursor : pointer;
}
}
}
.googleDriveIcon {
height : 20px;
padding : 0px;
margin : -5px;
}
}
}

View File

@@ -1,157 +1,75 @@
require('./userPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const RecentNavItem = require('../../navbar/recent.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const BrewItem = require('./brewItem/brewItem.jsx');
// const brew = {
// title : 'SUPER Long title woah now',
// authors : []
// };
const brew = {
title : 'SUPER Long title woah now',
authors : []
}
//const BREWS = _.times(25, ()=>{ return brew;});
const BREWS = _.times(25, ()=>{ return brew});
const UserPage = createClass({
getDefaultProps : function() {
const UserPage = React.createClass({
getDefaultProps: function() {
return {
username : '',
brews : [],
brews : []
};
},
getInitialState : function() {
return {
sortType : 'alpha',
sortDir : 'asc'
};
},
getUsernameWithS : function() {
if(this.props.username.endsWith('s'))
return `${this.props.username}'`;
return `${this.props.username}'s`;
},
renderBrews : function(brews){
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
const sortedBrews = this.sortBrews(brews);
const sortedBrews = _.sortBy(brews, (brew)=>{ return brew.title; });
return _.map(sortedBrews, (brew, idx)=>{
return <BrewItem brew={brew} key={idx}/>;
return _.map(sortedBrews, (brew, idx) => {
return <BrewItem brew={brew} key={idx}/>
});
},
sortBrewOrder : function(brew){
if(!brew.title){brew.title = 'No Title';};
const mapping = {
'alpha' : _.deburr(brew.title.toLowerCase()),
'created' : brew.createdAt,
'updated' : brew.updatedAt,
'views' : brew.views,
'latest' : brew.lastViewed
};
return mapping[this.state.sortType];
},
sortBrews : function(brews){
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
},
handleSortOptionChange : function(event){
this.setState({
sortType : event.target.value
});
},
handleSortDirChange : function(event){
this.setState({
sortDir : `${(this.state.sortDir == 'asc' ? 'desc' : 'asc')}`
});
},
renderSortOption : function(sortTitle, sortValue){
return <td>
<button
value={`${sortValue}`}
onClick={this.handleSortOptionChange}
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
>
{`${sortTitle}`}
</button>
</td>;
},
renderSortOptions : function(){
return <div className='sort-container'>
<table>
<tbody>
<tr>
<td>
<h6>Sort by :</h6>
</td>
{this.renderSortOption('Title', 'alpha')}
{this.renderSortOption('Created Date', 'created')}
{this.renderSortOption('Updated Date', 'updated')}
{this.renderSortOption('Views', 'views')}
{/* {this.renderSortOption('Latest', 'latest')} */}
<td>
<h6>Direction :</h6>
</td>
<td>
<button
onClick={this.handleSortDirChange}
className='sortDir'
>
{`${(this.state.sortDir == 'asc' ? '\u25B2 ASC' : '\u25BC DESC')}`}
</button>
</td>
</tr>
</tbody>
</table>
</div>;
},
getSortedBrews : function(){
return _.groupBy(this.props.brews, (brew)=>{
return (brew.published ? 'published' : 'private');
return (brew.published ? 'published' : 'private')
});
},
renderPrivateBrews : function(privateBrews){
if(!privateBrews || !privateBrews.length) return;
return [
<h1>{this.props.username}'s unpublished brews</h1>,
this.renderBrews(privateBrews)
];
},
render : function(){
const brews = this.getSortedBrews();
console.log('user brews', brews);
return <div className='userPage sitePage'>
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
return <div className='userPage page'>
<Navbar>
<Nav.section>
<NewBrew />
<RecentNavItem />
<Account />
<RecentNavItem.both />
<Account userPage={this.props.username} />
</Nav.section>
</Navbar>
<div className='content V3'>
<div className='content'>
<div className='phb'>
{this.renderSortOptions()}
<div>
<h1>{this.getUsernameWithS()} brews</h1>
{this.renderBrews(brews.published)}
</div>
<div>
<h1>{this.getUsernameWithS()} unpublished brews</h1>
{this.renderBrews(brews.private)}
</div>
<h1>{this.props.username}'s brews</h1>
{this.renderBrews(brews.published)}
{this.renderPrivateBrews(brews.private)}
</div>
</div>
</div>;
</div>
}
});

View File

@@ -17,7 +17,7 @@
.phb{
.noColumns();
height : auto;
min-height : 279.4mm;
min-height : 279.4mm;
margin : 20px auto;
&::after{
display : none;
@@ -30,44 +30,4 @@
}
}
.sort-container{
font-family : 'Open Sans', sans-serif;
position : fixed;
top : 35px;
border : 2px solid #58180D;
width : 675px;
background-color : #EEE5CE;
padding : 2px;
text-align : center;
z-index : 15;
h6{
text-transform : uppercase;
font-family : 'Open Sans', sans-serif;
font-size : 11px;
font-weight : bold;
color : #58180D;
}
table{
margin : 0px;
vertical-align : middle;
tbody tr{
background-color: transparent !important;
button{
background-color : transparent;
color : #58180D;
font-family : 'Open Sans', sans-serif;
font-size : 11px;
text-transform : uppercase;
font-weight : normal;
&.active{
font-weight : bold;
border : 2px solid #58180D;
}
&.sortDir{
width : 75px;
}
}
}
}
}
}
}

View File

@@ -1,19 +1,22 @@
module.exports = async(name, title = '', props = {})=>{
return `
<!DOCTYPE html>
<html>
<head>
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
</head>
<body>
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
<script src=${`/${name}/bundle.js`}></script>
<script>start_app(${JSON.stringify(props)})</script>
</body>
</html>
`;
};
module.exports = function(vitreum){
return `
<!DOCTYPE html>
<html>
<head>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link rel="icon" href="/assets/homebrew/favicon.ico" type="image/x-icon" />
<title>The Homebrewery - NaturalCrit</title>
${vitreum.head}
</head>
<body>
<main id="reactRoot">${vitreum.body}</main>
</body>
${vitreum.js}
</html>
`;
}

View File

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

4
config/production.json Normal file
View File

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

4
config/staging.json Normal file
View File

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

View File

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

View File

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

View File

@@ -1,20 +0,0 @@
#!/bin/sh
pkg install -y git nano node npm mongodb44
sysrc mongod_enable=YES
service mongod start
cd /usr/local/
git clone https://github.com/naturalcrit/homebrewery.git
cd homebrewery
npm install
npm audit fix
npm run postinstall
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
chmod +x /usr/local/etc/rc.d/homebrewery
sysrc homebrewery_enable=YES
service homebrewery start

View File

@@ -1,65 +0,0 @@
#!/bin/sh
#
# PROVIDE: homebrewery
# REQUIRE: NETWORKING
# KEYWORD: shutdown
# Author: S Robertson
# Version: 1.0.0
# Description:
# This script runs HomeBrewery as a service under the supplied user on boot
# How to use:
# Place this file in /usr/local/etc/rc.d/
# Add homebrewery_enable="YES" to /etc/rc.config
# (Optional) To run as non-root, add homebrewery_runAs="homebrewery" to /etc/rc.config
# (Optional) To pass HomeBrewery args, add homebrewery_args="" to /etc/rc.config
# Freebsd rc library
. /etc/rc.subr
# General Info
name="homebrewery" # Safe name of program
location="/usr/local/" # Install location
program_name="homebrewery" # Name of exec
title="HomeBrewery" # Title to display in top/htop
# RC.config vars
load_rc_config $name # Loading rc config vars
: ${homebrewery_enable="NO"} # Default: Do not enable HomeBrewery
: ${homebrewery_runAs="root"} # Default: Run HomeBrewery as root
: ${homebrewery_port=8000} # Default: Run HomeBrewery on port 8000
: ${homebrewery_NODE_ENV="local"} # Default: Run HomeBrewery in local mode
# Freebsd Setup
rcvar=homebrewery_enable # Enables the rc.conf YES/NO flag
pidfile="/var/run/${program_name}.pid" # File that allows the system to keep track of HomeBrewery status
# Env Setup
export HOME=$( getent passwd "homebrewery_runAs" | cut -d: -f6 ) # Gets the home directory of the runAs user
export NODE_ENV=${homebrewery_NODE_ENV}
export PORT=${homebrewery_port}
# Command Setup
exec_cmd="${location}/${program_name}/server.js" # Path to the HomeBrewery server.js, /usr/local/bin/ when installed globally
output_file="/var/log/${program_name}.log" # Path to HomeBrewery output file
# Command
command="/usr/sbin/daemon"
command_args="-r -t ${title} -u ${homebrewery_runAs} -o ${output_file} -P ${pidfile} /usr/local/bin/node ${exec_cmd} ${homebrewery_args}"
# Extra Commands
extra_commands="dev_mode"
dev_mode_cmd="homebrewery_dev_mode"
homebrewery_dev_mode() {
echo "Starting HomeBrewery in live rebuild Developer mode..."
cd ${location}/${program_name}/
/usr/local/bin/node ${location}/${program_name}/scripts/buildHomebrew.js --dev
}
# Loading Config
load_rc_config ${name}
run_rc_command "$1"

17037
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,82 +1,55 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "2.13.2",
"engines": {
"node": "14.15.x"
},
"repository": {
"type": "git",
"url": "git://github.com/naturalcrit/homebrewery.git"
},
"version": "3.0.0",
"scripts": {
"dev": "node scripts/dev.js",
"quick": "node scripts/quick.js",
"build": "node scripts/buildHomebrew.js",
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
"lint": "eslint --fix **/*.{js,jsx}",
"lint:dry": "eslint **/*.{js,jsx}",
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
"verify": "npm run lint && npm test",
"test": "pico-check",
"test:dev": "pico-check -v -w",
"phb": "node scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run buildall",
"start": "node server.js"
"build": "node scripts/build.js",
"populate": "node scripts/populate.js",
"prod": "set NODE_ENV=production&& npm run build",
"postinstall": "npm run build",
"start": "node server.js",
"snippet": "nodemon scripts/snippet.test.js",
"todo": "./node_modules/.bin/fixme -i node_modules/** -i build/**",
"test": "mocha tests",
"test:dev": "nodemon -x mocha tests || exit 0",
"test:markdown": "nodemon -x mocha tests/markdown.test.js || exit 0"
},
"author": "stolksdorf",
"license": "MIT",
"eslintIgnore": [
"build/*"
],
"pico-check": {
"require": "./tests/test.init.js"
},
"babel": {
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
},
"dependencies": {
"@babel/core": "^7.14.8",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.14.8",
"@babel/preset-react": "^7.14.5",
"body-parser": "^1.19.0",
"classnames": "^2.3.1",
"codemirror": "^5.62.2",
"cookie-parser": "^1.4.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.9.0",
"express": "^4.17.1",
"express-async-handler": "^1.1.4",
"express-static-gzip": "2.1.1",
"fs-extra": "10.0.0",
"googleapis": "82.0.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "2.1.3",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.1",
"mongoose": "^5.13.4",
"nanoid": "3.1.23",
"nconf": "^0.11.3",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-frame-component": "4.1.3",
"react-router-dom": "5.2.0",
"sanitize-filename": "1.6.3",
"superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
"basic-auth": "^1.0.3",
"body-parser": "^1.14.2",
"classnames": "^2.2.0",
"codemirror": "^5.22.0",
"cookie-parser": "^1.4.3",
"egads": "^1.0.1",
"express": "^4.13.3",
"jwt-simple": "^0.5.1",
"lodash": "^4.17.3",
"loglevel": "^1.4.1",
"marked": "^0.3.5",
"moment": "^2.11.0",
"mongoose": "^4.3.3",
"nconf": "^0.8.4",
"pico-flux": "^2.1.2",
"pico-router": "^1.1.0",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"shortid": "^2.2.4",
"striptags": "^2.1.1",
"superagent": "^1.6.1",
"vitreum": "^4.0.12"
},
"devDependencies": {
"eslint": "^7.31.0",
"eslint-plugin-react": "^7.24.0",
"pico-check": "^2.1.3"
"app-module-path": "^2.1.0",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"chai-subset": "^1.4.0",
"fixme": "^0.4.3",
"mocha": "^3.2.0",
"supertest": "^2.0.1",
"supertest-as-promised": "^4.0.2"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +0,0 @@
# Notes
User-agent: *
Disallow: /edit/

21
scripts/build.js Normal file
View File

@@ -0,0 +1,21 @@
const label = 'build';
console.time(label);
const clean = require('vitreum/steps/clean.js');
const jsx = require('vitreum/steps/jsx.js');
const lib = require('vitreum/steps/libs.js');
const less = require('vitreum/steps/less.js');
const asset = require('vitreum/steps/assets.js');
const Proj = require('./project.json');
Promise.resolve()
.then(()=>clean())
.then(()=>lib(Proj.libs))
.then(()=>jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, ['./shared']))
.then((deps)=>less('homebrew', ['./shared'], deps))
.then(()=>jsx('admin', './client/admin/admin.jsx', Proj.libs, ['./shared']))
.then((deps)=>less('admin', ['./shared'], deps))
.then(()=>asset(Proj.assets, ['./shared', './client']))
.then(()=>console.timeEnd.bind(console, label))
.catch(console.error);

View File

@@ -1,31 +0,0 @@
const fs = require('fs-extra');
const Proj = require('./project.json');
const { pack } = require('vitreum');
const isDev = !!process.argv.find((arg)=>arg=='--dev');
const lessTransform = require('vitreum/transforms/less.js');
const assetTransform = require('vitreum/transforms/asset.js');
//const Meta = require('vitreum/headtags');
const transforms = {
'.less' : lessTransform,
'*' : assetTransform('./build')
};
const build = async ({ bundle, render, ssr })=>{
await fs.outputFile('./build/admin/bundle.css', await lessTransform.generate({ paths: './shared' }));
await fs.outputFile('./build/admin/bundle.js', bundle);
await fs.outputFile('./build/admin/ssr.js', ssr);
await fs.outputFile('./build/admin/render.js', render);
};
fs.emptyDirSync('./build/admin');
pack('./client/admin/admin.jsx', {
paths : ['./shared'],
libs : Proj.libs,
dev : isDev && build,
transforms
})
.then(build)
.catch(console.error);

View File

@@ -1,78 +0,0 @@
const fs = require('fs-extra');
const zlib = require('zlib');
const Proj = require('./project.json');
const { pack, watchFile, livereload } = require('vitreum');
const isDev = !!process.argv.find((arg)=>arg=='--dev');
const lessTransform = require('vitreum/transforms/less.js');
const assetTransform = require('vitreum/transforms/asset.js');
const babel = require('@babel/core');
const less = require('less');
const babelify = async (code)=>(await babel.transformAsync(code, { presets: ['@babel/preset-env', '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
const transforms = {
'.js' : (code, filename, opts)=>babelify(code),
'.jsx' : (code, filename, opts)=>babelify(code),
'.less' : lessTransform,
'*' : assetTransform('./build')
};
const build = async ({ bundle, render, ssr })=>{
const css = await lessTransform.generate({ paths: './shared' });
await fs.outputFile('./build/homebrew/bundle.css', css);
await fs.outputFile('./build/homebrew/bundle.js', bundle);
await fs.outputFile('./build/homebrew/ssr.js', ssr);
await fs.copy('./themes/fonts', './build/fonts');
let src = './themes/5ePhbLegacy.style.less';
//Parse brew theme files
less.render(fs.readFileSync(src).toString(), {
compress : !isDev
}, function(e, output) {
fs.outputFile('./build/themes/5ePhbLegacy.style.css', output.css);
});
src = './themes/5ePhb.style.less';
less.render(fs.readFileSync(src).toString(), {
compress : !isDev
}, function(e, output) {
fs.outputFile('./build/themes/5ePhb.style.css', output.css);
});
// await less.render(lessCode, {
// compress : !dev,
// sourceMap : (dev ? {
// sourceMapFileInline: true,
// outputSourceFiles: true
// } : false),
// })
//compress files in production
if(!isDev){
await fs.outputFile('./build/homebrew/bundle.css.br', zlib.brotliCompressSync(css));
await fs.outputFile('./build/homebrew/bundle.js.br', zlib.brotliCompressSync(bundle));
await fs.outputFile('./build/homebrew/ssr.js.br', zlib.brotliCompressSync(ssr));
} else {
await fs.remove('./build/homebrew/bundle.css.br');
await fs.remove('./build/homebrew/bundle.js.br');
await fs.remove('./build/homebrew/ssr.js.br');
}
};
fs.emptyDirSync('./build');
pack('./client/homebrew/homebrew.jsx', {
paths : ['./shared'],
libs : Proj.libs,
dev : isDev && build,
transforms
})
.then(build)
.catch(console.error);
//In development set up a watch server and livereload
if(isDev){
livereload('./build');
watchFile('./server.js', {
watch : ['./client'] // Watch additional folders if you want
});
}

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