0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-23 20:53:05 +00:00

Compare commits

..

6 Commits
v2.13.2 ... tpk

Author SHA1 Message Date
Scott Tolksdorf
22a480871b 'Sheet 2016-05-16 22:42:22 -04:00
Scott Tolksdorf
daa3b096b3 'Adding 2016-05-16 22:20:35 -04:00
Scott Tolksdorf
bfcf6ca7f2 Getting ready to switch to jsx 2016-05-16 22:03:20 -04:00
Scott Tolksdorf
15ffb138eb Editor working 2016-05-16 21:58:40 -04:00
Scott Tolksdorf
7321cc81ec It livesssssssss 2016-05-16 21:51:48 -04:00
Scott Tolksdorf
ba6ba0e51f tpk server is working 2016-05-16 21:38:37 -04:00
342 changed files with 32650 additions and 27597 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,14 @@
<!-- CLICK "Preview" FOR INSTRUCTIONS IN A MORE READABLE FORMAT -->
**Browser Type/Version**: [Google Ultron v90.01]
## Before you submit
**Operating System**: [GLaDOS v34.5.8]
- 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.
**Issue Description**: [The thing won't thing]
**Markdown code to reproduce**:
*Delete the above section and the instructions in the sections below before submitting*
```
# thing
> thing 2
```
## 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>
**Related Images** :

28
.gitignore vendored
View File

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

View File

@@ -1,19 +1,29 @@
FROM node:14.15
FROM node:latest
ENV NODE_ENV=docker
MAINTAINER David Hudson <jendave@yahoo.com>
# Create app directory
WORKDIR /usr/src/app
# System update
RUN apt-get -q -y update
# 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
RUN apt-get -q -y install npm
RUN apt-get -q -y install mongodb
# Bundle app source and build application
COPY . .
RUN yarn build
RUN apt-get clean && rm -r /var/lib/apt/lists/*
EXPOSE 22
EXPOSE 8000
CMD [ "yarn", "start" ]
ADD start.sh /start.sh
RUN chmod +x /start.sh
VOLUME ["/opt/apps"]
COPY . /opt/apps/naturalcrit/
WORKDIR /opt/apps/naturalcrit/
RUN npm install
RUN npm install -g gulp-cli
RUN npm install gulp
RUN gulp fresh
CMD ["/start.sh"]

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

160
README.md
View File

@@ -1,127 +1,33 @@
# The Homebrewery
[![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).
[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:
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. `npm install`
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.
### Running the application via Docker
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
### Running the application on FreeBSD or FreeNAS
Please see the docs here: [README.FreeBSD.md](./README.FREEBSD.md)
### 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
# NaturalCrit
A tool suite for DMs to use for D&D. Check it out [here](http://www.naturalcrit.com).
### Getting started
1. Make sure you have [node](https://nodejs.org/en/)
1. Clone down the repo
1. In your terminal, head to the repo
1. Run `npm install` to get all the dependacies
2. Run `npm install -g gulp` to install the gulp build tool
1. Run `gulp fresh`, this will compile and build all the needed libraries (this only has to be done once, unless you add more libs)
1. Run `gulp` to run the project locally. Should be accessible at `localhost:8000`
2. Any changes to files within the proejct will be detected and the propject will automatically re-build
**Notes:** If you'd like to create and edit homebrews, you'll need to have MongoDB installed and running.
Have fun!
### Docker Image
You can use [Docker](https://docs.docker.com) to get up and running with NaturalCrit.
1. Install Docker
1. Clone the repo
1. In the terminal, go to the repo
1. Build the docker image `docker build -t naturalcrit .`
1. Run the docker container `docker run -dit -p 8000:8000 naturalcrit`
1. You can check out the website on your computer on port 8000
1. You may have to use `docker-machine env` to get the IP address of your docker instance
### changelog
You can check out the changelog [here](https://github.com/stolksdorf/NaturalCrit/blob/master/changelog.md)

View File

@@ -1,383 +1,111 @@
<style>
h5 {
font-size: .35cm !important;
}
</style>
# changelog
### Friday, 30/07/2021 - v2.13.2
- Background work to allow new themes in the future
- Fixed cursor getting stuck when resizing divider bar
##### G-Ambatte :
- Fix Style tab not copying when Cloned To New
- Basic brew sorting on User page
- Reduced data sent on each request from server
##### Gazook89 :
- Cleaned up styling on menus
### Saturday, 28/6/2021 - v2.13.1
- Fixed the issue with new brews not saving!
### Saturday, 26/6/2021 - v2.13.0
- "Share to Reddit" button now works with Google brews
- Downloading or viewing the source of your brew will now show the contents of the Style tab at the top of the document in a backtick code fence like this:
\`\`\`css
myStyle {color: black}
\`\`\`
##### G-Ambatte :
- New **Download**, **View**, and **Clone to New** buttons in the "Source" dropdown on the Share page.
- Pasting your brew into a "New" page and saving will transfer any CSS in the code fence to the Style tab.
- Unsaved work in the New page Style tab is now cached to your browser storage if you navigate away.
### Thursday, 10/6/2021 - v2.12.0
- New "style" tab to better organize custom CSS in preparation for new themes and sharable styles.
- Your own Google brews will no longer show up in the list when viewing someone else's profile.
### Saturday, 02/5/2021 - v2.11.2
- Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery.
- Move cursor to end of snippet after insertion
### Saturday, 20/3/2021 - v2.11.1
- Warning when opening brew in your Google Drive trash
##### G-Ambatte :
- Snippet to remove drop caps (fancy first letter after title)
```
```
### Saturday, 13/3/2021 - v2.11.0
- Many background things for upcoming v3. Get pumped.
##### G-Ambatte :
- Fixed new brews failing to save when auto-generated file name is too long.
- "New" button added to the Nav bar.
- "Download" button to download your brew as a text file.
- Reduced download size and improved caching.
##### RKuerten :
- Bold and Italics hotkeys for Mac users (Cmd+B, Cmd+I)
### Friday, 25/1/2021 - v2.10.7
- Cover Page snippet now flips left-right page numbering.
- Added instructions for [installing on a FreeBSD Jail](https://github.com/naturalcrit/homebrewery/blob/master/README.FREEBSD.md).
- Fix for box-shadows breaking across columns. <br>(Thanks G-Ambatte for all of these!)
- Small user interface tweaks (Thanks Ericsheid)
### Friday, 02/1/2021 - v2.10.6
- Fixed punctuation for usernames ending with 's' on the user page. (Thanks AlexeySachkov)
- Fixed server crashes due to excessive long lines in brews
- Fixed "automated request" lockouts from Google
### Friday, 18/12/2020 - v2.10.5
- Brews now immediately save when transferring between Google Drive and Homebrewery storage.
- Added confirmation popup to clarify the transfer process.
- Brews transferred or deleted from Google will be found in your Google Drive trash.
- Dependency updates.
### Wednesday, 25/11/2020 - v2.10.4
- Fixed Google Drive brews not saving metadata (view count, description, etc.) Note that we are still working on making published Google brews visible to the public when viewing your profile page.
### Thursday, 22/10/2020 - v2.10.3
- Fixed brews with broken code crashing the edit page when loaded (the "blue screen of death" bug).
### Monday, 19/10/2020 - v2.10.2
- Fixed issue with "recent" item links not updating when transferring between Google Drive.
### Monday, 12/10/2020 - v2.10.1
- Fixed issue with users unable to create new brews
- Fixing brews being lost when loaded via back button
\page
### Wednesday, 07/10/2020 - v2.10.0
- Google Drive integration -- Sign in 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!
### Friday, 28/08/2020 - v2.9.2
- Many dependency updates
- Finally fixed this changelog page to not run off the edge :P
### Sunday, 19/07/2020 - v2.9.1
- Fixed paragraphs appearing blank on new columns
### Wednesday, 20/05/2020 - v2.9.0
- Major refactoring of site backend to work with updated dependencies for security (should be invisible to users)
### Wednesday, 11/03/2020 - v2.8.2
- Fixed delete button removing everyone's copy for brews with multiple authors
- Compressed homebrew text in database
### Monday, 26/11/2018 - v2.8.1
- Fixed some SSL issues with images in the example page so they appear now
- Fixed duplicate scrollbars in Edit Page
- Fixed issue of being unable to change brew metadata
- Sanitized script tags-javascript typed into the editor was crashing brews
### Sunday, 08/04/2018 - v2.8.0
- Re-enabled box shadows for PDF output
- Added a "contributing guide" for the GitHub
- "Report Issue" navbar button now links to the subreddit
- Refactored background code
### Sunday, 04/06/2017 - v2.7.5
- Fixed the class feature snippet duplicating the entire brew
- Fixed headers in tables being duplicated
- Fixed border-image being scrambled on class tables and descriptive text boxes
- Fixed pages going out of sync in large brews, causing them to be rendered off-page
- Improved performance in the preview window when scrolling through large brews
- Text in the "view source" page now wraps
### Saturday, 22/04/2017 - v2.7.4
- Give ability to hide the render warning notification
### Friday, 03/03/2017 - v2.7.3
- Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews.
### Saturday, 18/02/2017 - v2.7.2
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
### Thursday, 19/01/2017 - v2.7.1
- Fixed saving multiple authors and multiple systems on brew metadata (thanks u/PalaNolho re:282)
- Adding in line highlight for new pages
- Added in a simple brew lookup for admin
### Saturday, 14/01/2017 - v2.7.0
- Added a new Render Warning overlay. It detects situations where the brew may not be rendering correctly (wrong browser, browser is zoomed in...) and let's the user know
### Sunday, 25/12/2016 - v2.7.0
- Switching over to using Vitreum v4
- Removed gulp, all tasks are run through npm scripts
- Updating docs for local dev
- Removing support for Docker. I have never used it, nor will I ever test for it, so I don't want to continue to explictly support it on this repo. Feel free to make a fork and make it docker-able though :)
- Changed icon for the metadata
- Made links useable in footer (thanks u/Dustfinger1 re:249)
- Added print media queries to remove box shadow on print (thanks u/dmmagic re: 246)
- Fixed realtime renderer not functioning if loaded with malformed html on load (thanks u/RattiganIV re:247)
- Removed a lot of unused files in shared
- vitreum v4 now lets me use codemirror as a pure node dependacy
### Saturday, 03/12/2016 - v2.6.0
- Added report back to the edit page
- Changed metaeditor icon
- Added a button to quickly share your brew to reddit :)
- Disabled Partial Page Rendering unless your brew hits 75 pages or longer
- The brew renderer will now try and use your first page to judge the page size of each of your brews. This allows you now to set landscape and other weird sizes and everthing should work fine :)
- UI on the user page improved (thanks u/PalaNolho)
- Fixed lists not breaking across columns (thanks u/tyson-nw)
- Added a table of contents snippet (thanks u/tullisar)
- Added a multicolumn snippet
### Thursday, 01/12/2016
- Added in a snippet for a split table
- Added an account nav item to new page
### Sunday, 27/11/2016 - v2.5.1
- Fixed the column rendering on the new user page. Really should have tested that better
- Added a hover tooltip to fully read the brew description
- Made the brew items take up only 25% allowing you to view more per row.
\page
### Wednesday, 23/11/2016 - v2.5.0
- Metadata can now be added to brews
- Added a metadata editor onto the edit and new pages
- Moved deleting a brew into the metadata editor
- Added in account middleware
- Can now search for brews by a specific author
- Editing a brew in anyway while logged in will now add you to the list of authors
- Added a new user page to see others published brews, as well as all of your own brews.
- Added a new nav item for accessing your profile and logging in
### Monday, 14/11/2016
- Updated snippet bar style
- You can now print from a new page without saving
- Added the ability to use ctrl+p and ctrl+s to print and save respectively.
### Monday, 07/11/2016
- Added final touches to the html validator and updating the rest of the branch
- If anyone finds issues with the new HTML validator, please let me know. I hope this will bring a more consistent feel to Homebrewery rendering.
### Friday, 09/09/2016 - v2.4.0
- Adding in a HTML validator that will display warnings whenever you save. This should stop a lot of the issues generated with pages not showing up.
### Saturday, 20/08/2016 - v2.3.0
- Added in a license file
- Updated the welcome text
- Added in a much better Error page
- If you visit a deleted brew, it will now remove it from your recent list. (Thanks u/sIllverback!)
- Improved parsing of embedded html text in brews. (Thanks u/com-charizard!)
- Added in a new coverpage snippet
- Homebrewery will now try and onsert a good title for your brew if you don't provide one
- Homebrewery now re-renders properly when you zoom
- Fixed the noteblock overlapping into titles (thanks u/dsompura!)
- Fixed a bad search route in the admin panel (thanks u/SnappyTom!)
### Friday, 29/07/2016 - v2.2.7
- Adding in descriptive note blocks. (Thanks calculuschild!)
### Thursday, 07/07/2016 - v2.2.6
- Added a new nav item on the homepage for accessing both recently viewed and edited brews (thanks [ChosenSeraph!](https://github.com/stolksdorf/homebrewery/issues/147))
### Friday, 10/06/2016 - v2.2.4
- Added an id to each rendered page
- Allows adding in hyperlinks to specific pages
- Even works after you print to pdf!
### Tuesday, 07/06/2016 - v2.2.2
- Fixed bug with new markdown lexer and aprser not working on print page
### Sunday, 05/06/2016 - v2.2.1
- Adding in a new Class table div block. The old Class table block used weird stacking of HTML elements, resulting is difficult to control behaviour and poor interactiosn with the rest of the page. This new block is much easier to style and work with.
- Added in a new wide table snippet
- Added in a new auto-incremeting page number snippet (thakns u/Ryrok!)
- Lists in monster stat blocks should be fixed now
### Saturday, 04/06/2016 - v2.2.0
- MIgrating The Homebrewery over to hombrewery.naturalcrit.com. It know runs on it's own server, with it's own repo separate from the other tools I'm working on. Makes updating and deploying much easier.
### Sunday, 29/05/2016 - v2.1.0
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
- The onboarding flow has also been confusing a few users (Homepage -> new -> save -> edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
- Paragraphs now indent properly after lists (thanks u/slitjen!)
### Friday, 27/05/2016 - v2.0.6
- Updated the issue template for (hopefully) better reporting
- Added suggestion to use chrome while PDF printing
### Wednesday, 25/05/2016 -v2.0.5
- The class table generators have the proper ability score improvement progression.
\page
### Tuesday, 24/05/2016 - v2.0.4
- Fixed extra wide monster stat blocks sometimes only being one column
- The class table generators now follow the proper progression from the PHB (thakns u/IrishBandit)
### Thursday, 19/05/2016 - v2.0.2
- No longer server-side pre-render brews, just incase the user entered invalid HTML, it might crahsh the server
- Bumped up the allowed entity size for extra-large brew (Thanks for reporting it dickboner93)
- Added a little error box when a save fails with a custom link to reporting the issue on github.
### Saturday, 14/05/2016 - v2.0.0 (finally!)
I've been working on v2 for a *very* long time. I want to thank you guys for being paitent.
It started rather small, but as more and more features were added, I decided to just wait until everything was polished.
Massive changelog incoming:
#### Brews
- New flow for creating new brews. Before creating a new brew would immedaitely create a brew in the database and let you edit it. Many people would create a new brew just to experiment and close it, which lead to many "abandoned brews" (see the Under the hood section below). This started eating up a ton of database space. You now have to manually save a new brew for the first time, however Homebrewery will store anything you don't have saved in local storage, just in case your browser crashes or whatever, it will load that up when you go back to `/homebrew/new`
- Black blocking edges around notes and stat blocks when printing to PDFs have been fixed
- Borders sometimes not showing up in the second column have been fixed
- All pseudo-element borders have been replaced with reliable border images.
- Chrome can finally print to PDF as good as Chrome Canary! Updating instructions.
- Added a little page number box.
- Added in a new editable Brew Title. This will be shown in the navbar on share pages, and will default to the file name when you save as PDF. All exsisting brews will be defaulted with an empty title.
- Mutliline lists render better now
- Firefox rendering has been slithgly improved. Firefox and Chrome render things **slightly** differently, over the course of a brew, these little changes add up and lead to very noticable rendering differences between the browsers. I'm trying my best to get Firefox rendering better, but it's a difficult problem.
- A bunch of you have wanted to donate to me. I am super flattered by that. I created a [patreon page](https://www.patreon.com/stolksdorf). If you feel like helping out, head here :)
#### Under the Hood Stuff
- Setup a proper staging environment. Will be using this for tests, and hopefully getting the community to help me test future versions
- Server-side prerendering now much faster
- Regular weekly database back-ups. Your brews are safe!
- Database is now uniquely indexed on both editId and shareId, page loads/saving should be much faster
- Improved Admin console. This helps me answer people's questions about issues with their brews
- Added a whole querying/pagniation API that I can use for stats and answering questions
- Clearing out "Abandoned" brews (smaller than a tweet and haven't been viewed for a week). These account for nearly a third of all stored brews.
#### Interface
- Added in a whole new editor with syntax highlighting for markdown
- Built a splitpane! Remembers where you left the split in between sessions
- Re-organized the snippets into a hierarchical groups. Should be much easier to find what you need
- Partial page rendering is working. The Homebrewery will now only load the viewable pages, and any page with `<style>` tags on them. If you are working on a large brew you should notice *significant* performance improvements
- Edit page saving interface has been improved significantly. Auto-saves after 3 seconds on inactivity, now allows user to save at anytime. Will stop the tab from closing witha pop-up if there are unsaved changes.
- Navbar and overall style has been improved and spacing made more consistent
- Elements under the hood are way more organized and should behaviour much more reliably in many sizes.
- Source now opens to it's own route `/source/:sharedId` instead of just a window. Now easier to share, and won't be blocked by some browsers.
- Print page now auto-opens print dialog. If you want to share your print page link, just remove the `?dialog=true` parameter and it won't open the dialog.
\page
### Wednesday, 20/04/2016
- A lot of admin improvements. Pagninated brew table
- Added a searching and removing abandoned brew api endpoints (turns out about 40% of brews are shorter that a tweet!).
- Fixed the require cache being cleared. Pages should render a bit faster now.
- Pulled in `kkragenbrink`s fix for nested lists, Thanks!
### Wednesday, 06/04/2016 - v1.4
* Pages will now partially render. This should greatly speed up *very* large homebrews. The Homebreery will figure out which page you should be looking at and render that page, the page before, and the page after.
* Zooming should be fixed. I've changed the font size units to be cm, which match the units of the page. Zooming in and out now look much better.
### Monday, 29/02/2016 - v1.3.1
* Removng the changelog button from the Share page
* Added a A4 page size snippet (thanks guppy42!)
* Added support for `<sup>` and `<sub>` tags (thanks crashinworld14!)
### Saturday, 20/02/2016
* Fixed h1 headers not going full width (thanks McToomin27)
* Added a github issue template
## v1.3.0
### Friday, 19/02/2016
* Improved the admin panel
* Added ability to clear away old empty brews
* Added delete button to the edit page
* Added a dynamically updating changelog page! Nifty!
* Added stlying for wide monster stat blocks and single column class tables
* Added snippets for wide monster stat blocks and single column class tables
### Tuesday, 16/02/2016
* Paragraphs right after tables now indent (thanks LikeAJi6!)
* Added a `@page` css rule to auto turn off margins when printing
* Added a `page-break` property on each `.phb` page to properly page the pages up when exporting (thanks Jokefury!)
* Improved first character rendering on Firefox
* Improved table spacing a bit
* Changed padding at page bottom for better fit and clipping of elements
* Improved spacing for bold text (thanks nickpunt!)
## v1.2.0
### Sunday, 17/01/2016
* Added a printer friendly snippet that injects some CSS to remove backbrounds and images
* Adjusted the styling specific to spell blocks to give it tighter spacing
* Added a changelog! How meta!
## v1.1.0
### Thursday, 14/01/2016
* Added view source to see the markdown that made the page
* Added print view
* Fixed API issues that were causing the server to crash
* Increased padding on table cells
* Raw html now shows in view source
## v1.0.0 - Release
### Wednesday, 3/01/2016
* Added `phb.standalone.css` plus a build system for creating it
* Added page numbers and footer text
* Page accent now flips each page
# changelog
### Saturday, 14/05/2016 - v2.0.0 (finally!)
I've been working on v2 for a *very* long time. I want to thank you guys for being paitent.
It started rather small, but as more and more features were added, I decided to just wait until everything was polished.
Massive changelog incoming:
#### Brews
- New flow for creating new brews. Before creating a new brew would immedaitely create a brew in the database and let you edit it. Many people would create a new brew just to experiment and close it, which lead to many "abandoned brews" (see the Under the hood section below). This started eating up a ton of database space. You now have to manually save a new brew for the first time, however Homebrewery will store anything you don't have saved in local storage, just in case your browser crashes or whatever, it will load that up when you go back to `/homebrew/new`
- Black blocking edges around notes and stat blocks when printing to PDFs have been fixed
- Borders sometimes not showing up in the second column have been fixed
- All pseudo-element borders have been replaced with reliable border images.
- Chrome can finally print to PDF as good as Chrome Canary! Updating instructions.
- Added a little page number box.
- Added in a new editable Brew Title. This will be shown in the navbar on share pages, and will default to the file name when you save as PDF. All exsisting brews will be defaulted with an empty title.
- Mutliline lists render better now
- Firefox rendering has been slithgly improved. Firefox and Chrome render things **slightly** differently, over the course of a brew, these little changes add up and lead to very noticable rendering differences between the browsers. I'm trying my best to get Firefox rendering better, but it's a difficult problem.
- A bunch of you have wanted to donate to me. I am super flattered by that. I created a [patreon page](https://www.patreon.com/stolksdorf). If you feel like helping out, head here :)
#### Under the Hood Stuff
- Setup a proper staging environment. Will be using this for tests, and hopefully getting the community to help me test future versions
- Server-side prerendering now much faster
- Regular weekly database back-ups. Your brews are safe!
- Database is now uniquely indexed on both editId and shareId, page loads/saving should be much faster
- Improved Admin console. This helps me answer people's questions about issues with their brews
- Added a whole querying/pagniation API that I can use for stats and answering questions
- Clearing out "Abandoned" brews (smaller than a tweet and haven't been viewed for a week). These account for nearly a third of all stored brews.
#### Interface
- Added in a whole new editor with syntax highlighting for markdown
- Built a splitpane! Remembers where you left the split in between sessions
- Re-organized the snippets into a hierarchical groups. Should be much easier to find what you need
- Partial page rendering is working. The Homebrewery will now only load the viewable pages, and any page with `<style>` tags on them. If you are working on a large brew you should notice *significant* performance improvements
- Edit page saving interface has been improved significantly. Auto-saves after 3 seconds on inactivity, now allows user to save at anytime. Will stop the tab from closing witha pop-up if there are unsaved changes.
- Navbar and overall style has been improved and spacing made more consistent
- Elements under the hood are way more organized and should behaviour much more reliably in many sizes.
- Source now opens to it's own route `/source/:sharedId` instead of just a window. Now easier to share, and won't be blocked by some browsers.
- Print page now auto-opens print dialog. If you want to share your print page link, just remove the `?dialog=true` parameter and it won't open the dialog.
\page
### Wednesday, 20/04/2016
- A lot of admin improvements. Pagninated brew table
- Added a searching and removing abandoned brew api endpoints (turns out about 40% of brews are shorter that a tweet!).
- Fixed the require cache being cleared. Pages should render a bit faster now.
- Pulled in `kkragenbrink`s fix for nested lists, Thanks!
### Wednesday, 06/04/2016 - v1.4
* Pages will now partially render. This should greatly speed up *very* large homebrews. The Homebreery will figure out which page you should be looking at and render that page, the page before, and the page after.
* Zooming should be fixed. I've changed the font size units to be cm, which match the units of the page. Zooming in and out now look much better.
### Monday, 29/02/2016 - v1.3.1
* Removng the changelog button from the Share page
* Added a A4 page size snippet (thanks guppy42!)
* Added support for `<sup>` and `<sub>` tags (thanks crashinworld14!)
### Saturday, 20/02/2016
* Fixed h1 headers not going full width (thanks McToomin27)
* Added a github issue template
## v1.3.0
### Friday, 19/02/2016
* Improved the admin panel
* Added ability to clear away old empty brews
* Added delete button to the edit page
* Added a dynamically updating changelog page! Nifty!
* Added stlying for wide monster stat blocks and single column class tables
* Added snippets for wide monster stat blocks and single column class tables
### Tuesday, 16/02/2016
* Paragraphs right after tables now indent (thanks LikeAJi6!)
* Added a `@page` css rule to auto turn off margins when printing
* Added a `page-break` property on each `.phb` page to properly page the pages up when exporting (thanks Jokefury!)
* Improved first character rendering on Firefox
* Improved table spacing a bit
* Changed padding at page bottom for better fit and clipping of elements
* Improved spacing for bold text (thanks nickpunt!)
## v1.2.0
### Sunday, 17/01/2016
* Added a printer friendly snippet that injects some CSS to remove backbrounds and images
* Adjusted the styling specific to spell blocks to give it tighter spacing
* Added a changelog! How meta!
## v1.1.0
### Thursday, 14/01/2016
* Added view source to see the markdown that made the page
* Added print view
* Fixed API issues that were causing the server to crash
* Increased padding on table cells
* Raw html now shows in view source
## v1.0.0 - Release
### Wednesday, 3/01/2016
* Added `phb.standalone.css` plus a build system for creating it
* Added page numbers and footer text
* Page accent now flips each page

View File

@@ -1,37 +1,40 @@
require('./admin.less');
const React = require('react');
const createClass = require('create-react-class');
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var HomebrewAdmin = require('./homebrewAdmin/homebrewAdmin.jsx');
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 {};
var Admin = React.createClass({
getDefaultProps: function() {
return {
url : "",
admin_key : "",
homebrews : [],
};
},
render : function(){
return <div className='admin'>
var self = this;
return(
<div className='admin'>
<header>
<div className='container'>
<i className='fa fa-rocket' />
naturalcrit admin
</div>
</header>
<header>
<div className='container'>
<i className='fas fa-rocket' />
homebrewery admin
<a target="_blank" href='https://www.google.com/analytics/web/?hl=en#report/defaultid/a72212009w109843310p114529111/'>Link to Google Analytics</a>
<HomebrewAdmin homebrews={this.props.homebrews} admin_key={this.props.admin_key} />
</div>
</header>
<div className='container'>
<Stats />
<hr />
<BrewLookup />
<hr />
<BrewCleanup />
<hr />
<BrewCompress />
</div>
</div>;
);
}
});

View File

@@ -1,44 +1,39 @@
@import 'naturalcrit/styles/reset.less';
@import 'naturalcrit/styles/elements.less';
@import 'naturalcrit/styles/animations.less';
@import 'naturalcrit/styles/colors.less';
@import 'naturalcrit/styles/tooltip.less';
@import 'font-awesome/css/font-awesome.css';
html,body, #reactContainer, .naturalCrit{
min-height : 100%;
}
@sidebarWidth : 250px;
body{
background-color : #eee;
font-family : 'Open Sans', sans-serif;
color : #4b5055;
font-weight : 100;
text-rendering : optimizeLegibility;
margin : 0;
padding : 0;
height : 100%;
}
.admin{
header{
background-color : @red;
font-size: 2em;
padding : 20px 0px;
color : white;
margin-bottom: 30px;
i{
margin-right: 30px;
}
}
hr{
margin : 30px 0px;
}
}
@import 'naturalcrit/styles/reset.less';
@import 'naturalcrit/styles/elements.less';
@import 'naturalcrit/styles/animations.less';
@import 'naturalcrit/styles/colors.less';
@import 'naturalcrit/styles/tooltip.less';
@import 'font-awesome/css/font-awesome.css';
html,body, #reactContainer, .naturalCrit{
min-height : 100%;
}
@sidebarWidth : 250px;
body{
background-color : #eee;
font-family : 'Open Sans', sans-serif;
color : #4b5055;
font-weight : 100;
text-rendering : optimizeLegibility;
margin : 0;
padding : 0;
height : 100%;
}
.admin{
header{
background-color : @red;
font-size: 2em;
padding : 20px 0px;
color : white;
margin-bottom: 30px;
i{
margin-right: 30px;
}
}
}

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,82 +0,0 @@
require('./brewLookup.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const request = require('superagent');
const Moment = require('moment');
const BrewLookup = createClass({
getDefaultProps() {
return {};
},
getInitialState() {
return {
query : '',
foundBrew : null,
searching : false,
error : null
};
},
handleChange(e){
this.setState({ query: e.target.value });
},
lookup(){
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 }));
},
renderFoundBrew(){
const brew = this.state.foundBrew;
return <div className='foundBrew'>
<dl>
<dt>Title</dt>
<dd>{brew.title}</dd>
<dt>Authors</dt>
<dd>{brew.authors.join(', ')}</dd>
<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>;
},
render(){
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>
{this.state.error
&& <div className='error'>{this.state.error.toString()}</div>
}
{this.state.foundBrew
? this.renderFoundBrew()
: <div className='noBrew'>No brew found.</div>
}
</div>;
}
});
module.exports = BrewLookup;

View File

@@ -1,30 +0,0 @@
.brewLookup{
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;
}
}
}

View File

@@ -0,0 +1,153 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var request = require('superagent');
var Moment = require('moment');
var HomebrewAdmin = React.createClass({
getDefaultProps: function() {
return {
admin_key : ''
};
},
getInitialState: function() {
return {
page: 0,
count : 20,
brewCache : {},
total : 0,
processingOldBrews : false
};
},
fetchBrews : function(page){
request.get('/homebrew/api/search')
.query({
admin_key : this.props.admin_key,
count : this.state.count,
page : page
})
.end((err, res)=>{
this.state.brewCache[page] = res.body.brews;
this.setState({
brewCache : this.state.brewCache,
total : res.body.total,
count : res.body.count
})
})
},
componentDidMount: function() {
this.fetchBrews(this.state.page);
},
changePageTo : function(page){
if(!this.state.brewCache[page]){
this.fetchBrews(page);
}
this.setState({
page : page
})
},
clearInvalidBrews : function(){
request.get('/homebrew/api/invalid')
.query({admin_key : this.props.admin_key})
.end((err, res)=>{
if(!confirm("This will remove " + res.body.count + " brews. Are you sure?")) return;
request.get('/homebrew/api/invalid')
.query({admin_key : this.props.admin_key, do_it : true})
.end((err, res)=>{
alert("Done!")
});
});
},
deleteBrew : function(brewId){
if(!confirm("Are you sure you want to delete '" + brewId + "'?")) return;
request.get('/homebrew/api/remove/' + brewId)
.query({admin_key : this.props.admin_key})
.end(function(err, res){
window.location.reload();
})
},
handlePageChange : function(dir){
this.changePageTo(this.state.page + dir);
},
renderPagnination : function(){
var outOf;
if(this.state.total){
outOf = this.state.page + ' / ' + Math.round(this.state.total/this.state.count);
}
return <div className='pagnination'>
<i className='fa fa-chevron-left' onClick={this.handlePageChange.bind(this, -1)}/>
{outOf}
<i className='fa fa-chevron-right' onClick={this.handlePageChange.bind(this, 1)}/>
</div>
},
renderBrews : function(){
var brews = this.state.brewCache[this.state.page] || _.times(this.state.count);
return _.map(brews, (brew)=>{
return <tr className={cx('brewRow', {'isEmpty' : brew.text == "false"})} key={brew.sharedId}>
<td><a href={'/homebrew/edit/' + brew.editId} target='_blank'>{brew.editId}</a></td>
<td><a href={'/homebrew/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></td>
<td>{Moment(brew.createdAt).fromNow()}</td>
<td>{Moment(brew.updatedAt).fromNow()}</td>
<td>{Moment(brew.lastViewed).fromNow()}</td>
<td>{brew.views}</td>
<td>
<div className='deleteButton' onClick={this.deleteBrew.bind(this, brew.editId)}>
<i className='fa fa-trash' />
</div>
</td>
</tr>
});
},
renderBrewTable : function(){
return <div className='brewTable'>
<table>
<thead>
<tr>
<th>Edit Id</th>
<th>Share Id</th>
<th>Created At</th>
<th>Last Updated</th>
<th>Last Viewed</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{this.renderBrews()}
</tbody>
</table>
</div>
},
render : function(){
var self = this;
return <div className='homebrewAdmin'>
<h2>
Homebrews - {this.state.total}
</h2>
{this.renderPagnination()}
{this.renderBrewTable()}
<button className='clearOldButton' onClick={this.clearInvalidBrews}>
Clear Old
</button>
</div>
}
});
module.exports = HomebrewAdmin;

View File

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

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,100 +1,45 @@
require('./brewRenderer.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 MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js');
const ErrorBar = require('./errorBar/errorBar.jsx');
var Markdown = require('marked');
//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;
var PAGE_HEIGHT = 1056 + 30;
const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50;
const BrewRenderer = createClass({
getDefaultProps : function() {
var BrewRenderer = React.createClass({
getDefaultProps: function() {
return {
text : '',
style : '',
renderer : 'legacy',
errors : []
text : ''
};
},
getInitialState : function() {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page/gm);
}
getInitialState: function() {
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>`
viewablePageNumber: 0,
height : 0
};
},
height : 0,
lastRender : <div></div>,
totalPages : 0,
height : 0,
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() {
componentDidMount: function() {
this.setState({
height : this.refs.main.parentNode.clientHeight,
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)
}));
this.setState({
viewablePageNumber : Math.floor(e.target.scrollTop / PAGE_HEIGHT)
});
},
//Implement later
scrollToPage : function(pageNumber){
},
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;
var viewIndex = this.state.viewablePageNumber;
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;
@@ -103,114 +48,45 @@ const BrewRenderer = createClass({
},
renderPageInfo : function(){
return <div className='pageInfo' ref='main'>
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div>;
return <div className='pageInfo'>
{this.state.viewablePageNumber + 1} / {this.totalPages}
</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>` }} />;
renderDummyPage : function(key){
return <div className='phb' key={key}>
<i className='fa fa-spinner fa-spin' />
</div>
},
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} />;
return <div className='phb' dangerouslySetInnerHTML={{__html:Markdown(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') {
var pages = this.props.text.split('\\page');
this.totalPages = pages.length;
return _.map(pages, (page, index)=>{
if(this.shouldRender(page, index)){
return this.renderPage(page, index);
} else {
}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 <div className='brewRenderer'
onScroll={this.handleScroll}
ref='main'
style={{height : this.state.height}}>
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>
);
<div className='pages'>
{this.renderPages()}
</div>
{this.renderPageInfo()}
</div>
}
});

View File

@@ -1,40 +1,28 @@
@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;
}
@import (less) './client/homebrew/phbStyle/phb.style.less';
.pane{
position : relative;
}
.brewRenderer{
overflow-y : scroll;
.pageInfo{
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
padding : 8px 10px;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
}
.pages{
margin : 30px 0px;
&>.phb{
margin-right : auto;
margin-bottom : 30px;
margin-left : auto;
box-shadow : 1px 4px 14px #000;
}
}
}

View File

@@ -1,75 +0,0 @@
require('./errorBar.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const ErrorBar = createClass({
getDefaultProps : function() {
return {
errors : []
};
},
hasOpenError : false,
hasCloseError : false,
hasMatchError : false,
renderErrors : function(){
this.hasOpenError = false;
this.hasCloseError = false;
this.hasMatchError = false;
const errors = _.map(this.props.errors, (err, idx)=>{
if(err.id == 'OPEN') this.hasOpenError = true;
if(err.id == 'CLOSE') this.hasCloseError = true;
if(err.id == 'MISMATCH') this.hasMatchError = true;
return <li key={idx}>
Line {err.line} : {err.text}, '{err.type}' tag
</li>;
});
return <ul>{errors}</ul>;
},
renderProtip : function(){
const msg = [];
if(this.hasOpenError){
msg.push(<div>
An unmatched opening tag means there's an opened tag that isn't closed. You need to close your tags, like this {'</div>'}. Make sure to match types!
</div>);
}
if(this.hasCloseError){
msg.push(<div>
An unmatched closing tag means you closed a tag without opening it. Either remove it, or check to where you think you opened it.
</div>);
}
if(this.hasMatchError){
msg.push(<div>
A type mismatch means you closed a tag, but the last open tag was a different type.
</div>);
}
return <div className='protips'>
<h4>Protips!</h4>
{msg}
</div>;
},
render : function(){
if(!this.props.errors.length) return null;
return <div className='errorBar'>
<i className='fas fa-exclamation-triangle' />
<h3> There are HTML errors in your markup</h3>
<small>If these aren't fixed your brew will not render properly when you print it to PDF or share it</small>
{this.renderErrors()}
<hr />
{this.renderProtip()}
</div>;
}
});
module.exports = ErrorBar;

View File

@@ -1,60 +0,0 @@
.errorBar{
position : absolute;
z-index : 10000;
box-sizing : border-box;
width : 100%;
margin-right : 13px;
padding : 20px;
padding-bottom : 10px;
padding-left : 100px;
background-color : @red;
color : white;
i{
position : absolute;
left : 30px;
opacity : 0.8;
font-size : 3em;
}
h3{
font-size : 1.1em;
font-weight : 800;
}
ul{
margin-top : 15px;
font-size : 0.8em;
list-style-position : inside;
list-style-type : disc;
li{
line-height : 1.6em;
}
}
hr{
box-sizing : border-box;
height : 2px;
width : 150%;
margin-top : 25px;
margin-bottom : 15px;
margin-left : -100px;
background-color : darken(@red, 8%);
border : none;
}
small{
font-size: 0.6em;
opacity: 0.7;
}
.protips{
margin-left : -80px;
font-size : 0.6em;
&>div{
margin-bottom : 10px;
line-height : 1.2em;
}
h4{
opacity : 0.8;
font-weight : 800;
line-height : 1.5em;
text-transform : uppercase;
}
}
}

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 +1,129 @@
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;
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
var CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
var Snippets = require('./snippets/snippets.js');
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){
var splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index);
};
var execute = function(val){
if(_.isFunction(val)) return val();
return val;
}
const Editor = createClass({
getDefaultProps : function() {
var Editor = React.createClass({
getDefaultProps: function() {
return {
brew : {
text : '',
style : ''
},
onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
renderer : 'legacy'
value : "",
onChange : function(){}
};
},
getInitialState : function() {
return {
view : 'text' //'text', 'style', 'meta'
};
cursorPosition : {
line : 0,
ch : 0
},
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);
componentDidMount: function() {
var paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= this.refs.snippetBar.clientHeight + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
},
componentWillUnmount : function() {
window.removeEventListener('resize', this.updateEditorSize);
handleTextChange : function(text){
this.props.onChange(text);
},
handleCursorActivty : function(curpos){
this.cursorPosition = curpos;
},
updateEditorSize : function() {
if(this.refs.codeEditor) {
let paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
}
},
handleSnippetClick : function(injectText){
var lines = this.props.value.split('\n');
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
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}`;
this.handleTextChange(lines.join('\n'));
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
},
//Called when there are changes to the editor's dimensions
update : function(){
this.refs.codeEditor?.updateSize();
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} />;
}
renderSnippetGroups : function(){
return _.map(Snippets, (snippetGroup)=>{
return <SnippetGroup
groupName={snippetGroup.groupName}
icon={snippetGroup.icon}
snippets={snippetGroup.snippets}
key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick}
/>
})
},
render : function(){
this.highlightCustomMarkdown();
return (
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 className='snippetBar' ref='snippetBar'>
{this.renderSnippetGroups()}
</div>
<CodeEditor
ref='codeEditor'
wrap={true}
language='gfm'
value={this.props.value}
onChange={this.handleTextChange}
onCursorActivity={this.handleCursorActivty} />
</div>
);
}
});
module.exports = Editor;
var SnippetGroup = React.createClass({
getDefaultProps: function() {
return {
groupName : '',
icon : 'fa-rocket',
snippets : [],
onSnippetClick : function(){},
};
},
handleSnippetClick : function(snippet){
this.props.onSnippetClick(execute(snippet.gen));
},
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={this.handleSnippetClick.bind(this, snippet)}>
<i className={'fa fa-fw ' + snippet.icon} />
{snippet.name}
</div>
})
},
render : function(){
return <div className='snippetGroup'>
<div className='text'>
<i className={'fa fa-fw ' + this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets()}
</div>
</div>
},
});

View File

@@ -1,45 +1,56 @@
.editor{
position : relative;
width : 100%;
.codeEditor{
height : 100%;
.pageLine{
background-color : fade(#333, 15%);
border-bottom : #333 solid 1px;
}
.columnSplit{
font-style : italic;
color : grey;
background-color : fade(#299, 15%);
border-bottom : #299 solid 1px;
}
.block{
color : purple;
font-weight : bold;
//font-style: italic;
}
.inline-block{
color : red;
font-weight : bold;
//font-style: italic;
}
}
.brewJump{
position: absolute;
background-color: @teal;
cursor: pointer;
width : 30px;
height : 30px;
display : flex;
align-items : center;
bottom : 20px;
right : 20px;
z-index: 1000000;
justify-content:center;
.tooltipLeft("Jump to brew page");
}
}
.editor{
position : relative;
width : 100%;
.snippetBar{
display : flex;
padding : 5px;
background-color : #ddd;
align-items : center;
.snippetGroup{
.animate(background-color);
margin : 0px 8px;
padding : 3px;
font-size : 13px;
border-radius : 5px;
&:hover, &.selected{
background-color : #999;
}
.text{
line-height : 20px;
.groupName{
margin-left : 6px;
font-size : 10px;
}
}
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown{
position : absolute;
visibility : hidden;
z-index : 1000;
padding : 5px;
background-color : #ddd;
.snippet{
.animate(background-color);
padding : 10px;
cursor : pointer;
font-size : 10px;
i{
margin-right: 8px;
font-size : 13px;
}
&:hover{
background-color : #999;
}
}
}
}
}
.codeEditor{
height : 100%;
}
}

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,87 +0,0 @@
.metadataEditor{
position : absolute;
z-index : 10000;
box-sizing : border-box;
width : 100%;
padding : 25px;
background-color : #999;
.field{
display : flex;
width : 100%;
margin-bottom : 10px;
&>label{
display : inline-block;
vertical-align : top;
width : 80px;
font-size : 0.7em;
font-weight : 800;
line-height : 1.8em;
text-transform : uppercase;
flex : 0 0 auto;
}
&>.value{
flex : 1 1 auto;
min-width : 200px;
}
}
.description.field textarea.value{
resize : none;
height : 5em;
font-family : 'Open Sans', sans-serif;
font-size : 0.8em;
}
.systems.field .value{
label{
vertical-align : middle;
margin-right : 15px;
cursor : pointer;
font-size : 0.7em;
font-weight : 800;
user-select : none;
white-space : nowrap;
display : inline-flex;
align-items : center;
}
input{
vertical-align : middle;
cursor : pointer;
margin : 3px;
}
}
.publish.field .value{
position : relative;
margin-bottom: 15px;
button{
width:100%;
}
button.publish{
.button(@blueLight);
}
button.unpublish{
.button(@silver);
}
small{
position : absolute;
bottom : -15px;
left : 0px;
font-size : 0.6em;
font-style : italic;
}
}
.delete.field .value{
button{
.button(@red);
}
}
.reddit.field .value{
button{
.button(@purple);
}
}
.authors.field .value{
font-size: 0.8em;
line-height : 1.5em;
}
}

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')
},
]
},
];

View File

@@ -1,72 +0,0 @@
const _ = require('lodash');
const getTOC = (pages)=>{
const add1 = (title, page)=>{
res.push({
title : title,
page : page + 1,
children : []
});
};
const add2 = (title, page)=>{
if(!_.last(res)) add1('', page);
_.last(res).children.push({
title : title,
page : page + 1,
children : []
});
};
const add3 = (title, page)=>{
if(!_.last(res)) add1('', page);
if(!_.last(_.last(res).children)) add2('', page);
_.last(_.last(res).children).children.push({
title : title,
page : page + 1,
children : []
});
};
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)=>{
r.push(`- **[${idx1 + 1} ${g1.title}](#p${g1.page})**`);
if(g1.children.length){
_.each(g1.children, (g2, idx2)=>{
r.push(` - [${idx1 + 1}.${idx2 + 1} ${g2.title}](#p${g2.page})`);
if(g2.children.length){
_.each(g2.children, (g3, idx3)=>{
r.push(` - [${idx1 + 1}.${idx2 + 1}.${idx3 + 1} ${g3.title}](#p${g3.page})`);
});
}
});
}
return r;
}, []).join('\n');
return `<div class='toc'>
##### Table Of Contents
${markdown}
</div>\n`;
};

View File

@@ -0,0 +1,42 @@
var _ = require('lodash');
module.exports = function(classname){
classname = classname || _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge'])
classname = classname.toLowerCase();
var hitDie = _.sample([4, 6, 8, 10, 12]);
var abilityList = ["Strength", "Dexerity", "Constitution", "Wisdom", "Charisma", "Intelligence"];
var skillList = ["Acrobatics ", "Animal Handling", "Arcana", "Athletics", "Deception", "History", "Insight", "Intimidation", "Investigation", "Medicine", "Nature", "Perception", "Performance", "Persuasion", "Religion", "Sleight of Hand", "Stealth", "Survival"];
return [
"## Class Features",
"As a " + classname + ", you gain the following class features",
"#### Hit Points",
"___",
"- **Hit Dice:** 1d" + hitDie + " per " + classname + " level",
"- **Hit Points at 1st Level:** " + hitDie + " + your Constituion modifier",
"- **Hit Points at Higher Levels:** 1d" + hitDie + " (or " + (hitDie/2 + 1) + ") + your Constituion 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

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

View File

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

View File

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

View File

@@ -0,0 +1,175 @@
var SpellGen = require('./spell.gen.js');
var ClassTableGen = require('./classtable.gen.js');
var MonsterBlockGen = require('./monsterblock.gen.js');
var ClassFeatureGen = require('./classfeature.gen.js');
var FullClassGen = require('./fullclass.gen.js');
module.exports = [
{
groupName : 'Editor',
icon : 'fa-pencil',
snippets : [
{
name : "Column Break",
icon : 'fa-columns',
gen : "```\n```\n\n"
},
{
name : "New Page",
icon : 'fa-file-text',
gen : "\\page\n\n"
},
{
name : "Vertical Spacing",
icon : 'fa-arrows-v',
gen : "<div style='margin-top:140px'></div>\n\n"
},
{
name : "Image",
icon : 'fa-image',
gen : [
"<img ",
" src='https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg' ",
" style='width:325px' />",
"Credit: Kyounghwan Kim"
].join('\n')
},
{
name : "Background Image",
icon : 'fa-tree',
gen : [
"<img ",
" src='http://i.imgur.com/hMna6G0.png' ",
" style='position:absolute; top:50px; right:30px; width:280px' />"
].join('\n')
},
{
name : "Page Number",
icon : 'fa-bookmark',
gen : "<div class='pageNumber'>1</div>\n<div class='footnote'>PART 1 | FANCINESS</div>\n\n"
},
]
},
/************************* PHB ********************/
{
groupName : 'PHB',
icon : 'fa-book',
snippets : [
{
name : 'Spell',
icon : 'fa-magic',
gen : SpellGen,
},
{
name : 'Class Feature',
icon : 'fa-trophy',
gen : ClassFeatureGen,
},
{
name : 'Note',
icon : 'fa-sticky-note',
gen : function(){
return [
"> ##### Time to Drop Knowledge",
"> Use notes to point out some interesting information. ",
"> ",
"> **Tables and lists** both work within a note."
].join('\n');
},
},
{
name : 'Monster Stat Block',
icon : 'fa-bug',
gen : MonsterBlockGen.half,
},
{
name : 'Wide Monster Stat Block',
icon : 'fa-paw',
gen : MonsterBlockGen.full,
}
]
},
/********************* TABLES *********************/
{
groupName : 'Tables',
icon : 'fa-table',
snippets : [
{
name : "Class Table",
icon : 'fa-table',
gen : ClassTableGen.full,
},
{
name : "Half Class Table",
icon : 'fa-list-alt',
gen : ClassTableGen.half,
},
{
name : 'Table',
icon : 'fa-th-list',
gen : function(){
return [
"##### Cookie Tastiness",
"| Tastiness | Cookie Type |",
"|:----:|:-------------|",
"| -5 | Raisin |",
"| 8th | Chocolate Chip |",
"| 11th | 2 or lower |",
"| 14th | 3 or lower |",
"| 17th | 4 or lower |\n\n",
].join('\n');
},
}
]
},
/**************** PRINT *************/
{
groupName : 'Print',
icon : 'fa-print',
snippets : [
{
name : "A4 PageSize",
icon : 'fa-file-o',
gen : ['<style>',
' .phb{',
' width : 210mm;',
' height : 296.8mm;',
' }',
'</style>'
].join('\n')
},
{
name : "Ink Friendly",
icon : 'fa-tint',
gen : ['<style>',
' .phb{ background : white;}',
' .phb img{ display : none;}',
' .phb hr+blockquote{background : white;}',
'</style>',
''
].join('\n')
},
]
},
]

View File

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

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,56 @@
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');
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
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');
var CreateRouter = require('pico-router').createRouter;
const Homebrew = createClass({
getDefaultProps : function() {
var HomePage = require('./pages/homePage/homePage.jsx');
var EditPage = require('./pages/editPage/editPage.jsx');
var SharePage = require('./pages/sharePage/sharePage.jsx');
var NewPage = require('./pages/newPage/newPage.jsx');
var Router;
var Homebrew = React.createClass({
getDefaultProps: function() {
return {
url : '',
welcomeText : '',
changelog : '',
version : '0.0.0',
account : null,
enable_v3 : false,
brew : {
title : '',
text : '',
shareId : null,
editId : null,
url : "",
welcomeText : "",
changelog : "",
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;
componentWillMount: function() {
Router = CreateRouter({
'/homebrew/edit/:id' : (args) => {
return <EditPage id={args.id} brew={this.props.brew} />
},
'/homebrew/share/:id' : (args) => {
return <SharePage id={args.id} brew={this.props.brew} />
},
'/homebrew/changelog' : (args) => {
return <SharePage brew={{title : 'Changelog', text : this.props.changelog}} />
},
'/homebrew/new' : (args) => {
return <NewPage />
},
'/homebrew*' : <HomePage welcomeText={this.props.welcomeText} />,
});
},
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>
render : function(){
return(
<div className='homebrew'>
<Router initialUrl={this.props.url}/>
</div>
);
}
});
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} />;
// },

View File

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

View File

@@ -1,34 +0,0 @@
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;

View File

@@ -1,15 +1,15 @@
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var Nav = require('naturalcrit/nav/nav.jsx');
const MAX_TITLE_LENGTH = 50;
const EditTitle = createClass({
getDefaultProps : function() {
var EditTitle = React.createClass({
getDefaultProps: function() {
return {
title : '',
title : '',
onChange : function(){}
};
},
@@ -22,12 +22,12 @@ const EditTitle = createClass({
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 })}>
<div className={cx('charCount', {'max' : this.props.title.length >= MAX_TITLE_LENGTH})}>
{this.props.title.length}/{MAX_TITLE_LENGTH}
</div>
</Nav.item>;
</Nav.item>
},
});
module.exports = EditTitle;
module.exports = EditTitle;

View File

@@ -1,13 +1,8 @@
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
var React = require('react');
var 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')}`} >
return <Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
report issue
</Nav.item>;
};
</Nav.item>
};

View File

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

View File

@@ -1,141 +1,58 @@
@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);
font-family : CodeBold;
font-size : 12px;
color : white;
div{
margin-top : 2px;
margin-bottom : -2px;
}
&:hover{
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;
color : white;
text-align : center;
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;
}
}
.recent.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;
box-sizing : border-box;
padding : 13px 5px;
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;
}
.time{
position : absolute;
right : 2px;
bottom : 2px;
font-size : 0.7em;
color : #888;
}
}
}
}
.warning.navItem{
position : relative;
background-color : @orange;
color : white;
&:hover>.dropdown{
visibility : visible;
}
.dropdown{
position : absolute;
display : block;
top : 28px;
left : 0px;
visibility : hidden;
z-index : 10000;
box-sizing : border-box;
width : 100%;
padding : 13px 5px;
background-color : #333;
text-align : center;
}
}
}
.homebrew nav{
.homebrewLogo{
.animate(color);
font-family : CodeBold;
font-size : 12px;
color : white;
div{
margin-top : 2px;
margin-bottom : -2px;
}
&:hover{
color : @blue;
}
}
.editTitle.navItem{
padding : 2px 12px;
input{
margin : 0;
padding : 2px;
width : 250px;
background-color : #444;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
border : 1px solid @blue;
outline : none;
}
.charCount{
display : inline-block;
vertical-align : bottom;
margin-left : 8px;
text-align : right;
color : #666;
&.max{
color : @red;
}
}
}
.brewTitle.navItem{
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
text-transform: initial;
}
.patreon.navItem{
i{
.animate(color);
&:hover{
color : @red;
}
}
}
}

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,13 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
var React = require('react');
var Nav = require('naturalcrit/nav/nav.jsx');
module.exports = function(props){
return <Nav.item
className='patreon'
newTab={true}
href='https://www.patreon.com/NaturalCrit'
href='https://www.patreon.com/stolksdorf'
color='green'
icon='fas fa-heart'>
icon='fa-heart'>
help out
</Nav.item>;
};
</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');
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='far fa-file-pdf'>
get PDF
</Nav.item>;
};
return <Nav.item newTab={true} href={'/homebrew/print/' + props.shareId +'?dialog=true'} color='purple' icon='fa-print'>
print
</Nav.item>
};

View File

@@ -1,186 +0,0 @@
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}
/>;
}
};

View File

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

View File

@@ -1,429 +1,171 @@
/* 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');
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var request = require("superagent");
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
var Nav = require('naturalcrit/nav/nav.jsx');
var 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;
var EditTitle = require('../../navbar/editTitle.navitem.jsx');
var ReportIssue = require('../../navbar/issue.navitem.jsx');
var PrintLink = require('../../navbar/print.navitem.jsx');
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');
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
var Editor = require('../../editor/editor.jsx');
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const googleDriveActive = require('../../googleDrive.png');
const googleDriveInactive = require('../../googleDriveMono.png');
const SAVE_TIMEOUT = 3000;
const EditPage = createClass({
getDefaultProps : function() {
var EditPage = React.createClass({
getDefaultProps: function() {
return {
id : null,
brew : {
text : '',
style : '',
shareId : null,
editId : null,
title : '',
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
gDrive : false,
trashed : false,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : [],
renderer : 'legacy'
}
};
},
getInitialState : function() {
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 : ''
title : this.props.brew.title,
text: this.props.brew.text,
isSaving : false,
isPending : false,
errors : null,
lastUpdated : this.props.brew.updatedAt
};
},
savedBrew : null,
componentDidMount : function(){
this.setState({
url : window.location.href
});
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
this.trySave();
componentDidMount: function(){
this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
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();
}
},
componentWillUnmount: function() {
window.onbeforeunload = function(){};
},
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 }),
handleTitleChange : function(title){
this.setState({
title : title,
isPending : true
}), ()=>this.trySave());
});
(this.hasChanges() ? this.debounceSave() : this.debounceSave.cancel());
},
handleMetaChange : function(metadata){
this.setState((prevState)=>({
brew : _.merge({}, prevState.brew, metadata),
isPending : true,
}), ()=>this.trySave());
handleTextChange : function(text){
this.setState({
text : text,
isPending : true
});
(this.hasChanges() ? this.debounceSave() : this.debounceSave.cancel());
},
handleDelete : function(){
if(!confirm("are you sure you want to delete this brew?")) return;
if(!confirm("are you REALLY sure? You will not be able to recover it")) return;
request.get('/homebrew/api/remove/' + this.props.brew.editId)
.send()
.end(function(err, res){
window.location.href = '/homebrew';
});
},
hasChanges : function(){
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();
if(this.savedBrew){
if(this.state.text !== this.savedBrew.text) return true;
if(this.state.title !== this.savedBrew.title) return true;
}else{
if(this.state.text !== this.props.brew.text) return true;
if(this.state.title !== this.props.brew.title) return true;
}
return false;
},
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
save : function(){
this.debounceSave.cancel();
this.setState({
alertTrashedGoogleBrew : false,
alertLoginToTransfer : false,
confirmGoogleTransfer : false
isSaving : true
});
},
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');
});
request
.put('/homebrew/api/update/' + this.props.brew.editId)
.send({
text : this.state.text,
title : this.state.title
})
.end((err, res) => {
this.savedBrew = res.body;
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>;
this.setState({
isPending : false,
isSaving : false,
lastUpdated : res.body.updatedAt
})
})
},
renderSaveButton : function(){
if(this.state.errors){
let errMsg = '';
try {
errMsg += `${this.state.errors.toString()}\n\n`;
errMsg += `\`\`\`\n${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
} catch (e){}
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>;
return <Nav.item className='save' icon="fa-spinner fa-spin">saving...</Nav.item>
}
if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item>;
return <Nav.item className='save saved'>saved.</Nav.item>
}
if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>
}
},
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>
<EditTitle title={this.state.title} onChange={this.handleTitleChange} />
</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'>
<Nav.item newTab={true} href={'/homebrew/share/' + this.props.brew.shareId} color='teal' icon='fa-share-alt'>
Share
</Nav.item>
<PrintLink shareId={this.processShareId()} />
<RecentNavItem brew={this.state.brew} storageKey='edit' />
<Account />
<PrintLink shareId={this.props.brew.shareId} />
<Nav.item color='red' icon='fa-trash' onClick={this.handleDelete}>
Delete
</Nav.item>
</Nav.section>
</Navbar>;
</Navbar>
},
render : function(){
return <div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
return <div className='editPage page'>
{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} />
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<BrewRenderer text={this.state.text} />
</SplitPane>
</div>
</div>;
</div>
}
});

View File

@@ -1,99 +1,12 @@
@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;
}
}
}
}
.editPage{
.navItem.save{
width : 75px;
text-align: center;
&.saved{
color : #666;
cursor : initial;
}
}
}

View File

@@ -1,48 +0,0 @@
require('./errorPage.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 PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const ErrorPage = createClass({
getDefaultProps : function() {
return {
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'>
<Navbar ver={this.props.ver}>
<Nav.section>
<Nav.item className='errorTitle'>
Crit Fail!
</Nav.item>
</Nav.section>
<Nav.section>
<PatreonNavItem />
<IssueNavItem />
<RecentNavItem />
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer text={this.text} />
</div>
</div>;
}
});
module.exports = ErrorPage;

View File

@@ -1,5 +0,0 @@
.errorPage{
.errorTitle{
background-color: @orange;
}
}

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,98 +1,69 @@
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');
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 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');
var Nav = require('naturalcrit/nav/nav.jsx');
var Navbar = require('../../navbar/navbar.jsx');
var PatreonNavItem = require('../../navbar/patreon.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
var Editor = require('../../editor/editor.jsx');
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const HomePage = createClass({
getDefaultProps : function() {
var HomePage = React.createClass({
getDefaultProps: function() {
return {
brew : {
text : '',
},
ver : '0.0.0'
welcomeText : ""
};
},
getInitialState : function() {
getInitialState: function() {
return {
brew : this.props.brew,
welcomeText : this.props.brew.text
text: this.props.welcomeText
};
},
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 }
text : text
});
},
renderNavbar : function(){
return <Navbar ver={this.props.ver}>
return <Navbar>
<Nav.section>
<NewBrewItem />
<IssueNavItem />
<Nav.item newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
<PatreonNavItem />
<Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
report issue
</Nav.item>
<Nav.item newTab={true} href='/homebrew/changelog' color='purple' icon='fa-file-text-o'>
Changelog
</Nav.item>
<RecentNavItem />
<AccountNavItem />
<Nav.item href='/homebrew/new' color='green' icon='fa-external-link'>
New Brew
</Nav.item>
</Nav.section>
</Navbar>;
</Navbar>
},
render : function(){
return <div className='homePage sitePage'>
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
return <div className='homePage page'>
{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} />
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<BrewRenderer text={this.state.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 href='/homebrew/new' className='floatingNewButton'>
Create your own <i className='fa fa-magic' />
</a>
</div>;
</div>
}
});

View File

@@ -1,43 +1,23 @@
.homePage{
position : relative;
a.floatingNewButton{
.animate(background-color);
position : absolute;
display : block;
right : 70px;
bottom : 70px;
z-index : 100;
z-index : 5001;
padding : 1em;
background-color : @orange;
font-size : 1.5em;
color : white;
text-decoration : none;
box-shadow : 3px 3px 15px black;
&:hover{
background-color : darken(@orange, 20%);
}
}
.floatingSaveButton{
.animateAll();
position : absolute;
display : block;
right : 200px;
bottom : 90px;
z-index : 100;
z-index : 5000;
padding : 0.8em;
cursor : pointer;
background-color : @blue;
font-size : 0.8em;
color : white;
text-decoration : none;
box-shadow : 3px 3px 15px black;
&:hover{
background-color : darken(@blue, 20%);
}
&.show{
right : 350px;
}
}
.homePage{
position : relative;
a.floatingNewButton{
.animate(background-color);
position : absolute;
display : block;
right : 70px;
bottom : 70px;
z-index : 100;
padding : 1em;
background-color : @orange;
font-size : 1.5em;
color : white;
text-decoration : none;
box-shadow : 3px 3px 15px black;
&:hover{
background-color : darken(@orange, 20%);
}
}
}

View File

@@ -1,100 +1,107 @@
# The Homebrewery
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
### Homebrew D&D made easy
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
**Try it! **Simply edit the text on the left and watch it *update live* on the right.
### Editing and Sharing
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
Anyone with the *share url* will be able to access a read-only version of your homebrew.
## Helping out
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/stolksdorf) to help me keep the servers running.
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
>##### PDF Exporting
> PDF Printing works best in Chrome. If you are having quality/consistency issues, try using Chrome to print instead.
>
> After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up.
> * Set the **Destination** to "Save as PDF"
> * Set **Paper Size** to "Letter"
> * If you are printing on A4 paper, make sure to have the "A4 page size snippet" in your brew
> * In **Options** make sure "Background Images" is selected.
> * Hit print and enjoy! You're done!
>
> If you want to save ink or have a monochrome printer, add the **Ink Friendly** snippet to your brew before you print
```
```
## Big things coming in v3.0.0
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. All brews made previous to the release of v3.0.0 will still render normally.
## New Things All The Time!
What's new in the latest update? Check out the full changelog [here](/changelog)
### Bugs, Issues, Suggestions?
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let me know!.
### Legal Junk
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). Which means you are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself.
If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
### More Resources
If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](https://www.reddit.com/r/UnearthedArcana/) and their list of useful resources [here](https://www.reddit.com/r/UnearthedArcana/comments/3uwxx9/resources_open_to_the_community/).
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
<div class='pageNumber'>1</div>
<div class='footnote'>PART 1 | FANCINESS</div>
\page
# Appendix
### Not quite Markdown
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
___
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
```
```
### Images
Images must be hosted online somewhere, like imgur. You use the address to that image to reference it in your brew. Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. I have added both a inflow image snippet and a background image snippet to give you exmaples of how to do it.
### Crediting Me
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
<div class='pageNumber'>2</div>
<div class='footnote'>PART 2 | BORING STUFF</div>
# The Homebrewery
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
### Homebrew D&D made easy
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
**Try it! **Simply edit the text on the left and watch it *update live* on the right.
#### Features
* Monster Stat Blocks
* Full class tables
* Notes and Tables
* Images
* Page numbering and footers
* Vertical spacing, column breaks, and multiple pages
### Editing and Sharing
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
Anyone with the *share url* will be able to access a read-only version of your homebrew.
## Helping out
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/stolksdorf) to help me keep the servers running.
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
### Bugs, Issues, Suggestions?
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://github.com/stolksdorf/NaturalCrit/issues/new) and let me know!.
```
```
## New Things in v2.0.0!
What's new in the latest update? Check out the full changelog [here](/homebrew/changelog)
* **A whole new look** The site has been re-built from the ground up!
* **Better editor and Split Pane** Syntax highlighting will make writing your brews even easier, and now you can customize how large your editor is.
* **More reliable rendering** Lots of work has been put into making the rendering more reliable, not just for web, but also for PDFs
* **PDF Printing on Chrome** You don't need to use Chrome Canary anymore!
* ** Performance Improvements** The site should load faster, save faster, and render large brews *much* faster.
* **Patreon page** If you like this tool and want to show some thanks you can [head here](https://www.patreon.com/stolksdorf).
>##### PDF Exporting
> After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up
> * Set the **Destination** to "Save as PDF"
> * Set **Paper Size** to "Letter"
> * If you are printing on A4 paper, make sure to have the "A4 page size snippet" in your brew
> * In **Options** make sure "Background Images" is selected.
> * Hit print and enjoy! You're done!
>
> If you want to save ink or have a monochrome printer, add the **Ink Friendly** snippet to your brew before you print
<img src='http://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
<div class='pageNumber'>1</div>
<div class='footnote'>PART 1 | FANCINESS</div>
\page
# Appendix
### Not quite Markdown
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
___
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
### Images
Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. I have added both a inflow image snippet and a background image snippet to give you exmaples of how to do it.
```
```
### Legal Junk
You are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself. If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
### Crediting Me
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
<div class='pageNumber'>2</div>
<div class='footnote'>PART 2 | BORING STUFF</div>

View File

@@ -1,169 +1,76 @@
/*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');
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var 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';
var Nav = require('naturalcrit/nav/nav.jsx');
var Navbar = require('../../navbar/navbar.jsx');
var EditTitle = require('../../navbar/editTitle.navitem.jsx');
const NewPage = createClass({
getDefaultProps : function() {
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
var Editor = require('../../editor/editor.jsx');
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const KEY = 'naturalCrit-homebrew-new';
var NewPage = React.createClass({
getInitialState: function() {
return {
brew : {
text : '',
style : undefined,
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
gDrive : false,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
}
title : 'My Awesome Brew v99',
text: '',
isSaving : false
};
},
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() {
var storage = localStorage.getItem(KEY);
if(storage){
this.setState({
text : storage
})
}
window.onbeforeunload = (e)=>{
if(this.state.text == '') return;
return "Your homebrew isn't saved. Are you sure you want to leave?";
};
},
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();
}
componentWillUnmount: function() {
window.onbeforeunload = function(){};
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleTitleChange : function(title){
this.setState({
title : title
});
},
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);
this.setState({
text : text
});
localStorage.setItem(KEY, 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(){
handleSave : 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)
request.post('/homebrew/api')
.send({
title : this.state.title,
text : this.state.text
})
.end((err, res)=>{
if(err){
this.setState({
isSaving : false
@@ -171,71 +78,52 @@ const NewPage = createClass({
return;
}
window.onbeforeunload = function(){};
brew = res.body;
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
window.location = `/edit/${brew.editId}`;
});
}
var brew = res.body;
localStorage.removeItem(KEY);
window.location = '/homebrew/edit/' + brew.editId;
})
},
renderSaveButton : function(){
if(this.state.isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='saveButton'>
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='saveButton' onClick={this.save}>
</Nav.item>
}else{
return <Nav.item icon='fa-save' className='saveButton' onClick={this.handleSave}>
save
</Nav.item>;
</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>
<EditTitle title={this.state.title} onChange={this.handleTitleChange} />
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
{this.renderLocalPrintButton()}
<IssueNavItem />
<RecentNavItem />
<AccountNavItem />
<Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
report issue
</Nav.item>
</Nav.section>
</Navbar>;
</Navbar>
},
render : function(){
return <div className='newPage sitePage'>
return <div className='newPage page'>
{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}/>
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<BrewRenderer text={this.state.text} />
</SplitPane>
</div>
</div>;
</div>
}
});

View File

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

View File

@@ -1,70 +0,0 @@
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 PrintPage = createClass({
getDefaultProps : function() {
return {
query : {},
brew : {
text : '',
style : '',
renderer : 'legacy'
}
};
},
getInitialState : function() {
return {
brewText : this.props.brew.text
};
},
componentDidMount : function() {
if(this.props.query.local){
this.setState((prevState, prevProps)=>({
brewText : localStorage.getItem(prevProps.query.local)
}));
}
if(this.props.query.dialog) window.print();
},
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} />;
});
}
},
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>` }} />
{this.renderPages()}
</div>;
}
});
module.exports = PrintPage;

View File

@@ -1,3 +0,0 @@
.printPage{
}

View File

@@ -1,111 +1,47 @@
require('./sharePage.less');
const React = require('react');
const createClass = require('create-react-class');
const { Meta } = require('vitreum/headtags');
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 PrintLink = require('../../navbar/print.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
var Nav = require('naturalcrit/nav/nav.jsx');
var Navbar = require('../../navbar/navbar.jsx');
var PrintLink = require('../../navbar/print.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const SharePage = createClass({
getDefaultProps : function() {
var SharePage = React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
text : '',
style : '',
shareId : null,
title : '',
text : '',
shareId : null,
createdAt : null,
updatedAt : null,
views : 0,
renderer : ''
views : 0
}
};
},
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' />
return <div className='sharePage page'>
<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 />
<PrintLink shareId={this.props.brew.shareId} />
<Nav.item href={'/homebrew/source/' + this.props.brew.shareId} color='teal' icon='fa-code'>
source
</Nav.item>
</Nav.section>
</Navbar>
<div className='content'>
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} />
<BrewRenderer text={this.props.brew.text} />
</div>
</div>;
</div>
}
});

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;
}
}
}
}
.sharePage{
}

View File

@@ -1,132 +0,0 @@
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() {
return {
brew : {
title : '',
description : '',
authors : []
}
};
},
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(){
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>;
},
render : function(){
const brew = this.props.brew;
return <div className='brewItem'>
<h2>{brew.title}</h2>
<p className='description' >{brew.description}</p>
<hr />
<div className='info'>
<span>
<i className='fas fa-user' /> {brew.authors.join(', ')}
</span>
<span>
<i className='fas fa-eye' /> {brew.views}
</span>
<span>
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
</span>
{this.renderGoogleDriveIcon()}
</div>
<div className='links'>
{this.renderShareLink()}
{this.renderEditLink()}
{this.renderDownloadLink()}
{this.renderDeleteBrewLink()}
</div>
</div>;
}
});
module.exports = BrewItem;

View File

@@ -1,72 +0,0 @@
.brewItem{
position : relative;
display : inline-block;
vertical-align : top;
box-sizing : border-box;
box-sizing : border-box;
overflow : hidden;
width : 48%;
min-height : 105px;
margin-right : 15px;
margin-bottom : 15px;
padding : 5px 15px 5px 8px;
padding-right : 15px;
border : 1px solid #c9ad6a;
border-radius : 5px;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
h4{
margin-bottom : 5px;
font-size : 2.2em;
}
.info{
position: absolute;
bottom: 0px;
margin-bottom: 4px;
font-family : ScalySans;
font-size : 1.2em;
&>span{
margin-right : 12px;
}
}
&:hover{
.links{
opacity : 1;
}
}
&:nth-child(2n + 1){
margin-right : 0px;
}
.links{
.animate(opacity);
position : absolute;
top : 0px;
right : 0px;
height : 100%;
width : 2em;
opacity : 0;
background-color : fade(black, 60%);
text-align : center;
a{
.animate(opacity);
display : block;
margin : 8px 0px;
opacity : 0.6;
font-size : 1.3em;
color : white;
&:hover{
opacity : 1;
}
i{
cursor : pointer;
}
}
}
.googleDriveIcon {
height : 20px;
padding : 0px;
margin : -5px;
}
}

View File

@@ -1,158 +0,0 @@
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 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 BREWS = _.times(25, ()=>{ return brew;});
const UserPage = createClass({
getDefaultProps : function() {
return {
username : '',
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);
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');
});
},
render : function(){
const brews = this.getSortedBrews();
return <div className='userPage sitePage'>
<link href='/themes/5ePhbLegacy.style.css' rel='stylesheet'/>
<Navbar>
<Nav.section>
<NewBrew />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>
<div className='content V3'>
<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>
</div>
</div>
</div>;
}
});
module.exports = UserPage;

View File

@@ -1,73 +0,0 @@
.noColumns(){
column-count : auto;
column-fill : auto;
column-gap : auto;
column-width : auto;
-webkit-column-count : auto;
-moz-column-count : auto;
-webkit-column-width : auto;
-moz-column-width : auto;
-webkit-column-gap : auto;
-moz-column-gap : auto;
}
.userPage{
.content{
overflow-y : scroll;
.phb{
.noColumns();
height : auto;
min-height : 279.4mm;
margin : 20px auto;
&::after{
display : none;
}
.noBrews{
margin : 10px 0px;
font-size : 1.3em;
font-style : italic;
}
}
}
.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;
}
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
@import (less) './themes/fonts/5e legacy/fonts.less';
@import (less) './themes/assets/assets.less';
@import (less) './themes/phb.depricated.less';
@import (less) 'shared/naturalcrit/styles/reset.less';
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
@import (less) './client/homebrew/phbStyle/phb.assets.less';
//Colors
@background : #EEE5CE;
@noteGreen : #e0e5c1;
@@ -9,12 +10,6 @@
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
}
*{
-webkit-print-color-adjust : exact;
}
.useSansSerif(){
font-family : ScalySans;
em{
@@ -27,21 +22,20 @@ body {
letter-spacing : -0.02em;
}
}
.useColumns(@multiplier : 1){
.useColumns(){
column-count : 2;
column-fill : auto;
column-gap : 1cm;
column-width : 8cm * @multiplier;
column-width : 8cm;
-webkit-column-count : 2;
-moz-column-count : 2;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
-webkit-column-width : 8cm;
-moz-column-width : 8cm;
-webkit-column-gap : 1cm;
-moz-column-gap : 1cm;
}
.phb{
.useColumns();
counter-increment : phb-page-numbers;
position : relative;
z-index : 15;
box-sizing : border-box;
@@ -64,26 +58,23 @@ body {
padding-bottom : 0.8em;
line-height : 1.3em;
&+p{
margin-top : -0.8em;
margin-top : -0.8em;
text-indent : 1em;
}
}
ul{
margin-bottom : 0.8em;
padding-left : 1.4em;
line-height : 1.3em;
list-style-position : outside;
list-style-type : disc;
padding-left: 1.4em;
}
ol{
margin-bottom : 0.8em;
padding-left : 1.4em;
line-height : 1.3em;
list-style-position : outside;
list-style-type : decimal;
}
//Indents after p or lists
p+p, ul+p, ol+p{
text-indent : 1em;
padding-left: 1.4em;
}
img{
z-index : -1;
@@ -154,7 +145,6 @@ body {
margin-bottom : 1em;
font-size : 10pt;
thead{
display: table-row-group;
font-weight : 800;
th{
vertical-align : bottom;
@@ -179,23 +169,23 @@ body {
// *****************************/
blockquote{
.useSansSerif();
box-sizing : border-box;
margin-bottom : 1em;
padding : 5px 10px;
background-color : @noteGreen;
border-style : solid;
border-width : 11px;
border-image : @noteBorderImage 11;
border-image-outset : 9px 0px;
box-shadow : 1px 4px 14px #888;
box-sizing : border-box;
margin-bottom : 1em;
padding : 5px 10px;
background-color : @noteGreen;
border-style: solid;
border-width: 11px;
border-image: @noteBorderImage 11;
border-image-outset: 9px 0px;
box-shadow : 1px 4px 14px #888;
p, ul{
font-size : 0.352cm;
line-height : 1.1em;
}
}
//If a note starts a column, give it space at the top to render border
pre+blockquote, h2+blockquote, h3+blockquote, h4+blockquote, h5+blockquote {
margin-top : 13px;
pre+blockquote{
margin-top: 11px;
}
//*****************************
// * MONSTER STAT BLOCK
@@ -206,7 +196,7 @@ body {
background-color : @monsterStatBackground;
border-style : solid;
border-width : 10px;
border-image : @monsterBorderImageLegacy 10;
border-image : @monsterBorderImage 10;
h2{
margin-top : -8px;
margin-bottom : 0px;
@@ -219,21 +209,21 @@ body {
font-weight : 400;
border-bottom : 1px solid @headerText;
}
hr+ul{
color : @headerText;
}
ul{
.useSansSerif();
padding-left : 1em;
font-size : 0.352cm;
padding-left : 1em;
font-size : 0.352cm;
color : @headerText;
text-indent : -1em;
list-style-type : none;
}
// Monster Ability table
hr+table{
margin : 0;
column-span : 1;
background-color : transparent;
border-style : none;
border-image : none;
border-style : none;
-webkit-column-span : 1;
tbody{
tr:nth-child(odd), tr:nth-child(even){
@@ -261,7 +251,29 @@ body {
}
//Full Width
hr+hr+blockquote{
.useColumns(0.96);
.useColumns();
}
//*****************************
// * CLASS TABLE
// *****************************/
hr+table{
margin-top : -5px;
margin-bottom : 50px;
padding-top : 10px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 37px 17px;
border-image-repeat : round;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
}
h5+hr+table{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * FOOTER
@@ -271,7 +283,6 @@ body {
position : absolute;
bottom : 0px;
left : 0px;
z-index : 100;
height : 50px;
width : 100%;
background-image : @footerAccentImage;
@@ -297,15 +308,11 @@ body {
font-size : 0.9em;
color : #c9ad6a;
text-align : center;
&.auto::after {
content : counter(phb-page-numbers);
}
}
.footnote{
position : absolute;
right : 80px;
bottom : 32px;
z-index : 150;
width : 200px;
font-size : 0.8em;
color : #c9ad6a;
@@ -325,19 +332,25 @@ body {
text-indent : -1em;
list-style-type : none;
}
//Double hr for full width elements
hr+hr+blockquote{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//Column Break
pre, code{
pre{
visibility : hidden;
-webkit-column-break-after : always;
break-after : always;
-moz-column-break-after : always;
}
//Avoid breaking up
p,blockquote,table{
p,ul,blockquote,table{
z-index : 15;
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
column-break-inside : avoid;
overflow: hidden; /* Firefox fix */
}
//Better spacing for spell blocks
h4+p+hr+ul{
@@ -352,118 +365,12 @@ body {
margin-bottom : 0px;
margin-left : 1.5em;
}
li{
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
}
}
//*****************************
// * SPELL LIST
// * PRINT
// *****************************/
.phb .spellList{
.useSansSerif();
column-count : 4;
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
ul+h5{
margin-top : 15px;
.phb.print{
blockquote{
box-shadow : none;
}
p, ul{
font-size : 0.352cm;
line-height : 1.3em;
}
ul{
margin-bottom : 0.5em;
padding-left : 1em;
text-indent : -1em;
list-style-type : none;
-webkit-column-break-inside : auto;
page-break-inside : auto;
break-inside : auto;
}
}
//*****************************
// * WIDE
// *****************************/
.phb .wide{
column-span : all;
-webkit-column-span : all;
-moz-column-span : all;
}
//*****************************
// * CLASS TABLE
// *****************************/
.phb .classTable{
margin-top : 25px;
margin-bottom : 40px;
border-collapse : separate;
background-color : white;
border : initial;
border-style : solid;
border-image-outset : 25px 17px;
border-image-repeat : stretch;
border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage;
border-image-width : 47px;
h5{
margin-bottom : 10px;
}
}
//************************************
// * DESCRIPTIVE TEXT BOX
// ************************************/
.phb .descriptive{
display : block-inline;
margin-bottom : 1em;
background-color : #faf7ea;
font-family : ScalySans;
border-style : solid;
border-width : 7px;
border-image : @descriptiveBoxImage 12 stretch;
border-image-outset : 4px;
box-shadow : 0px 0px 6px #faf7ea;
p{
display : block;
padding-bottom : 0px;
line-height : 1.5em;
}
p + p {
padding-top : .8em;
}
em {
font-family : ScalySans;
font-style : italic;
}
strong {
font-family : ScalySans;
font-weight : 800;
letter-spacing : -0.02em;
}
}
.phb pre+.descriptive{
margin-top : 8px;
}
//*****************************
// * TABLE OF CONTENTS
// *****************************/
.phb .toc{
-webkit-column-break-inside : avoid;
page-break-inside : avoid;
break-inside : avoid;
a{
color : black;
text-decoration : none;
&:hover{
text-decoration : underline;
}
}
ul{
padding-left : 0;
list-style-type : none;
}
&>ul>li{
margin-bottom : 10px;
}
}
}

View File

Before

Width:  |  Height:  |  Size: 864 B

After

Width:  |  Height:  |  Size: 864 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

86
client/main/main.jsx Normal file
View File

@@ -0,0 +1,86 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var Router = require('pico-router');
var NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
var HomebrewIcon = require('naturalcrit/svg/homebrew.svg.jsx');
var Main = React.createClass({
getDefaultProps: function() {
return {
tools : [
{
id : 'homebrew',
path : '/homebrew',
name : 'The Homebrewery',
icon : <HomebrewIcon />,
desc : 'Make authentic-looking 5e homebrews using Markdown',
show : true,
beta : false
},
{
id : 'homebrew2',
path : '/homebrew',
name : 'The Homebrewery',
icon : <HomebrewIcon />,
desc : 'Make authentic-looking 5e homebrews using Markdown',
show : false,
beta : true
},
{
id : 'homebrewfg2',
path : '/homebrew',
name : 'The Homebrewery',
icon : <HomebrewIcon />,
desc : 'Make authentic-looking 5e homebrews using Markdown',
show : false,
beta : false
}
]
};
},
renderTool : function(tool){
if(!tool.show) return null;
return <a href={tool.path} className={cx('tool', tool.id, {beta : tool.beta})} key={tool.id}>
<div className='content'>
{tool.icon}
<h2>{tool.name}</h2>
<p>{tool.desc}</p>
</div>
</a>;
},
renderTools : function(){
return _.map(this.props.tools, (tool)=>{
return this.renderTool(tool);
});
},
render : function(){
return <div className='main'>
<div className='top'>
<div className='logo'>
<NaturalCritIcon />
<span className='name'>
Natural
<span className='crit'>Crit</span>
</span>
</div>
<p>Top-tier tools for the discerning DM</p>
</div>
<div className='tools'>
{this.renderTools()}
</div>
</div>
}
});
module.exports = Main;

136
client/main/main.less Normal file
View File

@@ -0,0 +1,136 @@
@import 'naturalcrit/styles/core.less';
.main{
height : 100vh;
background-color : white;
.top{
.fadeInTop(1s);
.delay(0.5);
margin-bottom : 100px;
padding-top : 100px;
text-align : center;
.logo{
font-size : 4em;
color : black;
svg{
height : .9em;
margin-right : .2em;
cursor : pointer;
fill : black;
}
.name{
font-family : 'CodeLight';
.crit{
font-family : 'CodeBold';
}
}
}
p{
margin-top : 10px;
font-size : 1.3em;
font-style : italic;
color : @grey;
}
}
.tools{
width : 100%;
text-align : center;
.tool{
.sequentialDelay(0.5s, 1s);
.fadeInDown(1s);
.keep();
display : inline-block;
cursor : pointer;
opacity : 0;
color : black;
text-align : center;
text-decoration : none;
&+.tool{
border-left : 1px solid #666;
}
.content{
.addSketch(360px);
.animateAll(0.5s);
position : relative;
width : 320px;
padding : 35px;
&:hover{
svg, h2{
.transform(scale(1.3));
}
}
h2{
.animateAll(0.5s);
font-family : 'CodeBold';
font-size : 2em;
}
p{
max-width : 300px;
margin : 20px auto;
line-height : 1.5em;
}
svg{
.animateAll(0.5s);
height : 10em;
}
}
.content:hover{
background-color : fade(@teal, 20%);
}
//Beta styles
&.beta{
cursor : initial;
.content{
&:hover{
svg, h2{
.transform(scale(1.0));
}
}
svg, h2{
opacity : 0.3;
}
&:after{
.animateAll();
content : "beta!";
position : absolute;
display : block;
top : 120px;
left : 0px;
width : 100%;
padding : 10px 0px;
//opacity : 0;
background-color : fade(@grey, 50%);
font-size : 2em;
font-weight : 800;
text-align : center;
text-transform : uppercase;
}
}
}
}
}
}
.addSketch(@length, @color : black){
path, line, polyline, circle, rect, polygon {
.sketch(@length, @color, 4s);
stroke-dasharray : @length;
stroke-dashoffset : 0px;
stroke : @color;
stroke-width : 0.5px;
fill : @color;
//.animateAll(3s);
}
}
.sketch(@length, @color : black, @duration : 3s, @easing : @defaultEasing){
.createAnimation(sketch, @duration, @easing);
.sketchKeyFrames(){
0% { stroke-dashoffset : @length; fill: transparent;}
50% { stroke-dashoffset : @length; fill: transparent;}
80% { stroke-dashoffset : 0px; fill: transparent;}
100% { stroke-dashoffset : 0px; fill:@color;}
}
@-webkit-keyframes sketch {.sketchKeyFrames();}
@-moz-keyframes sketch {.sketchKeyFrames();}
@-ms-keyframes sketch {.sketchKeyFrames();}
@-o-keyframes sketch {.sketchKeyFrames();}
@keyframes sketch {.sketchKeyFrames();}
}

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