mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-23 16:33:05 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22a480871b | ||
|
|
daa3b096b3 | ||
|
|
bfcf6ca7f2 | ||
|
|
15ffb138eb | ||
|
|
7321cc81ec | ||
|
|
ba6ba0e51f |
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"unused-ignores": [
|
||||
"react-dom"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
# Javascript Node CircleCI 2.0 configuration file
|
||||
#
|
||||
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
|
||||
#
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
node: circleci/node@3.0.0
|
||||
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: cimg/node:16.11.0
|
||||
- image: mongo:4.4
|
||||
|
||||
working_directory: ~/homebrewery
|
||||
executor: node/default
|
||||
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/homebrewery
|
||||
|
||||
# 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-
|
||||
|
||||
- node/install-npm
|
||||
- node/install-packages:
|
||||
app-dir: ~/homebrewery
|
||||
cache-path: node_modules
|
||||
override-ci-command: npm i
|
||||
|
||||
- save_cache:
|
||||
paths:
|
||||
- node_modules
|
||||
key: v1-dependencies-{{ checksum "package.json" }}
|
||||
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- .
|
||||
|
||||
test:
|
||||
docker:
|
||||
- image: cimg/node:16.11.0
|
||||
|
||||
working_directory: ~/homebrewery
|
||||
parallelism: 4
|
||||
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
|
||||
# run tests!
|
||||
- run:
|
||||
name: Test - Basic
|
||||
command: npm run test:basic
|
||||
- run:
|
||||
name: Test - Mustache Spans
|
||||
command: npm run test:mustache-span
|
||||
- run:
|
||||
name: Test - Routes
|
||||
command: npm run test:route
|
||||
|
||||
workflows:
|
||||
build_and_test:
|
||||
jobs:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
tests
|
||||
78
.eslintrc.js
78
.eslintrc.js
@@ -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: 5 }],
|
||||
'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
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
||||
package-lock.json binary
|
||||
69
.github/dependabot.yml
vendored
69
.github/dependabot.yml
vendored
@@ -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
|
||||
32
.github/issue_template.md
vendored
32
.github/issue_template.md
vendored
@@ -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** :
|
||||
|
||||
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,13 +1,16 @@
|
||||
node_modules
|
||||
storage
|
||||
.idea
|
||||
*.swp
|
||||
|
||||
*.log
|
||||
build/*
|
||||
config/local.*
|
||||
config/docker.*
|
||||
|
||||
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
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,20 +1,29 @@
|
||||
FROM node:16.11-alpine
|
||||
RUN apk --no-cache add git
|
||||
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"]
|
||||
|
||||
|
||||
@@ -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`
|
||||
159
README.md
159
README.md
@@ -1,126 +1,33 @@
|
||||
# The Homebrewery
|
||||
|
||||
[](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 and 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 the 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" three times 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 / macOS: `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 don't contain any useful info. To help, you can [mark duplicates][github-mark-duplicate-url], try to
|
||||
reproduce some complex or weird issues, try finding a workaround for a
|
||||
reported bug, or just mention our issue managers team to let them know about
|
||||
outdated issues 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)
|
||||
|
||||
1075
changelog.md
1075
changelog.md
File diff suppressed because it is too large
Load Diff
@@ -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>;
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,10 +0,0 @@
|
||||
.BrewCleanup{
|
||||
.removeBox{
|
||||
margin-top: 20px;
|
||||
button{
|
||||
background-color: @red;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1,10 +0,0 @@
|
||||
.BrewCompress{
|
||||
.removeBox{
|
||||
margin-top: 20px;
|
||||
button{
|
||||
background-color: @red;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
153
client/admin/homebrewAdmin/homebrewAdmin.jsx
Normal file
153
client/admin/homebrewAdmin/homebrewAdmin.jsx
Normal 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;
|
||||
53
client/admin/homebrewAdmin/homebrewAdmin.less
Normal file
53
client/admin/homebrewAdmin/homebrewAdmin.less
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +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({
|
||||
displayName : 'BrewRenderer',
|
||||
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;
|
||||
@@ -104,119 +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 page' 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 {
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
return (
|
||||
<div className='page' id={`p${index + 1}`} key={index} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 id='BrewRenderer' initialContent={this.state.initialContent}
|
||||
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>
|
||||
<link href={`${this.props.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||
{this.state.isMounted
|
||||
&&
|
||||
<>
|
||||
{this.renderStyle()}
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</Frame>
|
||||
{this.renderPageInfo()}
|
||||
{this.renderPPRmsg()}
|
||||
</React.Fragment>
|
||||
);
|
||||
<div className='pages'>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
{this.renderPageInfo()}
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,76 +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({
|
||||
displayName : 'ErrorBar',
|
||||
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;
|
||||
@@ -1,60 +0,0 @@
|
||||
|
||||
.errorBar{
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
margin-right : 13px;
|
||||
padding : 20px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 100px;
|
||||
background-color : @red;
|
||||
color : white;
|
||||
i{
|
||||
position : absolute;
|
||||
left : 30px;
|
||||
opacity : 0.8;
|
||||
font-size : 3em;
|
||||
}
|
||||
h3{
|
||||
font-size : 1.1em;
|
||||
font-weight : 800;
|
||||
}
|
||||
ul{
|
||||
margin-top : 15px;
|
||||
font-size : 0.8em;
|
||||
list-style-position : inside;
|
||||
list-style-type : disc;
|
||||
li{
|
||||
line-height : 1.6em;
|
||||
}
|
||||
}
|
||||
hr{
|
||||
box-sizing : border-box;
|
||||
height : 2px;
|
||||
width : 150%;
|
||||
margin-top : 25px;
|
||||
margin-bottom : 15px;
|
||||
margin-left : -100px;
|
||||
background-color : darken(@red, 8%);
|
||||
border : none;
|
||||
}
|
||||
small{
|
||||
font-size: 0.6em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.protips{
|
||||
margin-left : -80px;
|
||||
font-size : 0.6em;
|
||||
&>div{
|
||||
margin-bottom : 10px;
|
||||
line-height : 1.2em;
|
||||
}
|
||||
h4{
|
||||
opacity : 0.8;
|
||||
font-weight : 800;
|
||||
line-height : 1.5em;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +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_notification08-27-22';
|
||||
|
||||
const NotificationPopup = createClass({
|
||||
displayName : 'NotificationPopup',
|
||||
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>V3.2.0 Released!</em> <br />
|
||||
We are happy to announce that after nearly a year of use by our many users,
|
||||
we are making the V3 render mode the default setting for all new brews.
|
||||
This mode has become quite popular, and has proven to be stable and powerful.
|
||||
Of course, we will always keep the option to use the Legacy renderer for any
|
||||
brew, which can still be accessed from the Properties menu.
|
||||
</li>
|
||||
|
||||
<li key='stubs'>
|
||||
<em>Change to Google Drive Storage!</em> <br />
|
||||
We have made a change to the process of tranferring brews between Google
|
||||
Drive and the Homebrewery storage. Starting now, any time a brew is
|
||||
transferred, it will keep the same links instead of generating new ones!
|
||||
We hope this change will help reduce issues where people "lost" their work
|
||||
by trying to visit old links.
|
||||
</li>
|
||||
|
||||
<li key='googleDriveFolder'>
|
||||
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
||||
We have had several reports of users losing their brews, not realizing
|
||||
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
||||
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
||||
We cannot help you recover files that you have deleted from your own
|
||||
Google Drive.
|
||||
</li>
|
||||
|
||||
<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!
|
||||
<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' />
|
||||
<div className='header'>
|
||||
<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>
|
||||
</div>
|
||||
<ul>{_.values(this.state.notifications)}</ul>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = NotificationPopup;
|
||||
@@ -1,64 +0,0 @@
|
||||
.popups{
|
||||
position : fixed;
|
||||
top : @navbarHeight;
|
||||
right : 15px;
|
||||
z-index : 10001;
|
||||
width : 450px;
|
||||
}
|
||||
|
||||
.notificationPopup{
|
||||
position : relative;
|
||||
display : inline-block;
|
||||
width : 100%;
|
||||
padding : 15px;
|
||||
padding-bottom : 10px;
|
||||
padding-left : 25px;
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
a{
|
||||
color : #e0e5c1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.header {
|
||||
padding-left : 50px;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,340 +1,129 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
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({
|
||||
displayName : 'Editor',
|
||||
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;
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||
this.highlightCustomMarkdown();
|
||||
if(prevProps.moveBrew !== this.props.moveBrew) {
|
||||
this.brewJump();
|
||||
};
|
||||
if(prevProps.moveSource !== this.props.moveSource) {
|
||||
this.sourceJump();
|
||||
};
|
||||
},
|
||||
handleSnippetClick : function(injectText){
|
||||
var lines = this.props.value.split('\n');
|
||||
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
|
||||
|
||||
updateEditorSize : function() {
|
||||
if(this.refs.codeEditor) {
|
||||
let paneHeight = this.refs.main.parentNode.clientHeight;
|
||||
paneHeight -= SNIPPETBAR_HEIGHT + 1;
|
||||
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
|
||||
}
|
||||
},
|
||||
|
||||
handleInject : function(injectText){
|
||||
let text;
|
||||
if(this.isText()) text = this.props.brew.text;
|
||||
if(this.isStyle()) text = this.props.brew.style ?? DEFAULT_STYLE_TEXT;
|
||||
|
||||
const lines = text.split('\n');
|
||||
const cursorPos = this.refs.codeEditor.getCursorPosition();
|
||||
lines[cursorPos.line] = splice(lines[cursorPos.line], cursorPos.ch, injectText);
|
||||
|
||||
const injectLines = injectText.split('\n');
|
||||
this.refs.codeEditor.setCursorPosition(cursorPos.line + injectLines.length, cursorPos.ch + injectLines[injectLines.length - 1].length);
|
||||
|
||||
if(this.isText()) this.props.onTextChange(lines.join('\n'));
|
||||
if(this.isStyle()) this.props.onStyleChange(lines.join('\n'));
|
||||
},
|
||||
|
||||
handleViewChange : function(newView){
|
||||
this.props.setMoveArrows(newView === 'text');
|
||||
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.refs.codeEditor.getCursorPosition().line + 1);
|
||||
return _.reduce(lines, (r, line)=>{
|
||||
if(
|
||||
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
|
||||
||
|
||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))
|
||||
) r++;
|
||||
return r;
|
||||
}, 1);
|
||||
},
|
||||
|
||||
highlightCustomMarkdown : function(){
|
||||
if(!this.refs.codeEditor) return;
|
||||
if(this.state.view === 'text') {
|
||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||
|
||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||
//reset custom text styles
|
||||
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
|
||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||
|
||||
let editorPageCount = 2; // start page count from page 2
|
||||
|
||||
_.forEach(this.props.brew.text.split('\n'), (line, lineNumber)=>{
|
||||
|
||||
//reset custom line styles
|
||||
codeMirror.removeLineClass(lineNumber, 'background', 'pageLine');
|
||||
codeMirror.removeLineClass(lineNumber, 'text');
|
||||
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||
|
||||
// Styling for \page breaks
|
||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
||||
|
||||
// add back the original class 'background' but also add the new class '.pageline'
|
||||
codeMirror.addLineClass(lineNumber, 'background', 'pageLine');
|
||||
const pageCountElement = Object.assign(document.createElement('span'), {
|
||||
className : 'editor-page-count',
|
||||
textContent : editorPageCount
|
||||
});
|
||||
codeMirror.setBookmark({ line: lineNumber, ch: line.length }, pageCountElement);
|
||||
|
||||
editorPageCount += 1;
|
||||
};
|
||||
|
||||
// New Codemirror styling for V3 renderer
|
||||
if(this.props.renderer == 'V3') {
|
||||
if(line.match(/^\\column$/)){
|
||||
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
|
||||
}
|
||||
|
||||
// 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' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
brewJump : function(targetPage=this.getCurrentPage()){
|
||||
if(!window) return;
|
||||
// console.log(`Scroll to: p${targetPage}`);
|
||||
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
||||
const currentPos = brewRenderer.scrollTop;
|
||||
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
||||
const interimPos = targetPos >= 0 ? -30 : 30;
|
||||
|
||||
const bounceDelay = 100;
|
||||
const scrollDelay = 500;
|
||||
|
||||
if(!this.throttleBrewMove) {
|
||||
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
|
||||
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
|
||||
setTimeout(()=>{
|
||||
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
|
||||
}, bounceDelay);
|
||||
}, scrollDelay, { leading: true, trailing: false });
|
||||
};
|
||||
this.throttleBrewMove(currentPos, interimPos, targetPos);
|
||||
|
||||
// const hashPage = (page != 1) ? `p${page}` : '';
|
||||
// window.location.hash = hashPage;
|
||||
},
|
||||
|
||||
sourceJump : function(targetLine=null){
|
||||
if(this.isText()) {
|
||||
if(targetLine == null) {
|
||||
targetLine = 0;
|
||||
|
||||
const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page');
|
||||
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height;
|
||||
|
||||
let currentPage = 1;
|
||||
for (const page of pageCollection) {
|
||||
if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) {
|
||||
currentPage = parseInt(page.id.slice(1)) || 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
||||
const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-1).join(textSplit);
|
||||
const textPosition = textString.length;
|
||||
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
|
||||
|
||||
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
||||
|
||||
let currentY = this.refs.codeEditor.codeMirror.getScrollInfo().top;
|
||||
let targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||
|
||||
//Scroll 1/10 of the way every 10ms until 1px off.
|
||||
const incrementalScroll = setInterval(()=>{
|
||||
currentY += (targetY - currentY) / 10;
|
||||
this.refs.codeEditor.codeMirror.scrollTo(null, currentY);
|
||||
|
||||
// Update target: target height is not accurate until within +-10 lines of the visible window
|
||||
if(Math.abs(targetY - currentY > 100))
|
||||
targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||
|
||||
// End when close enough
|
||||
if(Math.abs(targetY - currentY) < 1) {
|
||||
this.refs.codeEditor.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
||||
this.refs.codeEditor.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
||||
this.refs.codeEditor.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
||||
clearInterval(incrementalScroll);
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
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();
|
||||
},
|
||||
|
||||
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
|
||||
rerenderParent : function (){
|
||||
this.forceUpdate();
|
||||
},
|
||||
|
||||
renderEditor : function(){
|
||||
if(this.isText()){
|
||||
return <>
|
||||
<CodeEditor key='codeEditor'
|
||||
ref='codeEditor'
|
||||
language='gfm'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.text}
|
||||
onChange={this.props.onTextChange}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
if(this.isStyle()){
|
||||
return <>
|
||||
<CodeEditor key='codeEditor'
|
||||
ref='codeEditor'
|
||||
language='css'
|
||||
view={this.state.view}
|
||||
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
|
||||
onChange={this.props.onStyleChange}
|
||||
enableFolding={false}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
</>;
|
||||
}
|
||||
if(this.isMeta()){
|
||||
return <>
|
||||
<CodeEditor key='codeEditor'
|
||||
view={this.state.view}
|
||||
style={{ display: 'none' }}
|
||||
rerenderParent={this.rerenderParent} />
|
||||
<MetadataEditor
|
||||
metadata={this.props.brew}
|
||||
onChange={this.props.onMetaChange} />
|
||||
</>;
|
||||
}
|
||||
},
|
||||
|
||||
redo : function(){
|
||||
return this.refs.codeEditor?.redo();
|
||||
},
|
||||
|
||||
historySize : function(){
|
||||
return this.refs.codeEditor?.historySize();
|
||||
},
|
||||
|
||||
undo : function(){
|
||||
return this.refs.codeEditor?.undo();
|
||||
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(){
|
||||
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}
|
||||
undo={this.undo}
|
||||
redo={this.redo}
|
||||
historySize={this.historySize()} />
|
||||
|
||||
{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>
|
||||
},
|
||||
|
||||
});
|
||||
@@ -1,61 +1,56 @@
|
||||
|
||||
.editor{
|
||||
position : relative;
|
||||
width : 100%;
|
||||
|
||||
.codeEditor{
|
||||
height : 100%;
|
||||
.pageLine{
|
||||
background : #33333328;
|
||||
border-top : #339 solid 1px;
|
||||
}
|
||||
.editor-page-count{
|
||||
color : grey;
|
||||
float : right;
|
||||
}
|
||||
.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");
|
||||
}
|
||||
|
||||
.editorToolbar{
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 50%;
|
||||
color: black;
|
||||
font-size: 13px;
|
||||
z-index: 9;
|
||||
span {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
/* eslint-disable max-lines */
|
||||
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 StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||
|
||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||
|
||||
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||
|
||||
const MetadataEditor = createClass({
|
||||
displayName : 'MetadataEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
metadata : {
|
||||
editId : null,
|
||||
title : '',
|
||||
description : '',
|
||||
tags : [],
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : [],
|
||||
renderer : 'legacy'
|
||||
},
|
||||
onChange : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function(){
|
||||
return {
|
||||
showThumbnail : true
|
||||
};
|
||||
},
|
||||
|
||||
toggleThumbnailDisplay : function(){
|
||||
this.setState({
|
||||
showThumbnail : !this.state.showThumbnail
|
||||
});
|
||||
},
|
||||
|
||||
renderThumbnail : function(){
|
||||
if(!this.state.showThumbnail) return;
|
||||
return <img className='thumbnail-preview' src={this.props.metadata.thumbnail || homebreweryThumbnail}></img>;
|
||||
},
|
||||
|
||||
handleFieldChange : function(name, e){
|
||||
this.props.onChange({
|
||||
...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({
|
||||
...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.googleId ?? ''}${this.props.metadata.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
window.location.href = '/';
|
||||
});
|
||||
},
|
||||
|
||||
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>;
|
||||
},
|
||||
|
||||
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>
|
||||
|
||||
<a href='/legacy' target='_blank' rel='noopener noreferrer'>
|
||||
Click here to see the demo page for the old Legacy renderer!
|
||||
</a>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='metadataEditor'>
|
||||
<div className='field title'>
|
||||
<label>title</label>
|
||||
<input type='text' className='value'
|
||||
value={this.props.metadata.title}
|
||||
onChange={(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 thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text'
|
||||
value={this.props.metadata.thumbnail}
|
||||
placeholder='my.thumbnail.url'
|
||||
className='value'
|
||||
onChange={(e)=>this.handleFieldChange('thumbnail', e)} />
|
||||
<button className='display' onClick={this.toggleThumbnailDisplay}>
|
||||
<i className={`fas fa-caret-${this.state.showThumbnail ? 'right' : 'left'}`} />
|
||||
</button>
|
||||
{this.renderThumbnail()}
|
||||
</div>
|
||||
|
||||
<StringArrayEditor label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
||||
placeholder='add tag' unique={true}
|
||||
values={this.props.metadata.tags}
|
||||
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||
|
||||
{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.renderDelete()}
|
||||
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MetadataEditor;
|
||||
@@ -1,194 +0,0 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
|
||||
.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;
|
||||
}
|
||||
&.thumbnail{
|
||||
height : 1.4em;
|
||||
label{
|
||||
line-height: 2.0em;
|
||||
}
|
||||
.value{
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
button{
|
||||
border: 1px solid #999;
|
||||
color: white;
|
||||
padding: 0px 5px;
|
||||
background-color: black;
|
||||
&:hover{
|
||||
background-color: #777;
|
||||
}
|
||||
}
|
||||
.thumbnail-preview{
|
||||
position : relative;
|
||||
width : 80px;
|
||||
height : min-content;
|
||||
border : 2px solid white;
|
||||
margin-left : 5px;
|
||||
max-height : 115px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
a {
|
||||
font-size : 0.7em;
|
||||
font-weight : 800;
|
||||
display : inline-flex;
|
||||
}
|
||||
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{
|
||||
font-size : 0.6em;
|
||||
font-style : italic;
|
||||
}
|
||||
}
|
||||
|
||||
.delete.field .value{
|
||||
button{
|
||||
.button(@red);
|
||||
}
|
||||
}
|
||||
.authors.field .value{
|
||||
font-size: 0.8em;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
|
||||
.field .list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> * {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#groupedIcon {
|
||||
#backgroundColors;
|
||||
display: inline-block;
|
||||
height: ~"calc(100% + 0.6em)";
|
||||
position: relative;
|
||||
top: -0.3em;
|
||||
right: -0.3em;
|
||||
cursor: pointer;
|
||||
min-width: 20px;
|
||||
text-align: center;
|
||||
color: white;
|
||||
|
||||
i {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0.5em 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: #dddddd;
|
||||
border-radius: .5em;
|
||||
font-size: .9em;
|
||||
margin: 2px;
|
||||
padding: .3em;
|
||||
|
||||
.icon {
|
||||
#groupedIcon
|
||||
}
|
||||
}
|
||||
|
||||
.input-group {
|
||||
height: ~"calc(.9em + 4px + .6em)";
|
||||
|
||||
input {
|
||||
border-radius: .5em 0 0 .5em;
|
||||
}
|
||||
|
||||
input:last-child {
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 7.5vw;
|
||||
min-width: 75px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.invalid:focus {
|
||||
background-color: pink;
|
||||
}
|
||||
|
||||
.icon {
|
||||
#groupedIcon;
|
||||
height: 97%;
|
||||
font-size: .8em;
|
||||
right: 1px;
|
||||
top: -.54em;
|
||||
|
||||
i {
|
||||
font-size: 1.125em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,141 +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({
|
||||
displayName : 'SnippetBar',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
view : 'text',
|
||||
onViewChange : ()=>{},
|
||||
onInject : ()=>{},
|
||||
onToggle : ()=>{},
|
||||
showEditButtons : true,
|
||||
renderer : 'legacy',
|
||||
undo : ()=>{},
|
||||
redo : ()=>{},
|
||||
historySize : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
renderer : this.props.renderer
|
||||
};
|
||||
},
|
||||
|
||||
handleSnippetClick : function(injectedText){
|
||||
this.props.onInject(injectedText);
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
let snippets = [];
|
||||
|
||||
if(this.props.renderer === 'V3')
|
||||
snippets = SnippetsV3.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
else
|
||||
snippets = SnippetsLegacy.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||
|
||||
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={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||
onClick={this.props.undo} >
|
||||
<i className='fas fa-undo' />
|
||||
</div>
|
||||
<div className={`editorTool redo ${this.props.historySize.redo ? 'active' : ''}`}
|
||||
onClick={this.props.redo} >
|
||||
<i className='fas fa-redo' />
|
||||
</div>
|
||||
<div className='divider'></div>
|
||||
<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({
|
||||
displayName : 'SnippetGroup',
|
||||
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>;
|
||||
},
|
||||
|
||||
});
|
||||
@@ -1,113 +0,0 @@
|
||||
|
||||
.snippetBar{
|
||||
@menuHeight : 25px;
|
||||
position : relative;
|
||||
height : @menuHeight;
|
||||
background-color : #ddd;
|
||||
.editors{
|
||||
position : absolute;
|
||||
display : flex;
|
||||
top : 0px;
|
||||
right : 0px;
|
||||
height : @menuHeight;
|
||||
width : 125px;
|
||||
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');
|
||||
}
|
||||
&.undo{
|
||||
.tooltipLeft('Undo');
|
||||
font-size : 0.75em;
|
||||
color : grey;
|
||||
&.active{
|
||||
color : black;
|
||||
}
|
||||
}
|
||||
&.redo{
|
||||
.tooltipLeft('Redo');
|
||||
font-size : 0.75em;
|
||||
color : grey;
|
||||
&.active{
|
||||
color : black;
|
||||
}
|
||||
}
|
||||
&.divider {
|
||||
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
|
||||
width: 5px;
|
||||
&:hover{
|
||||
background-color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
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 spellSkill = _.sample(['Wisdom', 'Charisma', 'Intelligence']);
|
||||
|
||||
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 dedent`
|
||||
## 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(['Artisan\'s tools', 'one musical instrument', 'Thieves\' tools'], _.random(0, 2)).join(', ') || 'None'}
|
||||
|
||||
**Saving Throws:** :: ${_.sampleSize(abilityList, 2).join(', ')}
|
||||
**Skills:** :: Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}
|
||||
|
||||
#### Spellcasting Ability
|
||||
|
||||
{{text-align:center
|
||||
**Spell save DC**:: = ${_.sample([6, 8, 10])} + your proficiency bonus + your ${spellSkill} modifier
|
||||
|
||||
**Spell attack modifier**:: = your proficiency bonus + your ${spellSkill} modifier
|
||||
}}
|
||||
|
||||
#### 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'])}
|
||||
|
||||
`;
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
const features = [
|
||||
'Astrological Botany',
|
||||
'Biochemical Sorcery',
|
||||
'Civil Divination',
|
||||
'Consecrated Augury',
|
||||
'Demonic Anthropology',
|
||||
'Divinatory Mineralogy',
|
||||
'Exo Interfacer',
|
||||
'Genetic Banishing',
|
||||
'Gunpowder Torturer',
|
||||
'Gunslinger Corruptor',
|
||||
'Hermetic Geography',
|
||||
'Immunological Cultist',
|
||||
'Malefic Chemist',
|
||||
'Mathematical Pharmacy',
|
||||
'Nuclear Biochemistry',
|
||||
'Orbital Gravedigger',
|
||||
'Pharmaceutical Outlaw',
|
||||
'Phased Linguist',
|
||||
'Plasma Gunslinger',
|
||||
'Police Necromancer',
|
||||
'Ritual Astronomy',
|
||||
'Sixgun Poisoner',
|
||||
'Seismological Alchemy',
|
||||
'Spiritual Illusionism',
|
||||
'Statistical Occultism',
|
||||
'Spell Analyst',
|
||||
'Torque Interfacer'
|
||||
];
|
||||
|
||||
const classnames = ['Ackerman', 'Berserker-Typist', 'Concierge', 'Fishmonger',
|
||||
'Haberdasher', 'Manicurist', 'Netrunner', 'Weirkeeper'];
|
||||
|
||||
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 maxes = [4, 3, 3, 3, 3, 2, 2, 1, 1];
|
||||
|
||||
const drawSlots = function(Slots, rows, padding){
|
||||
let slots = Number(Slots);
|
||||
return _.times(rows, function(i){
|
||||
const max = maxes[i];
|
||||
if(slots < 1) return _.pad('—', padding);
|
||||
const res = _.min([max, slots]);
|
||||
slots -= res;
|
||||
return _.pad(res.toString(), padding);
|
||||
}).join(' | ');
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
full : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency | Features | Cantrips | Spells | --- Spell Slots Per Spell Level ---|||||||||\n`+
|
||||
`| ^| Bonus ^| ^| Known ^| Known ^|1st |2nd |3rd |4th |5th |6th |7th |8th |9th |\n`+
|
||||
`|:-----:|:-----------:|:-------------|:--------:|:------:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 21),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 6),
|
||||
drawSlots(slots, 9, 2),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 2);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
},
|
||||
|
||||
half : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let featureScore = 1;
|
||||
return `{{${classes}\n##### The ${classname}\n` +
|
||||
`| Level | Proficiency Bonus | Features | ${_.pad(_.sample(features), 21)} |\n` +
|
||||
`|:-----:|:-----------------:|:---------|:---------------------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 5),
|
||||
_.pad(`+${profBonus[level]}`, 2),
|
||||
_.padEnd(_.sample(features), 23),
|
||||
_.pad(`+${featureScore}`, 21),
|
||||
].join(' | ');
|
||||
|
||||
featureScore += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
},
|
||||
|
||||
third : function(classes){
|
||||
const classname = _.sample(classnames);
|
||||
|
||||
let cantrips = 3;
|
||||
let spells = 1;
|
||||
let slots = 2;
|
||||
return `{{${classes}\n##### ${classname} Spellcasting\n` +
|
||||
`| Class | Cantrips | Spells |--- Spells Slots per Spell Level ---||||\n` +
|
||||
`| Level ^| Known ^| Known ^| 1st | 2nd | 3rd | 4th |\n` +
|
||||
`|:------:|:--------:|:-------:|:-------:|:-------:|:-------:|:-------:|\n${
|
||||
_.map(levels, function(levelName, level){
|
||||
const res = [
|
||||
_.pad(levelName, 6),
|
||||
_.pad(cantrips.toString(), 8),
|
||||
_.pad(spells.toString(), 7),
|
||||
drawSlots(slots, 4, 7),
|
||||
].join(' | ');
|
||||
|
||||
cantrips += _.random(0, 1);
|
||||
spells += _.random(0, 1);
|
||||
slots += _.random(0, 1);
|
||||
|
||||
return `| ${res} |`;
|
||||
}).join('\n')}\n}}\n\n`;
|
||||
}
|
||||
};
|
||||
@@ -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>
|
||||
.page#p1{ text-align:center; counter-increment: none; }
|
||||
.page#p1:after{ display:none; }
|
||||
.page:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
|
||||
.page:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
|
||||
.page:nth-child(2n)::after { transform: scaleX(1); }
|
||||
.page:nth-child(2n+1)::after { transform: scaleX(-1); }
|
||||
.page:nth-child(2n) .footnote { left: inherit; text-align: right; }
|
||||
.page:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
|
||||
</style>
|
||||
|
||||
{{margin-top:225px}}
|
||||
|
||||
# ${_.sample(titles)}
|
||||
|
||||
{{margin-top:25px}}
|
||||
|
||||
{{wide
|
||||
##### ${_.sample(subtitles)}
|
||||
}}
|
||||
|
||||
\\page`;
|
||||
};
|
||||
@@ -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`;
|
||||
};
|
||||
@@ -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(4, 10)), (spell)=>{
|
||||
return `- ${spell}`;
|
||||
}).join('\n');
|
||||
return `##### ${level} \n${spells} \n`;
|
||||
}).join('\n');
|
||||
|
||||
return `{{spellList,wide\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');
|
||||
}
|
||||
};
|
||||
@@ -1,184 +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 genLongAbilities = function(){
|
||||
return _.sample([
|
||||
dedent`***Pack Tactics.*** These guys work together like peanut butter and jelly. Jelly and peanut butter.
|
||||
|
||||
When one of these guys attacks, the target is covered with, well, peanut butter and jelly.`,
|
||||
dedent`***Hangriness.*** This creature is angry, and hungry. It will refuse to do anything with you until its hunger is satisfied.
|
||||
|
||||
When in visual contact with this creature, you must purchase an extra order of fries, even if they say they aren't hungry.`,
|
||||
dedent`***Full of Detergent.*** This creature has swallowed an entire bottle of dish detergent and is actually having a pretty good time.
|
||||
|
||||
While walking near this creature, you must make a dexterity check or become "a soapy mess" for three hours, after which your skin will get all dry and itchy.`
|
||||
]);
|
||||
};
|
||||
|
||||
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:\n')}
|
||||
:
|
||||
${genLongAbilities()}
|
||||
### Actions
|
||||
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n:\n')}
|
||||
}}
|
||||
\n`;
|
||||
}
|
||||
};
|
||||
@@ -1,407 +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;
|
||||
const watercolorGen = require('./watercolor.gen.js');
|
||||
|
||||
|
||||
module.exports = [
|
||||
|
||||
{
|
||||
groupName : 'Text Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
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 manually place column breaks with \`\column\` to make the
|
||||
surrounding text flow with this wide block the way you want.
|
||||
}}
|
||||
\n`
|
||||
},
|
||||
{
|
||||
name : 'QR Code',
|
||||
icon : 'fas fa-qrcode',
|
||||
gen : (brew)=>{
|
||||
return `![]` +
|
||||
`(https://api.qrserver.com/v1/create-qr-code/?data=` +
|
||||
`https://homebrewery.naturalcrit.com${brew.shareId ? `/share/${brew.shareId}` : ''}` +
|
||||
`&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 : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
groupName : 'Style Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Remove Drop Cap',
|
||||
icon : 'fas fa-remove-format',
|
||||
gen : dedent`/* Removes Drop Caps */
|
||||
.page h1+p:first-letter {
|
||||
all: unset;
|
||||
}\n\n
|
||||
/* Removes Small-Caps in first line */
|
||||
.page h1+p:first-line {
|
||||
all: unset;
|
||||
}`
|
||||
},
|
||||
{
|
||||
name : 'Tweak Drop Cap',
|
||||
icon : 'fas fa-sliders-h',
|
||||
gen : dedent`/* Drop Cap settings */
|
||||
.page h1 + p::first-letter {
|
||||
font-family: SolberaImitationRemake;
|
||||
font-size: 3.5cm;
|
||||
background-image: linear-gradient(-45deg, #322814, #998250, #322814);
|
||||
line-height: 1em;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
gen : '/* This is a comment that will not be rendered into your brew. */'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
/*********************** IMAGES *******************/
|
||||
{
|
||||
groupName : 'Images',
|
||||
icon : 'fas fa-images',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Image',
|
||||
icon : 'fas fa-image',
|
||||
gen : dedent`
|
||||
 {width:325px,mix-blend-mode:multiply}
|
||||
|
||||
{{artist,position:relative,top:-230px,left:10px,margin-bottom:-30px
|
||||
##### Cat Warrior
|
||||
[Kyoung Hwan Kim](https://www.artstation.com/tahra)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Background Image',
|
||||
icon : 'fas fa-tree',
|
||||
gen : dedent`
|
||||
 {position:absolute,top:50px,right:30px,width:280px}
|
||||
|
||||
{{artist,top:80px,right:30px
|
||||
##### Homebrew Mug
|
||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||
}}`
|
||||
},
|
||||
{
|
||||
name : 'Watercolor Splatter',
|
||||
icon : 'fas fa-fill-drip',
|
||||
gen : watercolorGen,
|
||||
},
|
||||
{
|
||||
name : 'Watermark',
|
||||
icon : 'fas fa-id-card',
|
||||
gen : dedent`
|
||||
{{watermark Homebrewery}}\n`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fas fa-book',
|
||||
view : 'text',
|
||||
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,
|
||||
},
|
||||
{
|
||||
name : 'Artist Credit',
|
||||
icon : 'fas fa-signature',
|
||||
gen : function(){
|
||||
return dedent`
|
||||
{{artist,top:90px,right:30px
|
||||
##### Starry Night
|
||||
[Van Gogh](https://www.vangoghmuseum.nl/en)
|
||||
}}
|
||||
\n`;
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
/********************* TABLES *********************/
|
||||
|
||||
{
|
||||
groupName : 'Tables',
|
||||
icon : 'fas fa-table',
|
||||
view : 'text',
|
||||
snippets : [
|
||||
{
|
||||
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* | | | | |
|
||||
|   Club | 1 sp | 1d4 bludgeoning | 2 lb. | Light |
|
||||
|   Dagger | 2 gp | 1d4 piercing | 1 lb. | Finesse |
|
||||
|   Spear | 1 gp | 1d6 piercing | 3 lb. | Thrown |
|
||||
| *Simple Ranged Weapons* | | | | |
|
||||
|   Dart | 5 cp | 1d4 piercig | 1/4 lb. | Finesse |
|
||||
|   Shortbow | 25 gp | 1d6 piercing | 2 lb. | Ammunition |
|
||||
|   Sling | 1 sp | 1d4 bludgeoning | — | 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`;
|
||||
}
|
||||
},
|
||||
{
|
||||
name : 'Class Table',
|
||||
icon : 'fas fa-table',
|
||||
gen : ClassTableGen.full('classTable,frame,decoration,wide'),
|
||||
},
|
||||
{
|
||||
name : 'Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.full('classTable,wide'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table',
|
||||
icon : 'fas fa-list-alt',
|
||||
gen : ClassTableGen.half('classTable,decoration,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/2 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.half('classTable'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table',
|
||||
icon : 'fas fa-border-all',
|
||||
gen : ClassTableGen.third('classTable,frame'),
|
||||
},
|
||||
{
|
||||
name : '1/3 Class Table (unframed)',
|
||||
icon : 'fas fa-border-none',
|
||||
gen : ClassTableGen.third('classTable'),
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** PAGE *************/
|
||||
|
||||
{
|
||||
groupName : 'Print',
|
||||
icon : 'fas fa-print',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'A4 Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : dedent`/* A4 Page Size */
|
||||
.page{
|
||||
width : 210mm;
|
||||
height : 296.8mm;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Square Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : dedent`/* Square Page Size */
|
||||
.page {
|
||||
width : 125mm;
|
||||
height : 125mm;
|
||||
padding : 12.5mm;
|
||||
columns : unset;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Ink Friendly',
|
||||
icon : 'fas fa-tint',
|
||||
gen : dedent`
|
||||
/* Ink Friendly */
|
||||
*:is(.page,.monster,.note,.descriptive) {
|
||||
background : white !important;
|
||||
filter : drop-shadow(0px 0px 3px #888) !important;
|
||||
}
|
||||
|
||||
.page img {
|
||||
visibility : hidden;
|
||||
}\n\n`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
];
|
||||
@@ -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(`- ### [{{ ${g1.title}}}{{ ${g1.page}}}](#p${g1.page})`);
|
||||
}
|
||||
if(g1.children.length){
|
||||
_.each(g1.children, (g2, idx2)=>{
|
||||
if(g2.title !== null) {
|
||||
r.push(` - #### [{{ ${g2.title}}}{{ ${g2.page}}}](#p${g2.page})`);
|
||||
}
|
||||
if(g2.children.length){
|
||||
_.each(g2.children, (g3, idx3)=>{
|
||||
if(g2.title !== null) {
|
||||
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||
} else { // Don't over-indent if no level-2 parent entry
|
||||
r.push(` - [{{ ${g3.title}}}{{ ${g3.page}}}](#p${g3.page})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return r;
|
||||
}, []).join('\n');
|
||||
|
||||
return dedent`
|
||||
{{toc,wide
|
||||
# Table Of Contents
|
||||
|
||||
${markdown}
|
||||
}}
|
||||
\n`;
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
const _ = require('lodash');
|
||||
|
||||
module.exports = ()=>{
|
||||
return `{{watercolor${_.random(1, 12)},top:20px,left:30px,width:300px,background-color:#BBAD82,opacity:80%}}\n\n`;
|
||||
};
|
||||
@@ -1,52 +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 spellSkill = _.sample(['Wisdom', 'Charisma', 'Intelligence']);
|
||||
|
||||
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(['Artisan\'s tools', 'one musical instrument', 'Thieves\' tools'], _.random(0, 2)).join(', ') || 'None'}`,
|
||||
'',
|
||||
'___',
|
||||
`- **Saving Throws:** ${_.sampleSize(abilityList, 2).join(', ')}`,
|
||||
`- **Skills:** Choose two from ${_.sampleSize(skillList, _.random(4, 6)).join(', ')}`,
|
||||
'',
|
||||
'#### Spellcasting Ability',
|
||||
'',
|
||||
`<div style=text-align:center>`,
|
||||
'___',
|
||||
`- **Spell save DC** = ${_.sample([6, 8, 10])} + your proficiency bonus + your ${spellSkill} modifier`,
|
||||
'',
|
||||
`- **Spell attack modifier** = your proficiency bonus + your ${spellSkill} modifier`,
|
||||
`</div>`,
|
||||
'',
|
||||
'#### Equipment',
|
||||
'You start with the following equipment, in addition to the equipment granted by your background:',
|
||||
'- *(a)* a martial weapon and a shield or *(b)* two martial weapons',
|
||||
'- *(a)* five javelins or *(b)* any simple melee weapon',
|
||||
`- ${_.sample(['10 lint fluffs', '1 button', 'a cherished lost sock'])}`,
|
||||
'\n\n\n'
|
||||
].join('\n');
|
||||
};
|
||||
@@ -1,114 +0,0 @@
|
||||
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`;
|
||||
}
|
||||
};
|
||||
@@ -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`;
|
||||
};
|
||||
@@ -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`;
|
||||
};
|
||||
@@ -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)', '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 `<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');
|
||||
}
|
||||
};
|
||||
@@ -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`;
|
||||
}
|
||||
};
|
||||
@@ -1,327 +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 : 'Text Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'text',
|
||||
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 : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
gen : '<!-- This is a comment that will not be rendered into your brew. Hotkey (Ctrl/Cmd + /). -->'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
groupName : 'Style Editor',
|
||||
icon : 'fas fa-pencil-alt',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'Remove Drop Cap',
|
||||
icon : 'fas fa-remove-format',
|
||||
gen : dedent`/* Removes Drop Caps */
|
||||
.phb h1+p:first-letter {
|
||||
all: unset;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Tweak Drop Cap',
|
||||
icon : 'fas fa-sliders-h',
|
||||
gen : dedent`/* Drop Cap Settings */
|
||||
.phb h1 + p::first-letter {
|
||||
float: left;
|
||||
font-family: Solberry;
|
||||
font-size: 10em;
|
||||
color: #222;
|
||||
line-height: .8em;
|
||||
}\n\n`
|
||||
},
|
||||
{
|
||||
name : 'Add Comment',
|
||||
icon : 'fas fa-code',
|
||||
gen : '/* This is a comment that will not be rendered into your brew. */'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
/************************* PHB ********************/
|
||||
|
||||
{
|
||||
groupName : 'PHB',
|
||||
icon : 'fas fa-book',
|
||||
view : 'text',
|
||||
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,
|
||||
},
|
||||
{
|
||||
name : 'Artist Credit',
|
||||
icon : 'fas fa-signature',
|
||||
gen : '<div class=\'artist\' style=\'top:90px;right:30px;\'>\n' +
|
||||
'##### Starry Night\n' +
|
||||
'[Van Gogh](https://www.vangoghmuseum.nl/en)\n' +
|
||||
'</div>\n'
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
/********************* TABLES *********************/
|
||||
|
||||
{
|
||||
groupName : 'Tables',
|
||||
icon : 'fas fa-table',
|
||||
view : 'text',
|
||||
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 : dedent`\n
|
||||
<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`
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
/**************** PRINT *************/
|
||||
|
||||
{
|
||||
groupName : 'Print',
|
||||
icon : 'fas fa-print',
|
||||
view : 'style',
|
||||
snippets : [
|
||||
{
|
||||
name : 'A4 Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : ['/* A4 Page Size */',
|
||||
'.phb {',
|
||||
' width : 210mm;',
|
||||
' height : 296.8mm;',
|
||||
'}'
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Square Page Size',
|
||||
icon : 'far fa-file',
|
||||
gen : ['/* Square Page Size */',
|
||||
'.phb {',
|
||||
' width : 125mm;',
|
||||
' height : 125mm;',
|
||||
' padding : 12.5mm;',
|
||||
' columns : unset;',
|
||||
'}',
|
||||
''
|
||||
].join('\n')
|
||||
},
|
||||
{
|
||||
name : 'Ink Friendly',
|
||||
icon : 'fas fa-tint',
|
||||
gen : dedent`
|
||||
/* Ink Friendly */
|
||||
.phb, .phb blockquote, .phb hr+blockquote {
|
||||
background : white;
|
||||
box-shadow : 0px 0px 3px;
|
||||
}
|
||||
|
||||
.phb img {
|
||||
visibility : hidden;
|
||||
}`
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
];
|
||||
@@ -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`;
|
||||
};
|
||||
42
client/homebrew/editor/snippets/classfeature.gen.js
Normal file
42
client/homebrew/editor/snippets/classfeature.gen.js
Normal 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');
|
||||
}
|
||||
105
client/homebrew/editor/snippets/classtable.gen.js
Normal file
105
client/homebrew/editor/snippets/classtable.gen.js
Normal 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';
|
||||
}
|
||||
};
|
||||
43
client/homebrew/editor/snippets/fullclass.gen.js
Normal file
43
client/homebrew/editor/snippets/fullclass.gen.js
Normal 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';
|
||||
};
|
||||
196
client/homebrew/editor/snippets/monsterblock.gen.js
Normal file
196
client/homebrew/editor/snippets/monsterblock.gen.js
Normal 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';
|
||||
}
|
||||
}
|
||||
175
client/homebrew/editor/snippets/snippets.js
Normal file
175
client/homebrew/editor/snippets/snippets.js
Normal 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')
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
|
||||
78
client/homebrew/editor/snippets/spell.gen.js
Normal file
78
client/homebrew/editor/snippets/spell.gen.js
Normal 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');
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
|
||||
const StringArrayEditor = createClass({
|
||||
displayName : 'StringArrayEditor',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
label : '',
|
||||
values : [],
|
||||
valuePatterns : null,
|
||||
placeholder : '',
|
||||
unique : false,
|
||||
cannotEdit : [],
|
||||
onChange : ()=>{}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
valueContext : !!this.props.values ? this.props.values.map((value)=>({
|
||||
value,
|
||||
editing : false
|
||||
})) : [],
|
||||
temporaryValue : '',
|
||||
updateValue : ''
|
||||
};
|
||||
},
|
||||
|
||||
componentDidUpdate : function(prevProps) {
|
||||
if(!_.eq(this.props.values, prevProps.values)) {
|
||||
this.setState({
|
||||
valueContext : this.props.values ? this.props.values.map((newValue)=>({
|
||||
value : newValue,
|
||||
editing : this.state.valueContext.find(({ value })=>value === newValue)?.editing || false
|
||||
})) : []
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleChange : function(value) {
|
||||
this.props.onChange({
|
||||
target : {
|
||||
value
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addValue : function(value){
|
||||
this.handleChange(_.uniq([...this.props.values, value]));
|
||||
this.setState({
|
||||
temporaryValue : ''
|
||||
});
|
||||
},
|
||||
|
||||
removeValue : function(index){
|
||||
this.handleChange(this.props.values.filter((_, i)=>i !== index));
|
||||
},
|
||||
|
||||
updateValue : function(value, index){
|
||||
const valueContext = this.state.valueContext;
|
||||
valueContext[index].value = value;
|
||||
valueContext[index].editing = false;
|
||||
this.handleChange(valueContext.map((context)=>context.value));
|
||||
this.setState({ valueContext, updateValue: '' });
|
||||
},
|
||||
|
||||
editValue : function(index){
|
||||
if(!!this.props.cannotEdit && this.props.cannotEdit.includes(this.props.values[index])) {
|
||||
return;
|
||||
}
|
||||
const valueContext = this.state.valueContext.map((context, i)=>{
|
||||
context.editing = index === i;
|
||||
return context;
|
||||
});
|
||||
this.setState({ valueContext, updateValue: this.props.values[index] });
|
||||
},
|
||||
|
||||
valueIsValid : function(value, index) {
|
||||
const values = _.clone(this.props.values);
|
||||
if(index !== undefined) {
|
||||
values.splice(index, 1);
|
||||
}
|
||||
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||
return matchesPatterns && uniqueIfSet;
|
||||
},
|
||||
|
||||
handleValueInputKeyDown : function(event, index) {
|
||||
if(event.key === 'Enter') {
|
||||
if(this.valueIsValid(event.target.value, index)) {
|
||||
if(index !== undefined) {
|
||||
this.updateValue(event.target.value, index);
|
||||
} else {
|
||||
this.addValue(event.target.value);
|
||||
}
|
||||
}
|
||||
} else if(event.key === 'Escape') {
|
||||
this.closeEditInput(index);
|
||||
}
|
||||
},
|
||||
|
||||
closeEditInput : function(index) {
|
||||
const valueContext = this.state.valueContext;
|
||||
valueContext[index].editing = false;
|
||||
this.setState({ valueContext, updateValue: '' });
|
||||
},
|
||||
|
||||
render : function() {
|
||||
const valueElements = Object.values(this.state.valueContext).map((context, i)=>context.editing
|
||||
? <React.Fragment key={i}>
|
||||
<div className='input-group'>
|
||||
<input type='text' className={`value ${this.valueIsValid(this.state.updateValue, i) ? '' : 'invalid'}`} autoFocus placeholder={this.props.placeholder}
|
||||
value={this.state.updateValue}
|
||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e, i)}
|
||||
onChange={(e)=>this.setState({ updateValue: e.target.value })}/>
|
||||
{<div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.closeEditInput(i); }}><i className='fa fa-undo fa-fw'/></div>}
|
||||
{this.valueIsValid(this.state.updateValue, i) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
: <div className='badge' key={i} onClick={()=>this.editValue(i)}>{context.value}
|
||||
{!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null : <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.removeValue(i); }}><i className='fa fa-times fa-fw'/></div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div className='field values'>
|
||||
<label>{this.props.label}</label>
|
||||
<div className='list'>
|
||||
{valueElements}
|
||||
<div className='input-group'>
|
||||
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||
value={this.state.temporaryValue}
|
||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = StringArrayEditor;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 305 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,95 +1,56 @@
|
||||
require('./homebrew.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const { StaticRouter:Router } = require('react-router-dom/server');
|
||||
const { Route, Routes, useParams, useSearchParams } = require('react-router-dom');
|
||||
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 WithRoute = (props)=>{
|
||||
const params = useParams();
|
||||
const [searchParams] = useSearchParams();
|
||||
const queryParams = {};
|
||||
for (const [key, value] of searchParams?.entries() || []) {
|
||||
queryParams[key] = value;
|
||||
}
|
||||
const Element = props.el;
|
||||
const allProps = {
|
||||
...props,
|
||||
...params,
|
||||
query : queryParams,
|
||||
el : undefined
|
||||
};
|
||||
return <Element {...allProps} />;
|
||||
};
|
||||
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');
|
||||
|
||||
const Homebrew = createClass({
|
||||
displayName : 'Homebrewery',
|
||||
getDefaultProps : function() {
|
||||
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() {
|
||||
Router = CreateRouter({
|
||||
'/homebrew/edit/:id' : (args) => {
|
||||
return <EditPage id={args.id} brew={this.props.brew} />
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
global.enable_v3 = this.props.enable_v3;
|
||||
global.config = this.props.config;
|
||||
|
||||
return {};
|
||||
'/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}>
|
||||
render : function(){
|
||||
return(
|
||||
<div className='homebrew'>
|
||||
<Routes>
|
||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||
<Route path='/print/:id' element={<WithRoute el={PrintPage} brew={this.props.brew} />} />
|
||||
<Route path='/print' element={<WithRoute el={PrintPage} />} />
|
||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||
</Routes>
|
||||
<Router initialUrl={this.props.url}/>
|
||||
</div>
|
||||
</Router>;
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Homebrew;
|
||||
|
||||
//TODO: Nicer Error page instead of just "cant get that"
|
||||
// '/share/:id' : (args)=>{
|
||||
// if(!this.props.brew.shareId){
|
||||
// return <ErrorPage errorId={args.id}/>;
|
||||
// }
|
||||
//
|
||||
// return <SharePage
|
||||
// id={args.id}
|
||||
// brew={this.props.brew} />;
|
||||
// },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const request = require('superagent');
|
||||
|
||||
const Account = createClass({
|
||||
displayName : 'AccountNavItem',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
url : ''
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function(){
|
||||
if(typeof window !== 'undefined'){
|
||||
this.setState({
|
||||
url : window.location.href
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleLogout : function(){
|
||||
if(confirm('Are you sure you want to log out?')) {
|
||||
// Reset divider position
|
||||
window.localStorage.removeItem('naturalcrit-pane-split');
|
||||
// Clear login cookie
|
||||
let domain = '';
|
||||
if(window.location?.hostname) {
|
||||
let domainArray = window.location.hostname.split('.');
|
||||
if(domainArray.length > 2){
|
||||
domainArray = [''].concat(domainArray.slice(-2));
|
||||
}
|
||||
domain = domainArray.join('.');
|
||||
}
|
||||
document.cookie = `nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;samesite=lax;${domain ? `domain=${domain}` : ''}`;
|
||||
window.location = '/';
|
||||
}
|
||||
},
|
||||
|
||||
localLogin : async function(){
|
||||
const username = prompt('Enter username:');
|
||||
if(!username) {return;}
|
||||
|
||||
const expiry = new Date;
|
||||
expiry.setFullYear(expiry.getFullYear() + 1);
|
||||
|
||||
const token = await request.post('/local/login')
|
||||
.send({ username })
|
||||
.then((response)=>{
|
||||
return response.body;
|
||||
})
|
||||
.catch((err)=>{
|
||||
console.warn(err);
|
||||
});
|
||||
if(!token) return;
|
||||
|
||||
document.cookie = `nc_session=${token};expires=${expiry};path=/;samesite=lax;${window.domain ? `domain=${window.domain}` : ''}`;
|
||||
window.location.reload(true);
|
||||
},
|
||||
|
||||
render : function(){
|
||||
// Logged in
|
||||
if(global.account){
|
||||
return <Nav.dropdown>
|
||||
<Nav.item
|
||||
className='account'
|
||||
color='orange'
|
||||
icon='fas fa-user'
|
||||
>
|
||||
{global.account.username}
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
href={`/user/${global.account.username}`}
|
||||
color='yellow'
|
||||
icon='fas fa-beer'
|
||||
>
|
||||
brews
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='logout'
|
||||
color='red'
|
||||
icon='fas fa-power-off'
|
||||
onClick={this.handleLogout}
|
||||
>
|
||||
logout
|
||||
</Nav.item>
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
// Logged out
|
||||
// LOCAL ONLY
|
||||
if(global.config.local) {
|
||||
return <Nav.item color='teal' icon='fas fa-sign-in-alt' onClick={this.localLogin}>
|
||||
login
|
||||
</Nav.item>;
|
||||
};
|
||||
|
||||
// Logged out
|
||||
// Production site
|
||||
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;
|
||||
@@ -1,16 +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({
|
||||
displayName : 'EditTitleNavItem',
|
||||
getDefaultProps : function() {
|
||||
var EditTitle = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
title : '',
|
||||
title : '',
|
||||
onChange : function(){}
|
||||
};
|
||||
},
|
||||
@@ -23,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;
|
||||
@@ -1,30 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const dedent = require('dedent-tabs').default;
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.dropdown>
|
||||
<Nav.item color='grey' icon='fas fa-question-circle'>
|
||||
need help?
|
||||
</Nav.item>
|
||||
<Nav.item color='red' icon='fas fa-fw fa-bug'
|
||||
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent`
|
||||
**Browser(s)** :
|
||||
**Operating System** :
|
||||
**Legacy or v3 Renderer** :
|
||||
**Issue** : `)}`}
|
||||
newTab={true}
|
||||
rel='noopener noreferrer'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' icon='fas fa-fw fa-file-import'
|
||||
href='/migrate'
|
||||
newTab={true}
|
||||
rel='noopener noreferrer'>
|
||||
migrate
|
||||
</Nav.item>
|
||||
</Nav.dropdown>;
|
||||
};
|
||||
8
client/homebrew/navbar/issue.navitem.jsx
Normal file
8
client/homebrew/navbar/issue.navitem.jsx
Normal file
@@ -0,0 +1,8 @@
|
||||
var React = require('react');
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
|
||||
module.exports = function(props){
|
||||
return <Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
};
|
||||
@@ -1,51 +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({
|
||||
displayName : 'Navbar',
|
||||
getInitialState : function() {
|
||||
return {
|
||||
//showNonChromeWarning : false,
|
||||
ver : '0.0.0'
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
ver : global.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 newTab={true} href='/changelog' color='purple' icon='far fa-file-alt'>
|
||||
{`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>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,148 +1,58 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
@navbarHeight : 28px;
|
||||
@keyframes pinkColoring {
|
||||
//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: pinkColoring;
|
||||
animation-duration: 2s;
|
||||
color: pink;
|
||||
}
|
||||
}
|
||||
.recent.navItem {
|
||||
position : relative;
|
||||
.dropdown{
|
||||
position : absolute;
|
||||
top : 28px;
|
||||
left : 0px;
|
||||
z-index : 10000;
|
||||
width : 100%;
|
||||
overflow : hidden auto;
|
||||
max-height : ~"calc(100vh - 28px)";
|
||||
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{
|
||||
#backgroundColorsHover;
|
||||
.animate(background-color);
|
||||
position : relative;
|
||||
display : block;
|
||||
box-sizing : border-box;
|
||||
padding : 8px 5px 13px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
.account.navItem{
|
||||
min-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.homebrew nav{
|
||||
.homebrewLogo{
|
||||
.animate(color);
|
||||
font-family : CodeBold;
|
||||
font-size : 12px;
|
||||
color : white;
|
||||
div{
|
||||
margin-top : 2px;
|
||||
margin-bottom : -2px;
|
||||
}
|
||||
&:hover{
|
||||
color : @blue;
|
||||
}
|
||||
}
|
||||
.editTitle.navItem{
|
||||
padding : 2px 12px;
|
||||
input{
|
||||
margin : 0;
|
||||
padding : 2px;
|
||||
width : 250px;
|
||||
background-color : #444;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
border : 1px solid @blue;
|
||||
outline : none;
|
||||
}
|
||||
.charCount{
|
||||
display : inline-block;
|
||||
vertical-align : bottom;
|
||||
margin-left : 8px;
|
||||
text-align : right;
|
||||
color : #666;
|
||||
&.max{
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
.brewTitle.navItem{
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
text-align : center;
|
||||
text-transform: initial;
|
||||
}
|
||||
.patreon.navItem{
|
||||
i{
|
||||
.animate(color);
|
||||
&:hover{
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,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>;
|
||||
};
|
||||
@@ -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>
|
||||
};
|
||||
@@ -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>
|
||||
};
|
||||
@@ -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({
|
||||
DisplayName : 'RecentItems',
|
||||
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, i)=>{
|
||||
return <a href={brew.url} className='item' key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
|
||||
<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}
|
||||
/>;
|
||||
}
|
||||
};
|
||||
@@ -1,20 +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({
|
||||
displayName : 'RedditShareNavItem',
|
||||
getDefaultProps : function() {
|
||||
var RedditShare = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
title : '',
|
||||
sharedId : '',
|
||||
text : ''
|
||||
text : ''
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -25,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');
|
||||
@@ -38,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;
|
||||
@@ -1,151 +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 dedent = require('dedent-tabs').default;
|
||||
|
||||
const BrewItem = createClass({
|
||||
displayName : 'BrewItem',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {
|
||||
title : '',
|
||||
description : '',
|
||||
authors : [],
|
||||
stubbed : true
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
||||
.send()
|
||||
.end(function(err, res){
|
||||
location.reload();
|
||||
});
|
||||
},
|
||||
|
||||
renderDeleteBrewLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
return <a className='deleteLink' onClick={this.deleteBrew}>
|
||||
<i className='fas fa-trash-alt' title='Delete' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderEditLink : function(){
|
||||
if(!this.props.brew.editId) return;
|
||||
|
||||
let editLink = this.props.brew.editId;
|
||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
||||
editLink = this.props.brew.googleId + editLink;
|
||||
}
|
||||
|
||||
return <a className='editLink' href={`/edit/${editLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fas fa-pencil-alt' title='Edit' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderShareLink : function(){
|
||||
if(!this.props.brew.shareId) return;
|
||||
|
||||
let shareLink = this.props.brew.shareId;
|
||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
||||
shareLink = this.props.brew.googleId + shareLink;
|
||||
}
|
||||
|
||||
return <a className='shareLink' href={`/share/${shareLink}`} target='_blank' rel='noopener noreferrer'>
|
||||
<i className='fas fa-share-alt' title='Share' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderDownloadLink : function(){
|
||||
if(!this.props.brew.shareId) return;
|
||||
|
||||
let shareLink = this.props.brew.shareId;
|
||||
if(this.props.brew.googleId && !this.props.brew.stubbed) {
|
||||
shareLink = this.props.brew.googleId + shareLink;
|
||||
}
|
||||
|
||||
return <a className='downloadLink' href={`/download/${shareLink}`}>
|
||||
<i className='fas fa-download' title='Download' />
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderGoogleDriveIcon : function(){
|
||||
if(!this.props.brew.googleId) return;
|
||||
|
||||
return <span>
|
||||
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
|
||||
</span>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
const brew = this.props.brew;
|
||||
if(Array.isArray(brew.tags)) { // temporary fix until dud tags are cleaned
|
||||
brew.tags = brew.tags?.filter(tag => tag); //remove tags that are empty strings
|
||||
}
|
||||
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
return <div className='brewItem'>
|
||||
<div className='text'>
|
||||
<h2>{brew.title}</h2>
|
||||
<p className='description'>{brew.description}</p>
|
||||
</div>
|
||||
<hr />
|
||||
<div className='info'>
|
||||
|
||||
{brew.tags?.length ? <>
|
||||
<div className='brewTags' title={`Tags:\n${brew.tags.join('\n')}`}>
|
||||
<i className='fas fa-tags'/>
|
||||
{brew.tags.map((tag, idx)=>{
|
||||
let matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return <span className={matches[1]}>{matches[2]}</span>;
|
||||
})}
|
||||
</div>
|
||||
</> : <></>
|
||||
}
|
||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||
<i className='fas fa-user'/> {brew.authors?.join(', ')}
|
||||
</span>
|
||||
<br />
|
||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||
<i className='fas fa-eye'/> {brew.views}
|
||||
</span>
|
||||
{brew.pageCount &&
|
||||
<span title={`Page count: ${brew.pageCount}`}>
|
||||
<i className='far fa-file' /> {brew.pageCount}
|
||||
</span>
|
||||
}
|
||||
<span title={dedent`
|
||||
Created: ${moment(brew.createdAt).local().format(dateFormatString)}
|
||||
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
|
||||
<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;
|
||||
@@ -1,87 +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 2px 8px;
|
||||
padding-right : 15px;
|
||||
border : 1px solid #c9ad6a;
|
||||
border-radius : 5px;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
box-shadow : 0px 4px 5px 0px #333;
|
||||
background-color : #cab2802e;
|
||||
.text {
|
||||
min-height : 54px;
|
||||
h4{
|
||||
margin-bottom : 5px;
|
||||
font-size : 2.2em;
|
||||
}
|
||||
}
|
||||
.info{
|
||||
position: initial;
|
||||
bottom: 2px;
|
||||
font-family : ScalySansRemake;
|
||||
font-size : 1.2em;
|
||||
&>span{
|
||||
margin-right : 12px;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
}
|
||||
.brewTags span {
|
||||
background-color: #c8ac6e3b;
|
||||
margin: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid #c8ac6e;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
}
|
||||
&: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;
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
require('./listPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const moment = require('moment');
|
||||
|
||||
const BrewItem = require('./brewItem/brewItem.jsx');
|
||||
|
||||
const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
|
||||
|
||||
const DEFAULT_SORT_TYPE = 'alpha';
|
||||
const DEFAULT_SORT_DIR = 'asc';
|
||||
|
||||
const ListPage = createClass({
|
||||
displayName : 'ListPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brewCollection : [
|
||||
{
|
||||
title : '',
|
||||
class : '',
|
||||
brews : []
|
||||
}
|
||||
],
|
||||
navItems : <></>
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
// HIDE ALL GROUPS UNTIL LOADED
|
||||
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||
brewGroup.visible = false;
|
||||
return brewGroup;
|
||||
});
|
||||
|
||||
return {
|
||||
filterString : this.props.query?.filter || '',
|
||||
sortType : this.props.query?.sort || null,
|
||||
sortDir : this.props.query?.dir || null,
|
||||
query : this.props.query,
|
||||
brewCollection : brewCollection
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
// SAVE TO LOCAL STORAGE WHEN LEAVING PAGE
|
||||
window.onbeforeunload = this.saveToLocalStorage;
|
||||
|
||||
// LOAD FROM LOCAL STORAGE
|
||||
if(typeof window !== 'undefined') {
|
||||
const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
|
||||
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
|
||||
this.updateUrl(this.state.filterString, newSortType, newSortDir);
|
||||
|
||||
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
|
||||
brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
|
||||
return brewGroup;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
brewCollection : brewCollection,
|
||||
sortType : newSortType,
|
||||
sortDir : newSortDir
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
componentWillUnmount : function() {
|
||||
window.onbeforeunload = function(){};
|
||||
},
|
||||
|
||||
saveToLocalStorage : function() {
|
||||
this.state.brewCollection.map((brewGroup)=>{
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
|
||||
});
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
|
||||
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
|
||||
},
|
||||
|
||||
renderBrews : function(brews){
|
||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||
|
||||
return _.map(brews, (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' : moment(brew.createdAt).format(),
|
||||
'updated' : moment(brew.updatedAt).format(),
|
||||
'views' : brew.views,
|
||||
'latest' : moment(brew.lastViewed).format()
|
||||
};
|
||||
return mapping[this.state.sortType];
|
||||
},
|
||||
|
||||
handleSortOptionChange : function(event){
|
||||
this.updateUrl(this.state.filterString, event.target.value, this.state.sortDir);
|
||||
this.setState({
|
||||
sortType : event.target.value
|
||||
});
|
||||
},
|
||||
|
||||
handleSortDirChange : function(event){
|
||||
const newDir = this.state.sortDir == 'asc' ? 'desc' : 'asc';
|
||||
|
||||
this.updateUrl(this.state.filterString, this.state.sortType, newDir);
|
||||
this.setState({
|
||||
sortDir : newDir
|
||||
});
|
||||
},
|
||||
|
||||
renderSortOption : function(sortTitle, sortValue){
|
||||
return <td>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.handleSortOptionChange}
|
||||
className={`${(this.state.sortType == sortValue ? 'active' : '')}`}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
</td>;
|
||||
},
|
||||
|
||||
handleFilterTextChange : function(e){
|
||||
this.setState({
|
||||
filterString : e.target.value,
|
||||
});
|
||||
this.updateUrl(e.target.value, this.state.sortType, this.state.sortDir);
|
||||
return;
|
||||
},
|
||||
|
||||
updateUrl : function(filterTerm, sortType, sortDir){
|
||||
const url = new URL(window.location.href);
|
||||
const urlParams = new URLSearchParams(url.search);
|
||||
|
||||
urlParams.set('sort', sortType);
|
||||
urlParams.set('dir', sortDir);
|
||||
|
||||
if(!filterTerm)
|
||||
urlParams.delete('filter');
|
||||
else
|
||||
urlParams.set('filter', filterTerm);
|
||||
|
||||
url.search = urlParams;
|
||||
window.history.replaceState(null, null, url);
|
||||
},
|
||||
|
||||
renderFilterOption : function(){
|
||||
return <td>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
type='search'
|
||||
placeholder='filter title/description'
|
||||
onChange={this.handleFilterTextChange}
|
||||
value={this.state.filterString}
|
||||
/>
|
||||
</label>
|
||||
</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>
|
||||
{this.renderFilterOption()}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>;
|
||||
},
|
||||
|
||||
getSortedBrews : function(brews){
|
||||
const testString = _.deburr(this.state.filterString).toLowerCase();
|
||||
|
||||
brews = _.filter(brews, (brew)=>{
|
||||
const brewStrings = _.deburr([
|
||||
brew.title,
|
||||
brew.description,
|
||||
brew.tags].join('\n')
|
||||
.toLowerCase());
|
||||
|
||||
return brewStrings.includes(testString);
|
||||
});
|
||||
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
|
||||
},
|
||||
|
||||
toggleBrewCollectionState : function(brewGroupClass) {
|
||||
this.setState((prevState)=>({
|
||||
brewCollection : prevState.brewCollection.map(
|
||||
(brewGroup)=>brewGroup.class === brewGroupClass ? { ...brewGroup, visible: !brewGroup.visible } : brewGroup
|
||||
)
|
||||
}));
|
||||
},
|
||||
|
||||
renderBrewCollection : function(brewCollection){
|
||||
if(brewCollection == []) return <div className='brewCollection'>
|
||||
<h1>No Brews</h1>
|
||||
</div>;
|
||||
return _.map(brewCollection, (brewGroup, idx)=>{
|
||||
return <div key={idx} className={`brewCollection ${brewGroup.class ?? ''}`}>
|
||||
<h1 className={brewGroup.visible ? 'active' : 'inactive'} onClick={()=>{this.toggleBrewCollectionState(brewGroup.class);}}>{brewGroup.title || 'No Title'}</h1>
|
||||
{brewGroup.visible ? this.renderBrews(this.getSortedBrews(brewGroup.brews)) : <></>}
|
||||
</div>;
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='listPage sitePage'>
|
||||
<link href='/themes/5ePhb.style.css' rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='phb page'>
|
||||
{this.renderSortOptions()}
|
||||
{this.renderBrewCollection(this.state.brewCollection)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ListPage;
|
||||
@@ -1,99 +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;
|
||||
}
|
||||
.listPage{
|
||||
.content{
|
||||
overflow-y : overlay;
|
||||
.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;
|
||||
left : calc(50vw - 400px);
|
||||
border : 2px solid #58180D;
|
||||
width : 800px;
|
||||
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;
|
||||
i{
|
||||
padding-right : 5px
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h1 {
|
||||
cursor: pointer;
|
||||
&.active {
|
||||
color: #58180D;
|
||||
}
|
||||
&.inactive {
|
||||
color: #707070;
|
||||
|
||||
}
|
||||
&.active::before, &.inactive::before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
font-size: 0.6cm;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
&.active::before {
|
||||
content: '\f107';
|
||||
}
|
||||
&.inactive::before {
|
||||
content: '\f105';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,427 +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 HelpNavItem = require('../../navbar/help.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({
|
||||
displayName : 'EditPage',
|
||||
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 : { ...prevState.brew, text: text },
|
||||
isPending : true,
|
||||
htmlErrors : htmlErrors
|
||||
}), ()=>this.trySave());
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...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 : {
|
||||
...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);
|
||||
|
||||
const brew = this.state.brew;
|
||||
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
|
||||
const res = await request
|
||||
.put(`/api/update/${brew.editId}${params}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log('Error Updating Local Brew');
|
||||
this.setState({ errors: err });
|
||||
});
|
||||
|
||||
this.savedBrew = res.body;
|
||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...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>;
|
||||
request
|
||||
.put('/homebrew/api/update/' + this.props.brew.editId)
|
||||
.send({
|
||||
text : this.state.text,
|
||||
title : this.state.title
|
||||
})
|
||||
.end((err, res) => {
|
||||
this.savedBrew = res.body;
|
||||
this.setState({
|
||||
isPending : false,
|
||||
isSaving : false,
|
||||
lastUpdated : res.body.updatedAt
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.errors){
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} 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>;
|
||||
// }
|
||||
|
||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit our log in page to sign out
|
||||
and sign back in with Google,
|
||||
then try saving again!
|
||||
<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.stubbed ?
|
||||
this.state.brew.googleId + this.state.brew.shareId :
|
||||
this.state.brew.shareId;
|
||||
},
|
||||
|
||||
getRedditLink : function(){
|
||||
|
||||
const shareLink = this.processShareId();
|
||||
const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
|
||||
const title = `${this.props.brew.title} ${systems}`;
|
||||
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
|
||||
|
||||
**[Homebrewery Link](${global.config.publicUrl}/share/${shareLink})**`;
|
||||
|
||||
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title)}&text=${encodeURIComponent(text)}`;
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
const shareLink = this.processShareId();
|
||||
|
||||
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 />
|
||||
<HelpNavItem/>
|
||||
<Nav.dropdown>
|
||||
<Nav.item color='teal' icon='fas fa-share-alt'>
|
||||
share
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/share/${shareLink}`}>
|
||||
view
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.publicUrl}/share/${shareLink}`);}}>
|
||||
copy url
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
|
||||
post to reddit
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
<PrintLink shareId={this.processShareId()} />
|
||||
<RecentNavItem brew={this.state.brew} storageKey='edit' />
|
||||
<Account />
|
||||
<Nav.item newTab={true} href={'/homebrew/share/' + this.props.brew.shareId} color='teal' icon='fa-share-alt'>
|
||||
Share
|
||||
</Nav.item>
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<Nav.item color='red' icon='fa-trash' onClick={this.handleDelete}>
|
||||
Delete
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
</Navbar>;
|
||||
</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>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
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 />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.text} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ErrorPage;
|
||||
@@ -1,5 +0,0 @@
|
||||
.errorPage{
|
||||
.errorTitle{
|
||||
background-color: @orange;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,97 +1,69 @@
|
||||
require('./homePage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
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 HelpNavItem = require('../../navbar/help.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({
|
||||
displayName : 'HomePage',
|
||||
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((prevState)=>({
|
||||
brew : { ...prevState.brew, text: text }
|
||||
}));
|
||||
this.setState({
|
||||
text : text
|
||||
});
|
||||
},
|
||||
renderNavbar : function(){
|
||||
return <Navbar ver={this.props.ver}>
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<AccountNavItem />
|
||||
<PatreonNavItem />
|
||||
<Nav.item newTab={true} href='https://github.com/stolksdorf/naturalcrit/issues' color='red' icon='fa-bug'>
|
||||
report issue
|
||||
</Nav.item>
|
||||
<Nav.item newTab={true} href='/homebrew/changelog' color='purple' icon='fa-file-text-o'>
|
||||
Changelog
|
||||
</Nav.item>
|
||||
<Nav.item href='/homebrew/new' color='green' icon='fa-external-link'>
|
||||
New Brew
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
</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} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
|
||||
<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>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,43 +1,23 @@
|
||||
.homePage{
|
||||
position : relative;
|
||||
a.floatingNewButton{
|
||||
.animate(background-color);
|
||||
position : absolute;
|
||||
display : block;
|
||||
right : 70px;
|
||||
bottom : 50px;
|
||||
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 : 70px;
|
||||
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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
# How to Convert a Legacy Document to v3
|
||||
Here you will find a number of steps to guide you through converting a Legacy document into a Homebrewery v3 document.
|
||||
|
||||
**The first thing you'll want to do is switch the editor's rendering engine from `Legacy` to `v3`.** This will be the renderer we design features for moving forward.
|
||||
|
||||
There are some examples of Legacy code in the code pane if you need more context behind some of the changes.
|
||||
|
||||
**This document will evolve as users like yourself inform us of issues with it, or areas of conversion that it does not cover. _Please_ reach out if you have any suggestions for this document.**
|
||||
|
||||
## Simple Replacements
|
||||
To make your life a little easier with this section, a text editor like [VSCode](https://code.visualstudio.com/) or Notepad will help a lot.
|
||||
|
||||
The following table describes Legacy and other document elements and their Homebrewery counterparts. A simple find/replace should get these in working order.
|
||||
|
||||
| Legacy / Other | Homebrewery |
|
||||
|:----------------|:-----------------------------|
|
||||
| `\pagebreak` | `\page` |
|
||||
| `======` | `\page` |
|
||||
| `\pagebreaknum` | `{{pageNumber,auto}}\n\page` |
|
||||
| `@=====` | `{{pageNumber,auto}}\n\page` |
|
||||
| `\columnbreak` | `\column` |
|
||||
| `.phb` | `.page` |
|
||||
|
||||
## Classed or Styled Divs
|
||||
Anything that relies on the following syntax can be changed to the new Homebrewery v3 curly brace syntax:
|
||||
|
||||
```
|
||||
<div class="classTable wide">
|
||||
...
|
||||
</div>
|
||||
```
|
||||
:
|
||||
The above example is equivalent to the following in v3 syntax.
|
||||
|
||||
```
|
||||
{{classTable,wide
|
||||
...
|
||||
}}
|
||||
```
|
||||
:
|
||||
Some examples of this include class tables (as shown above), descriptive blocks, notes, and spell lists.
|
||||
|
||||
\column
|
||||
|
||||
## Margins and Padding
|
||||
Any manual margins and padding to push text down the page will likely need to be updated. Colons can be used on lines by themselves to push things down the page vertically if you'd rather not set pixel-perfect margins or padding.
|
||||
|
||||
## Notes
|
||||
|
||||
In Legacy, notes are denoted using markdown blockquote syntax. In Homebrewery v3, this is replaced by the curly brace syntax.
|
||||
|
||||
<!--
|
||||
> ##### Catchy Title
|
||||
> Useful Information
|
||||
-->
|
||||
|
||||
{{note
|
||||
##### Title
|
||||
Information
|
||||
}}
|
||||
|
||||
## Split Tables
|
||||
Split tables also use the curly brace syntax, as the new renderer can handle style values separately from class names.
|
||||
|
||||
<!--
|
||||
<div style='column-count:2'>
|
||||
|
||||
| d8 | Loot |
|
||||
|:---:|:-----------:|
|
||||
| 1 | 100gp |
|
||||
| 2 | 200gp |
|
||||
| 3 | 300gp |
|
||||
| 4 | 400gp |
|
||||
|
||||
| d8 | Loot |
|
||||
|:---:|:-----------:|
|
||||
| 5 | 500gp |
|
||||
| 6 | 600gp |
|
||||
| 7 | 700gp |
|
||||
| 8 | 1000gp |
|
||||
|
||||
</div>
|
||||
-->
|
||||
|
||||
##### 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 |
|
||||
}}
|
||||
|
||||
## Blockquotes
|
||||
Blockquotes are denoted by the `>` character at the beginning of the line. In Homebrewery's v3 renderer, they hold virtually no meaning and have no CSS styling. You are free to use blockquotes when styling your document or creating themes without needing to worry about your CSS affecting other parts of the document.
|
||||
|
||||
{{pageNumber,auto}}
|
||||
|
||||
\page
|
||||
|
||||
## Stat Blocks
|
||||
|
||||
There are pretty significant differences between stat blocks on the Legacy renderer and Homebrewery v3. This section contains a list of changes that will need to be made to update the stat block.
|
||||
|
||||
### Initial Changes
|
||||
You will want to **remove all leading** `___` that started the stat block in Legacy, and replace that with `{{monster` before the stat block, and `}}` after it.
|
||||
|
||||
**If you want a frame** around the stat block, you can add `,frame` to the curly brace definition.
|
||||
|
||||
**If the stat block was wide**, make sure to add `,wide` to the curly brace definition.
|
||||
|
||||
### Blockquotes
|
||||
The key difference is the lack of blockquotes. Legacy documents use the `>` symbol at the start of the line for each line in the stat block, and the v3 renderer does not. **You will want to remove all `>` characters at the beginning of all lines, and delete any leading spaces.**
|
||||
|
||||
### Lists
|
||||
The basic characteristics and advanced characteristics sections are not list elements in Homebrewery. You will want to **remove all `-` or `*` characters from the beginning of lines.**
|
||||
|
||||
### Spacing
|
||||
In order to have the correct spacing after removing the list elements, you will want to **add two colons between the name of each basic/advanced characteristic and its value.** _(see example in the code pane)_
|
||||
|
||||
Additionally, in the special traits and actions sections, you will want to add a colon at the beginning of each line that separates a trait/action from another, as seen below. **Any empty lines between special traits and actions should contain only a colon.** _(see example in the code pane)_
|
||||
|
||||
\column
|
||||
|
||||
{{margin-top:102px}}
|
||||
|
||||
<!--
|
||||
### Legacy/Other Document Example:
|
||||
___
|
||||
> ## Centaur
|
||||
> *Large Monstrosity, neutral good*
|
||||
>___
|
||||
> - **Armor Class** 12
|
||||
> - **Hit Points** 45(6d10 + 12)
|
||||
> - **Speed** 50ft.
|
||||
>___
|
||||
>|STR|DEX|CON|INT|WIS|CHA|
|
||||
>|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
>|18 (+4)|14 (+2)|14 (+2)|9 (-1)|13 (+1)|11 (+0)|
|
||||
>___
|
||||
> - **Skills** Athletics +6, Perception +3, Survival +3
|
||||
> - **Senses** passive Perception 13
|
||||
> - **Languages** Elvish, Sylvan
|
||||
> - **Challenge** 2 (450 XP)
|
||||
> ___
|
||||
> ***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||
>
|
||||
> ***Second Thing*** More details.
|
||||
>
|
||||
> ### Actions
|
||||
> ***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||
>
|
||||
> ***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||
>
|
||||
> ***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||
>
|
||||
> ***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||
-->
|
||||
|
||||
### Homebrewery v3 Example:
|
||||
|
||||
{{monster
|
||||
## Centaur
|
||||
*Large monstrosity, neutral good*
|
||||
___
|
||||
**Armor Class** :: 12
|
||||
**Hit Points** :: 45(6d10 + 12)
|
||||
**Speed** :: 50ft.
|
||||
___
|
||||
| STR | DEX | CON | INT | WIS | CHA |
|
||||
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
|
||||
|18 (+4)|14 (+2)|14 (+2)|9 (-1) |13 (+1)|11 (+0)|
|
||||
___
|
||||
**Skills** :: Athletics +6, Perception +3, Survival +3
|
||||
**Senses** :: passive Perception 13
|
||||
**Languages** :: Elvish, Sylvan
|
||||
**Challenge** :: 2 (450 XP)
|
||||
___
|
||||
***Charge.*** If the centaur moves at least 30 feet straight toward a target and then hits it with a pike attack on the same turn, the target takes an extra 10 (3d6) piercing damage.
|
||||
:
|
||||
***Second Thing*** More details.
|
||||
|
||||
### Actions
|
||||
***Multiattack.*** The centaur makes two attacks: one with its pike and one with its hooves or two with its longbow.
|
||||
:
|
||||
***Pike.*** *Melee Weapon Attack:* +6 to hit, reach 10 ft., one target. *Hit:* 9 (1d10 + 4) piercing damage.
|
||||
:
|
||||
***Hooves.*** *Melee Weapon Attack:* +6 to hit, reach 5 ft., one target. *Hit:* 11 (2d6 + 4) bludgeoning damage.
|
||||
:
|
||||
***Longbow.*** *Ranged Weapon Attack:* +4 to hit, range 150/600 ft., one target. *Hit:* 6 (1d8 + 2) piercing damage.
|
||||
}}
|
||||
|
||||
{{pageNumber,auto}}
|
||||
|
||||
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
```css
|
||||
.page #example + table td {
|
||||
border:1px dashed #00000030;
|
||||
}
|
||||
.page {
|
||||
padding-bottom : 1.1cm;
|
||||
}
|
||||
```
|
||||
|
||||
# The Homebrewery *V3*
|
||||
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. Note that not every button is visible on this demo page. Click New {{fas,fa-plus-square}} in the navbar above to start brewing with all the features!
|
||||
|
||||
### Editing and Sharing
|
||||
When you create your own homebrew, you will be given a *edit url* and a *share url*.
|
||||
|
||||
Any changes you make while on the *edit url* 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.
|
||||
|
||||
{{note
|
||||
##### PDF Creation
|
||||
PDF Printing works best in Google 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 **PRINT → {{far,fa-file}} A4 Pagesize** 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 **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
||||
}}
|
||||
|
||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||
|
||||
{{artist,bottom:160px,left:100px
|
||||
##### Homebrew Mug
|
||||
[naturalcrit](https://homebrew.naturalcrit.com)
|
||||
}}
|
||||
|
||||
{{pageNumber 1}}
|
||||
{{footnote PART 1 | FANCINESS}}
|
||||
|
||||
\column
|
||||
|
||||
## New in V3.0.0
|
||||
We've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like `div` and `span` in most cases. No raw HTML tags should be needed in a brew (*but can still be used if you insist*).
|
||||
|
||||
Much of the syntax and styling has changed in V3, so converting a Legacy brew to V3 (or vice-versa) will require tweaking your document. *However*, all brews made prior to the release of v3.0.0 will still render normally, and you may switch between the "Legacy" brew renderer and the newer "V3" renderer via the {{fa,fa-info-circle}} **Properties** button on your brew at any time.
|
||||
|
||||
Scroll down to the next page for a brief summary of the changes and new features available in V3!
|
||||
|
||||
#### New Things All The Time!
|
||||
Check out the latest updates in the full changelog [here](/changelog).
|
||||
|
||||
### Helping out
|
||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/Naturalcrit) 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?
|
||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
||||
|
||||
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
||||
|
||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
||||
|
||||
### 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 codebase 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 me in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
### More Homebrew Resources
|
||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||
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/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
{{position:absolute;top:20px;right:20px;width:auto
|
||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things' style='color: black;'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||
}}
|
||||
|
||||
\page
|
||||
|
||||
## Markdown+
|
||||
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||
|
||||
In version 3.0.0, with a goal of adding maximum flexibility without users resorting to complex HTML to accomplish simple tasks, Homebrewery provides an extended verision of Markdown with additional syntax.
|
||||
**You can enable V3 via the {{fa,fa-info-circle}} Properties button!**
|
||||
|
||||
### Curly Brackets
|
||||
The biggest change in V3 is the replacement of `<span></span>` and `<div></div>` with `{{ }}` for a cleaner custom formatting. Inline spans and block elements can be created and given ID's and Classes, as well as css properties, each of which are comma separated with no spaces. Use double quotes if a value requires spaces. Spans and Blocks start the same:
|
||||
|
||||
#### Span
|
||||
My favorite author is {{pen,#author,color:orange,font-family:"trebuchet ms" Brandon Sanderson}}. The orange text has a class of `pen`, an id of `author`, is colored orange, and given a new font. The first space outside of quotes marks the beginning of the content.
|
||||
|
||||
|
||||
#### Block
|
||||
{{purple,#book,text-align:center,background:#aa88aa55
|
||||
My favorite book is Wheel of Time. This block has a class of `purple`, an id of `book`, and centered text with a colored background. The opening and closing brackets are on lines separate from the block contents.
|
||||
}}
|
||||
|
||||
#### Injection
|
||||
For any element not inside a span or block, you can *inject* attributes using the same syntax but with single brackets in a single line immediately after the element.
|
||||
|
||||
Inline elements like *italics* {color:#D35400} or images require the injection on the same line.
|
||||
|
||||
Block elements like headers require the injection to start on the line immediately following.
|
||||
|
||||
##### A Purple Header
|
||||
{color:purple,text-align:center}
|
||||
|
||||
\* *this does not currently work for tables yet*
|
||||
|
||||
### Vertical Spacing
|
||||
A blank line can be achieved with a run of one or more `:` alone on a line. More `:`'s will create more space.
|
||||
|
||||
::
|
||||
|
||||
Much nicer than `<br><br><br><br><br>`
|
||||
|
||||
### Definition Lists
|
||||
**Example** :: V3 uses HTML *definition lists* to create "lists" with hanging indents.
|
||||
|
||||
### Column Breaks
|
||||
Column and page breaks with `\column` and `\page`.
|
||||
|
||||
\column
|
||||
|
||||
### Tables
|
||||
Tables now allow column & row spanning between cells. This is included in some updated snippets, but a simplified example is given below.
|
||||
|
||||
A cell can be spanned across columns by grouping multiple pipe `|` characters at the end of a cell.
|
||||
|
||||
Row spanning is achieved by adding a `^` at the end of a cell just before the `|`.
|
||||
|
||||
These can be combined to span a cell across both columns and rows. Cells must have the same colspan if they are to be rowspan'd.
|
||||
|
||||
##### Example
|
||||
| Head A | Spanned Header ||
|
||||
| Head B | Head C | Head D |
|
||||
|:-------|:------:|:------:|
|
||||
| 1A | 1B | 1C |
|
||||
| 2A ^| 2B | 2C |
|
||||
| 3A ^| 3B 3C ||
|
||||
| 4A | 4B 4C^||
|
||||
| 5A ^| 5B | 5C |
|
||||
| 6A | 6B ^| 6C |
|
||||
|
||||
## Images
|
||||
Images must be hosted online somewhere, like [Imgur](https://www.imgur.com). You use the address to that image to reference it in your brew\*.
|
||||
|
||||
Using *Curly Injection* you can assign an id, classes, or inline CSS properties to the Markdown image syntax.
|
||||
|
||||
 {width:100px,border:"2px solid",border-radius:10px}
|
||||
|
||||
\* *When using Imgur-hosted images, use the "direct link", which can be found when you click into your image in the Imgur interace.*
|
||||
|
||||
## Snippets
|
||||
Homebrewery comes with a series of *code snippets* found at the top of the editor pane that make it easy to create brews as quickly as possible. Just set your cursor where you want the code to appear in the editor pane, choose a snippet, and make the adjustments you need.
|
||||
|
||||
## Style Editor Panel
|
||||
{{fa,fa-paint-brush}} Technically released prior to v3 but still new to many users, check out the new **Style Editor** located on the right side of the Snippet bar. This editor accepts CSS for styling without requiring `<style>` tags-- anything that would have gone inside style tags before can now be placed here, and snippets that insert CSS styles are now located on that tab.
|
||||
|
||||
{{pageNumber 2}}
|
||||
{{footnote PART 2 | BORING STUFF}}
|
||||
107
client/homebrew/pages/homePage/welcome_msg.txt
Normal file
107
client/homebrew/pages/homePage/welcome_msg.txt
Normal file
@@ -0,0 +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.
|
||||
|
||||
|
||||
|
||||
|
||||
#### Features
|
||||
* Monster Stat Blocks
|
||||
* Full class tables
|
||||
* Notes and Tables
|
||||
* Images
|
||||
* Page numbering and footers
|
||||
* Vertical spacing, column breaks, and multiple pages
|
||||
|
||||
|
||||
|
||||
### Editing and Sharing
|
||||
When you create your own homebrew you will be given a *edit url* and a *share url*. Any changes you make will be automatically saved to the database within a few seconds. Anyone with the edit url will be able to make edits to your homebrew. So be careful about who you share it with.
|
||||
|
||||
Anyone with the *share url* will be able to access a read-only version of your homebrew.
|
||||
|
||||
## Helping out
|
||||
Like this tool? Want to buy me a beer? [Head here](https://www.patreon.com/stolksdorf) to help me keep the servers running.
|
||||
|
||||
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
|
||||
|
||||
### Bugs, Issues, Suggestions?
|
||||
Have an idea of how to make The Homebrewery better? Or did you find something that wasn't quite right? Head [here](https://github.com/stolksdorf/NaturalCrit/issues/new) and let me know!.
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## New Things in v2.0.0!
|
||||
What's new in the latest update? Check out the full changelog [here](/homebrew/changelog)
|
||||
|
||||
* **A whole new look** The site has been re-built from the ground up!
|
||||
* **Better editor and Split Pane** Syntax highlighting will make writing your brews even easier, and now you can customize how large your editor is.
|
||||
* **More reliable rendering** Lots of work has been put into making the rendering more reliable, not just for web, but also for PDFs
|
||||
* **PDF Printing on Chrome** You don't need to use Chrome Canary anymore!
|
||||
* ** Performance Improvements** The site should load faster, save faster, and render large brews *much* faster.
|
||||
* **Patreon page** If you like this tool and want to show some thanks you can [head here](https://www.patreon.com/stolksdorf).
|
||||
|
||||
>##### PDF Exporting
|
||||
> After clicking the "Print" item in the navbar a new page will open and a print dialog will pop-up
|
||||
> * Set the **Destination** to "Save as PDF"
|
||||
> * Set **Paper Size** to "Letter"
|
||||
> * If you are printing on A4 paper, make sure to have the "A4 page size snippet" in your brew
|
||||
> * In **Options** make sure "Background Images" is selected.
|
||||
> * Hit print and enjoy! You're done!
|
||||
>
|
||||
> If you want to save ink or have a monochrome printer, add the **Ink Friendly** snippet to your brew before you print
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<img src='http://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:50px;right:30px;width:280px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
|
||||
|
||||
|
||||
\page
|
||||
|
||||
# Appendix
|
||||
|
||||
### Not quite Markdown
|
||||
Although the Homebrewery uses Markdown, to get all the styling features from the PHB, we had to get a little creative. Some base HTML elements are not used as expected and I've had to include a few new keywords.
|
||||
|
||||
___
|
||||
* **Horizontal Rules** are generally used to *modify* existing elements into a different style. For example, a horizontal rule before a blockquote will give it the style of a Monster Stat Block instead of a note.
|
||||
* **New Pages** are controlled by the author. It's impossible for the site to detect when the end of a page is reached, so indicate you'd like to start a new page, use the new page snippet to get the syntax.
|
||||
* **Code Blocks** are used only to indicate column breaks. Since they don't allow for styling within them, they weren't that useful to use.
|
||||
* **HTML** can be used to get *just* the right look for your homebrew. I've included some examples in the snippet icons above the editor.
|
||||
|
||||
|
||||
|
||||
### Images
|
||||
Images can be included 'inline' with the text using Markdown-style images. However for background images more control is needed.
|
||||
|
||||
Background images should be included as HTML-style img tags. Using inline CSS you can precisely position your image where you'd like it to be. I have added both a inflow image snippet and a background image snippet to give you exmaples of how to do it.
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
### Legal Junk
|
||||
You are free to use The Homebrewery is any way that you want, except for claiming that you made it yourself. If you wish to sell or in some way gain profit for what's created on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
### Crediting Me
|
||||
If you'd like to credit The Homebrewery in your brew, I'd be flattered! Just reference that you made it with The Homebrewery.
|
||||
|
||||
|
||||
|
||||
<div class='pageNumber'>2</div>
|
||||
<div class='footnote'>PART 2 | BORING STUFF</div>
|
||||
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
# The Homebrewery
|
||||
|
||||
Welcome traveler from an antique land. Please sit and tell us of what you have seen. The unheard of monsters, who slither and bite. Tell us of the wondrous items and and artifacts you have found, their mysteries yet to be unlocked. Of the vexing vocations and surprising skills you have seen.
|
||||
|
||||
### Homebrew D&D made easy
|
||||
The Homebrewery makes the creation and sharing of authentic looking Fifth-Edition homebrews easy. It uses [Markdown](https://help.github.com/articles/markdown-basics/) with a little CSS magic to make your brews come to life.
|
||||
|
||||
**Try it!** Simply edit the text on the left and watch it *update live* on the right.
|
||||
|
||||
|
||||
|
||||
### 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/Naturalcrit) 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
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## V3.0.0 Released!
|
||||
With the latest major update to *The Homebrewery* we've implemented an extended Markdown-like syntax for block and span elements, plus a few other changes, eliminating the need for HTML tags like **div** and **span** in most cases. No raw HTML tags should be needed in a brew, and going forward, raw HTML will no longer receive debugging support (*but can still be used if you insist*).
|
||||
|
||||
**You can enable V3 via the <span class="fa fa-info-circle" style="text-indent:0"></span> Properties button!**
|
||||
|
||||
## New Things All The Time!
|
||||
What's new in the latest update? Check out the full changelog [here](/changelog)
|
||||
|
||||
### Bugs, Issues, Suggestions?
|
||||
Take a quick look at our [Frequently Asked Questions page](/faq) to see if your question has a handy answer.
|
||||
|
||||
Need help getting started or just the right look for your brew? Head to [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) and let us know!
|
||||
|
||||
Have an idea to make The Homebrewery better? Or did you find something that wasn't quite right? Check out the [GitHub Repo](https://github.com/naturalcrit/homebrewery/) to report technical issues.
|
||||
|
||||
### Legal Junk
|
||||
The Homebrewery is licensed using the [MIT License](https://github.com/naturalcrit/homebrewery/blob/master/license). This means you are free to use The Homebrewery codebase 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 you make on this site, it's your responsibility to ensure you have the proper licenses/rights for any images or resources used.
|
||||
|
||||
### More Resources
|
||||
<a href='https://discord.gg/by3deKx' target='_blank'><img src='/assets/discordOfManyThings.svg' alt='Discord of Many Things Logo' title='Discord of Many Things Logo' style='width:50px; float: right; padding-left: 10px;'/></a>
|
||||
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/wiki/resources). The <a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'>Discord of Many Things</a> is another great resource to connect with fellow homebrewers for help and feedback.
|
||||
|
||||
<img src='https://i.imgur.com/hMna6G0.png' style='position:absolute;bottom:40px;right:30px;width:280px' />
|
||||
|
||||
<div class='pageNumber'>1</div>
|
||||
<div class='footnote'>PART 1 | FANCINESS</div>
|
||||
|
||||
<div style='position: absolute; top: 20px; right: 20px;'>
|
||||
<a href='https://discord.gg/by3deKx' target='_blank' title='Discord of Many Things'><img src='/assets/discord.png' style='height:30px'/></a>
|
||||
<a href='https://github.com/naturalcrit/homebrewery' target='_blank' title='Github' style='color: black; padding-left: 5px;'><img src='/assets/github.png' style='height:30px'/></a>
|
||||
<a href='https://patreon.com/NaturalCrit' target='_blank' title='Patreon' style='color: black; padding-left: 5px;'><img src='/assets/patreon.png' style='height:30px'/></a>
|
||||
<a href='https://www.reddit.com/r/homebrewery/' target='_blank' title='Reddit' style='color: black; padding-left: 5px;'><img src='/assets/reddit.png' style='height:30px'/></a>
|
||||
</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>
|
||||
@@ -1,296 +1,129 @@
|
||||
/*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 HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
var Nav = require('naturalcrit/nav/nav.jsx');
|
||||
var Navbar = require('../../navbar/navbar.jsx');
|
||||
var EditTitle = require('../../navbar/editTitle.navitem.jsx');
|
||||
|
||||
|
||||
const NewPage = createClass({
|
||||
displayName : 'NewPage',
|
||||
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,
|
||||
title : '',
|
||||
description : '',
|
||||
renderer : 'V3'
|
||||
}
|
||||
title : 'My Awesome Brew v99',
|
||||
text: '',
|
||||
isSaving : false
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
let brew = this.props.brew;
|
||||
|
||||
if(this.props.brew.shareId) {
|
||||
brew = {
|
||||
text : brew.text ?? '',
|
||||
style : brew.style ?? undefined,
|
||||
title : brew.title ?? '',
|
||||
description : brew.description ?? '',
|
||||
renderer : brew.renderer ?? 'legacy'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
brew : brew,
|
||||
isSaving : false,
|
||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||
errors : null,
|
||||
htmlErrors : Markdown.validate(brew.text)
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
|
||||
const brew = this.state.brew;
|
||||
|
||||
if(!this.props.brew.shareId && typeof window !== 'undefined') { //Load from localStorage if in client browser
|
||||
const brewStorage = localStorage.getItem(BREWKEY);
|
||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
|
||||
brew.text = brewStorage ?? brew.text;
|
||||
brew.style = styleStorage ?? brew.style;
|
||||
// brew.title = metaStorage?.title || this.state.brew.title;
|
||||
// brew.description = metaStorage?.description || this.state.brew.description;
|
||||
brew.renderer = metaStorage?.renderer ?? brew.renderer;
|
||||
|
||||
componentDidMount: function() {
|
||||
var storage = localStorage.getItem(KEY);
|
||||
if(storage){
|
||||
this.setState({
|
||||
brew : brew
|
||||
});
|
||||
text : storage
|
||||
})
|
||||
}
|
||||
|
||||
localStorage.setItem(BREWKEY, brew.text);
|
||||
localStorage.setItem(STYLEKEY, brew.style);
|
||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer }));
|
||||
},
|
||||
componentWillUnmount : function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
window.onbeforeunload = (e)=>{
|
||||
if(this.state.text == '') return;
|
||||
return "Your homebrew isn't saved. Are you sure you want to leave?";
|
||||
};
|
||||
},
|
||||
|
||||
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();
|
||||
},
|
||||
|
||||
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 : { ...prevState.brew, text: text },
|
||||
htmlErrors : htmlErrors
|
||||
}));
|
||||
localStorage.setItem(BREWKEY, text);
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style },
|
||||
}));
|
||||
localStorage.setItem(STYLEKEY, style);
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, ...metadata },
|
||||
}));
|
||||
localStorage.setItem(METAKEY, JSON.stringify({
|
||||
// 'title' : this.state.brew.title,
|
||||
// 'description' : this.state.brew.description,
|
||||
'renderer' : this.state.brew.renderer
|
||||
}));
|
||||
},
|
||||
|
||||
clearErrors : function(){
|
||||
handleTitleChange : function(title){
|
||||
this.setState({
|
||||
errors : null,
|
||||
isSaving : false
|
||||
|
||||
title : title
|
||||
});
|
||||
},
|
||||
|
||||
save : async function(){
|
||||
handleTextChange : function(text){
|
||||
this.setState({
|
||||
text : text
|
||||
});
|
||||
localStorage.setItem(KEY, text);
|
||||
},
|
||||
|
||||
handleSave : function(){
|
||||
this.setState({
|
||||
isSaving : true
|
||||
});
|
||||
request.post('/homebrew/api')
|
||||
.send({
|
||||
title : this.state.title,
|
||||
text : this.state.text
|
||||
})
|
||||
.end((err, res)=>{
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||
|
||||
const res = await request
|
||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||
.send(brew)
|
||||
.catch((err)=>{
|
||||
console.log(err);
|
||||
this.setState({ isSaving: false, errors: err });
|
||||
});
|
||||
if(!res) return;
|
||||
|
||||
brew = res.body;
|
||||
localStorage.removeItem(BREWKEY);
|
||||
localStorage.removeItem(STYLEKEY);
|
||||
localStorage.removeItem(METAKEY);
|
||||
window.location = `/edit/${brew.editId}`;
|
||||
if(err){
|
||||
this.setState({
|
||||
isSaving : false
|
||||
});
|
||||
return;
|
||||
}
|
||||
window.onbeforeunload = function(){};
|
||||
var brew = res.body;
|
||||
localStorage.removeItem(KEY);
|
||||
window.location = '/homebrew/edit/' + brew.editId;
|
||||
})
|
||||
},
|
||||
|
||||
renderSaveButton : function(){
|
||||
if(this.state.errors){
|
||||
let errMsg = '';
|
||||
try {
|
||||
errMsg += `${this.state.errors.toString()}\n\n`;
|
||||
errMsg += `\`\`\`\n${this.state.errors.stack}\n`;
|
||||
errMsg += `${JSON.stringify(this.state.errors.response.error, null, ' ')}\n\`\`\``;
|
||||
console.log(errMsg);
|
||||
} 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>;
|
||||
// }
|
||||
|
||||
if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||
Oops!
|
||||
<div className='errorContainer' onClick={this.clearErrors}>
|
||||
Looks like your Google credentials have
|
||||
expired! Visit our log in page to sign out
|
||||
and sign back in with Google,
|
||||
then try saving again!
|
||||
<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 icon='fas fa-spinner fa-spin' className='save'>
|
||||
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
|
||||
save...
|
||||
</Nav.item>;
|
||||
} else {
|
||||
return <Nav.item icon='fas fa-save' className='save' onClick={this.save}>
|
||||
</Nav.item>
|
||||
}else{
|
||||
return <Nav.item icon='fa-save' className='saveButton' onClick={this.handleSave}>
|
||||
save
|
||||
</Nav.item>;
|
||||
</Nav.item>
|
||||
}
|
||||
},
|
||||
|
||||
print : function(){
|
||||
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()}
|
||||
<HelpNavItem />
|
||||
<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>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,82 +1,10 @@
|
||||
.newPage{
|
||||
.navItem.save{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
&.error{
|
||||
position : relative;
|
||||
background-color : @red;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.newPage{
|
||||
|
||||
.saveButton{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,97 +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 BREWKEY = 'homebrewery-new';
|
||||
const STYLEKEY = 'homebrewery-new-style';
|
||||
const METAKEY = 'homebrewery-new-meta';
|
||||
|
||||
const PrintPage = createClass({
|
||||
displayName : 'PrintPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
query : {},
|
||||
brew : {
|
||||
text : '',
|
||||
style : '',
|
||||
renderer : 'legacy'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState : function() {
|
||||
return {
|
||||
brew : {
|
||||
text : this.props.brew.text || '',
|
||||
style : this.props.brew.style || undefined,
|
||||
renderer : this.props.brew.renderer || 'legacy'
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : function() {
|
||||
if(this.props.query.local == 'print'){
|
||||
const brewStorage = localStorage.getItem(BREWKEY);
|
||||
const styleStorage = localStorage.getItem(STYLEKEY);
|
||||
const metaStorage = JSON.parse(localStorage.getItem(METAKEY));
|
||||
|
||||
this.setState((prevState, prevProps)=>{
|
||||
return {
|
||||
brew : {
|
||||
text : brewStorage,
|
||||
style : styleStorage,
|
||||
renderer : metaStorage?.renderer || 'legacy'
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if(this.props.query.dialog) window.print();
|
||||
},
|
||||
|
||||
renderStyle : function() {
|
||||
if(!this.state.brew.style) return;
|
||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.state.brew.style} </style>` }} />;
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
if(this.state.brew.renderer == 'legacy') {
|
||||
return _.map(this.state.brew.text.split('\\page'), (pageText, index)=>{
|
||||
return <div
|
||||
className='phb page'
|
||||
id={`p${index + 1}`}
|
||||
dangerouslySetInnerHTML={{ __html: MarkdownLegacy.render(pageText) }}
|
||||
key={index} />;
|
||||
});
|
||||
} else {
|
||||
return _.map(this.state.brew.text.split(/^\\page$/gm), (pageText, index)=>{
|
||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||
return (
|
||||
<div className='page' id={`p${index + 1}`} key={index} >
|
||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<link href={`${this.state.brew.renderer == 'legacy' ? '/themes/5ePhbLegacy.style.css' : '/themes/5ePhb.style.css'}`} rel='stylesheet'/>
|
||||
{/* Apply CSS from Style tab */}
|
||||
{this.renderStyle()}
|
||||
<div className='pages' ref='pages'>
|
||||
{this.renderPages()}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PrintPage;
|
||||
@@ -1,3 +0,0 @@
|
||||
.printPage{
|
||||
|
||||
}
|
||||
@@ -1,94 +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({
|
||||
displayName : 'SharePage',
|
||||
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
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
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.processShareId()}?dialog=true`, '_blank').focus();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
|
||||
processShareId : function() {
|
||||
return this.props.brew.googleId && !this.props.brew.stubbed ?
|
||||
this.props.brew.googleId + this.props.brew.shareId :
|
||||
this.props.brew.shareId;
|
||||
},
|
||||
|
||||
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.dropdown>
|
||||
<Nav.item color='red' icon='fas fa-code'>
|
||||
source
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/source/${this.processShareId()}`}>
|
||||
view
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/download/${this.processShareId()}`}>
|
||||
download
|
||||
</Nav.item>
|
||||
<Nav.item color='blue' href={`/new/${this.processShareId()}`}>
|
||||
clone to new
|
||||
</Nav.item>
|
||||
</Nav.dropdown>
|
||||
</>}
|
||||
<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>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
.sharePage{
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
}
|
||||
}
|
||||
.sharePage{
|
||||
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||
|
||||
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 HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||
|
||||
const UserPage = createClass({
|
||||
displayName : 'UserPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
username : '',
|
||||
brews : [],
|
||||
query : ''
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `’` : `’s`);
|
||||
|
||||
const brews = _.groupBy(this.props.brews, (brew)=>{
|
||||
return (brew.published ? 'published' : 'private');
|
||||
});
|
||||
|
||||
const brewCollection = [
|
||||
{
|
||||
title : `${usernameWithS} published brews`,
|
||||
class : 'published',
|
||||
brews : brews.published
|
||||
}
|
||||
];
|
||||
if(this.props.username == global.account?.username){
|
||||
brewCollection.push(
|
||||
{
|
||||
title : `${usernameWithS} unpublished brews`,
|
||||
class : 'unpublished',
|
||||
brews : brews.private
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
brewCollection : brewCollection
|
||||
};
|
||||
},
|
||||
|
||||
navItems : function() {
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<NewBrew />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UserPage;
|
||||
BIN
client/homebrew/phbStyle/border.png
Normal file
BIN
client/homebrew/phbStyle/border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 327 B |
BIN
client/homebrew/phbStyle/note_border.png
Normal file
BIN
client/homebrew/phbStyle/note_border.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 530 B |
6
client/homebrew/phbStyle/phb.assets.less
Normal file
6
client/homebrew/phbStyle/phb.assets.less
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user