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-
|
||||
|
||||
- run: sudo npm install -g npm@8.10.0
|
||||
- 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
@@ -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 +0,0 @@
|
||||
package-lock.json binary
|
||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +0,0 @@
|
||||
contact_links:
|
||||
- name: /r/Homebrewery Subreddit
|
||||
url: https://www.reddit.com/r/homebrewery
|
||||
about: The Homebrewery community on Reddit!
|
||||
- name: Discord of Many Things
|
||||
url: https://discord.gg/domt
|
||||
about: "Join the conversation in the #formatting channel on DoMT!"
|
||||
17
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: Feature Request
|
||||
description: Have an idea to improve the Homebrewery? Let us know!
|
||||
labels: ["feature request"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "We'd love to hear your idea! Please be sure to [search the current Issues](https://github.com/naturalcrit/homebrewery/issues) for any duplicate requests."
|
||||
- type: textarea
|
||||
id: user-request
|
||||
attributes:
|
||||
label: "Your idea:"
|
||||
description: The best feature requests provide an explanation of the current issue and then an explanation of how it could be improved. Screenshots/images can be pasted right in as well!
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "Please be sure to search for any close matches to your request in the GitHub Issues tracker before opening a new request, thanks!"
|
||||
55
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: General Issue
|
||||
description: Report an issue unrelated to Saving
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please include as much information as possible.
|
||||
- type: dropdown
|
||||
id: renderer
|
||||
attributes:
|
||||
label: Renderer
|
||||
description: Which renderer does this issue occur on? If you are unsure, you can check the renderer in the Properties Editor (click the "i" in the Snippet Menu bar above the editor).
|
||||
options:
|
||||
- v3
|
||||
- Legacy
|
||||
- Both
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: browser
|
||||
attributes:
|
||||
label: Browser
|
||||
description: Which browser were you using when the issue occurred?
|
||||
options:
|
||||
- Chrome
|
||||
- Firefox
|
||||
- Edge
|
||||
- Safari
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: operating-system
|
||||
attributes:
|
||||
label: Operating System
|
||||
description: Which OS were you using when the issue occurred?
|
||||
options:
|
||||
- Windows
|
||||
- MacOS
|
||||
- Linux
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: user-description
|
||||
attributes:
|
||||
label: "What happened?"
|
||||
description: Please include any steps you took leading up to the issue and if you can reproduce it. Let us know what you expected to happen, and what did happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: code
|
||||
attributes:
|
||||
label: Code
|
||||
description: Paste in any relevant code snippet below.
|
||||
render: gfm
|
||||
26
.github/ISSUE_TEMPLATE/save_issue.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Saving Issue
|
||||
description: Report an issue Saving
|
||||
labels: ["Saving"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Woops, sorry there was an issue Saving. Please add any detail you can to this report and check back soon!
|
||||
- type: textarea
|
||||
id: error-code
|
||||
attributes:
|
||||
label: Error Code
|
||||
render: shell
|
||||
- type: textarea
|
||||
id: user-description
|
||||
attributes:
|
||||
label: "Your description of what happened:"
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for the report. Here are some steps that may help in the meantime:
|
||||
1. Refreshing your Google credentials in Homebrewery by signing out, and back in (they expire after one year).
|
||||
2. Waiting a few minutes and trying again - sometimes there is just a momentary blip in the server.
|
||||
3. Check the Issues in Github or the /r/homebrewery subreddit to see if others are experiencing the same issue.
|
||||
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
@@ -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** :
|
||||
|
||||
30
.gitignore
vendored
@@ -1,14 +1,16 @@
|
||||
node_modules
|
||||
storage
|
||||
.idea
|
||||
*.swp
|
||||
|
||||
*.log
|
||||
build/*
|
||||
config/local.*
|
||||
config/docker.*
|
||||
|
||||
todo.md
|
||||
startDB.bat
|
||||
startMViewer.bat
|
||||
.vscode
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
#Ignore our built files
|
||||
build/*
|
||||
architecture.json
|
||||
|
||||
# Ignore sensitive stuff
|
||||
/config/*
|
||||
!/config/default.json
|
||||
|
||||
node_modules
|
||||
storage
|
||||
.idea
|
||||
*.swp
|
||||
|
||||
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
@@ -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)
|
||||
|
||||
1197
changelog.md
@@ -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
@@ -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
@@ -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,105 +1,45 @@
|
||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||
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 Themes = require('themes/themes.json');
|
||||
|
||||
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',
|
||||
theme : '5ePHB',
|
||||
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;
|
||||
@@ -108,131 +48,45 @@ const BrewRenderer = createClass({
|
||||
},
|
||||
|
||||
renderPageInfo : function(){
|
||||
return <div className='pageInfo' ref='main'>
|
||||
<div>
|
||||
{this.props.renderer}
|
||||
</div>
|
||||
<div>
|
||||
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
|
||||
</div>
|
||||
</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 is enabled, because your brew is so large. May affect 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.
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
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={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/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,46 +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;
|
||||
background-color : #333;
|
||||
font-size : 10px;
|
||||
font-weight : 800;
|
||||
color : white;
|
||||
div {
|
||||
display: inline-block;
|
||||
padding : 8px 10px;
|
||||
&:not(:last-child){
|
||||
border-right: 1px solid #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
.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,332 +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');
|
||||
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
const DEFAULT_STYLE_TEXT = dedent`
|
||||
/*=======--- Example CSS styling ---=======*/
|
||||
/* Any CSS here will apply to your document! */
|
||||
|
||||
.myExampleClass {
|
||||
color: black;
|
||||
}`;
|
||||
var CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
var Snippets = require('./snippets/snippets.js');
|
||||
|
||||
|
||||
const Editor = createClass({
|
||||
displayName : 'Editor',
|
||||
getDefaultProps : function() {
|
||||
var splice = function(str, index, inject){
|
||||
return str.slice(0, index) + inject + str.slice(index);
|
||||
};
|
||||
var execute = function(val){
|
||||
if(_.isFunction(val)) return val();
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
var 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){
|
||||
this.refs.codeEditor?.injectText(injectText, false);
|
||||
},
|
||||
|
||||
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 injectors {style}
|
||||
if(line.includes('{') && line.includes('}')){
|
||||
const regex = /(?<!{){(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
||||
let match;
|
||||
while ((match = regex.exec(line)) != null) {
|
||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'injection' });
|
||||
}
|
||||
}
|
||||
// Highlight inline spans {{content}}
|
||||
if(line.includes('{{') && line.includes('}}')){
|
||||
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/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]*)*))\1 *$|^ *}}$/);
|
||||
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}
|
||||
theme={this.props.brew.theme}
|
||||
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,65 +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;
|
||||
}
|
||||
.injection{
|
||||
color : green;
|
||||
font-weight : bold;
|
||||
}
|
||||
}
|
||||
|
||||
.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,310 +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 Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
const validations = require('./validations.js')
|
||||
|
||||
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 : '',
|
||||
thumbnail : '',
|
||||
tags : [],
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : [],
|
||||
renderer : 'legacy',
|
||||
theme : '5ePHB'
|
||||
},
|
||||
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){
|
||||
e.persist();
|
||||
|
||||
// load validation rules, and check input value against them
|
||||
const inputRules = validations[name] ?? [];
|
||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||
|
||||
// if no validation rules, save to props
|
||||
if(validationErr.length === 0){
|
||||
e.target.setCustomValidity('');
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
[name] : e.target.value
|
||||
});
|
||||
} else {
|
||||
// if validation issues, display built-in browser error popup with each error.
|
||||
console.log(validationErr);
|
||||
const errMessage = validationErr.map((err)=>{
|
||||
return `- ${err}`;
|
||||
}).join('\n');
|
||||
e.target.setCustomValidity(errMessage);
|
||||
e.target.reportValidity();
|
||||
};
|
||||
},
|
||||
|
||||
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;
|
||||
if(renderer == 'legacy')
|
||||
this.props.metadata.theme = '5ePHB';
|
||||
}
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
handlePublish : function(val){
|
||||
this.props.onChange({
|
||||
...this.props.metadata,
|
||||
published : val
|
||||
});
|
||||
},
|
||||
|
||||
handleTheme : function(theme){
|
||||
this.props.metadata.renderer = theme.renderer;
|
||||
this.props.metadata.theme = theme.path;
|
||||
this.props.onChange(this.props.metadata);
|
||||
},
|
||||
|
||||
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>;
|
||||
},
|
||||
|
||||
renderThemeDropdown : function(){
|
||||
if(!global.enable_themes) return;
|
||||
|
||||
const listThemes = (renderer)=>{
|
||||
return _.map(_.values(Themes[renderer]), (theme)=>{
|
||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||
{`${theme.renderer} : ${theme.name}`}
|
||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
||||
</div>;
|
||||
});
|
||||
};
|
||||
|
||||
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
|
||||
let dropdown;
|
||||
|
||||
if(this.props.metadata.renderer == 'legacy') {
|
||||
dropdown =
|
||||
<Nav.dropdown className='disabled' trigger='disabled'>
|
||||
<div>
|
||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
</Nav.dropdown>;
|
||||
} else {
|
||||
dropdown =
|
||||
<Nav.dropdown trigger='click'>
|
||||
<div>
|
||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
||||
</div>
|
||||
{/*listThemes('Legacy')*/}
|
||||
{listThemes('V3')}
|
||||
</Nav.dropdown>;
|
||||
}
|
||||
|
||||
return <div className='field themes'>
|
||||
<label>theme</label>
|
||||
{dropdown}
|
||||
</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'
|
||||
defaultValue={this.props.metadata.title}
|
||||
onChange={(e)=>this.handleFieldChange('title', e)} />
|
||||
</div>
|
||||
<div className='field-group'>
|
||||
<div className='field-column'>
|
||||
<div className='field description'>
|
||||
<label>description</label>
|
||||
<textarea defaultValue={this.props.metadata.description} className='value'
|
||||
onChange={(e)=>this.handleFieldChange('description', e)} />
|
||||
</div>
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text'
|
||||
defaultValue={this.props.metadata.thumbnail}
|
||||
placeholder='https://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>
|
||||
</div>
|
||||
</div>
|
||||
{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.renderThemeDropdown()}
|
||||
|
||||
{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,278 +0,0 @@
|
||||
@import 'naturalcrit/styles/colors.less';
|
||||
|
||||
.metadataEditor{
|
||||
position : absolute;
|
||||
z-index : 10000;
|
||||
box-sizing : border-box;
|
||||
width : 100%;
|
||||
padding : 25px;
|
||||
background-color : #999;
|
||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||
overflow-y : auto;
|
||||
|
||||
& > div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field-group {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 5 0 200px;
|
||||
gap: 10px;
|
||||
|
||||
}
|
||||
.field{
|
||||
display : flex;
|
||||
width : 100%;
|
||||
min-width : 200px;
|
||||
&>label{
|
||||
width : 80px;
|
||||
font-size : 11px;
|
||||
font-weight : 800;
|
||||
line-height : 1.8em;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
&>.value{
|
||||
flex : 1 1 auto;
|
||||
width : 50px;
|
||||
&:invalid {
|
||||
background : #ffb9b9;
|
||||
}
|
||||
}
|
||||
input[type='text'], textarea {
|
||||
border : 1px solid gray;
|
||||
}
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.description {
|
||||
flex: 1;
|
||||
textarea.value {
|
||||
resize : none;
|
||||
height : auto;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 0.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.thumbnail-preview {
|
||||
position: relative;
|
||||
justify-self: center;
|
||||
width: 80px;
|
||||
height: min-content;
|
||||
flex: 1 1;
|
||||
max-height: 115px;
|
||||
aspect-ratio: 1 / 1;
|
||||
object-fit: contain;
|
||||
background-color: #AAA;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.themes.field{
|
||||
font-size : 13.33px;
|
||||
.navDropdownContainer {
|
||||
background-color : white;
|
||||
width : 100%;
|
||||
position : relative;
|
||||
z-index : 500;
|
||||
&.disabled {
|
||||
font-style :italic;
|
||||
font-style : italic;
|
||||
background-color : darkgray;
|
||||
color : dimgray;
|
||||
}
|
||||
&>div:first-child {
|
||||
border : 2px solid rgb(118,118,118);
|
||||
padding : 6px 3px;
|
||||
background-color : inherit;
|
||||
i {
|
||||
float : right;
|
||||
}
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
}
|
||||
}
|
||||
.navDropdown {
|
||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
position : absolute;
|
||||
width : 100%;
|
||||
.item {
|
||||
padding : 3px 3px;
|
||||
border-top : 1px solid rgb(118, 118, 118);
|
||||
position : relative;
|
||||
overflow : hidden;
|
||||
background-color : white;
|
||||
&:hover {
|
||||
background-color : @blue;
|
||||
color : white;
|
||||
}
|
||||
img {
|
||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||
position : absolute;
|
||||
left : ~"max(100px, 100% - 300px)";
|
||||
top : 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.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,34 +0,0 @@
|
||||
module.exports = {
|
||||
title : [
|
||||
(value)=>{
|
||||
return value?.length > 100 ? 'Max title length of 100 characters' : null;
|
||||
}
|
||||
],
|
||||
description : [
|
||||
(value)=>{
|
||||
return value?.length > 500 ? 'Max description length of 500 characters.' : null;
|
||||
}
|
||||
],
|
||||
thumbnail : [
|
||||
(value)=>{
|
||||
return value?.length > 256 ? 'Max URL length of 256 characters.' : null;
|
||||
},
|
||||
(value)=>{
|
||||
if(value?.length == 0){return null;}
|
||||
try {
|
||||
Boolean(new URL(value));
|
||||
return null;
|
||||
} catch (e) {
|
||||
return 'Must be a valid URL';
|
||||
}
|
||||
}
|
||||
],
|
||||
language : [
|
||||
(value)=>{
|
||||
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null;
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
require('./snippetbar.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
//Import all themes
|
||||
|
||||
const Themes = require('themes/themes.json');
|
||||
|
||||
const ThemeSnippets = {};
|
||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
||||
ThemeSnippets['V3_5eDMG'] = require('themes/V3/5eDMG/snippets.js');
|
||||
ThemeSnippets['V3_Journal'] = require('themes/V3/Journal/snippets.js');
|
||||
ThemeSnippets['V3_Blank'] = require('themes/V3/Blank/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,
|
||||
snippets : []
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount : async function() {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
},
|
||||
|
||||
componentDidUpdate : async function(prevProps) {
|
||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
|
||||
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.props.theme ?? '5ePHB';
|
||||
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||
this.setState({
|
||||
snippets : snippets
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
mergeCustomizer : function(valueA, valueB, key) {
|
||||
if(key == 'snippets') {
|
||||
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
||||
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
||||
}
|
||||
},
|
||||
|
||||
compileSnippets : function(rendererPath, themePath, snippets) {
|
||||
let compiledSnippets = snippets;
|
||||
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
|
||||
|
||||
const objB = _.keyBy(compiledSnippets, 'groupName');
|
||||
|
||||
if(baseSnippetsPath) {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
|
||||
} else {
|
||||
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
|
||||
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||
}
|
||||
return compiledSnippets;
|
||||
},
|
||||
|
||||
handleSnippetClick : function(injectedText){
|
||||
this.props.onInject(injectedText);
|
||||
},
|
||||
|
||||
renderSnippetGroups : function(){
|
||||
const snippets = this.state.snippets.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
Before Width: | Height: | Size: 305 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,100 +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');
|
||||
const AccountPage = require('./pages/accountPage/accountPage.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.enable_themes = this.props.enable_themes;
|
||||
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}>
|
||||
<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='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} uiItems={this.props.brew.uiItems} />} />
|
||||
<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>
|
||||
</div>
|
||||
</Router>
|
||||
render : function(){
|
||||
return(
|
||||
<div className='homebrew'>
|
||||
<Router initialUrl={this.props.url}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Homebrew;
|
||||
|
||||
//TODO: Nicer Error page instead of just "cant get that"
|
||||
// '/share/:id' : (args)=>{
|
||||
// if(!this.props.brew.shareId){
|
||||
// return <ErrorPage errorId={args.id}/>;
|
||||
// }
|
||||
//
|
||||
// return <SharePage
|
||||
// id={args.id}
|
||||
// brew={this.props.brew} />;
|
||||
// },
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.homebrew{
|
||||
height : 100%;
|
||||
.sitePage{
|
||||
display : flex;
|
||||
height : 100%;
|
||||
background-color : @steel;
|
||||
flex-direction : column;
|
||||
overflow-y : hidden;
|
||||
.content{
|
||||
position : relative;
|
||||
height : calc(~"100% - 29px"); //Navbar height
|
||||
flex : auto;
|
||||
overflow-y : hidden;
|
||||
}
|
||||
&.listPage .content {
|
||||
overflow-y : scroll;
|
||||
}
|
||||
}
|
||||
@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,114 +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/${encodeURI(global.account.username)}`}
|
||||
color='yellow'
|
||||
icon='fas fa-beer'
|
||||
>
|
||||
brews
|
||||
</Nav.item>
|
||||
<Nav.item
|
||||
className='account'
|
||||
color='orange'
|
||||
icon='fas fa-user'
|
||||
href='/account'
|
||||
>
|
||||
account
|
||||
</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,36 +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='green' icon='fas fa-question-circle'
|
||||
href='/faq'
|
||||
newTab={true}
|
||||
rel='noopener noreferrer'>
|
||||
FAQ
|
||||
</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
@@ -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,190 +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;
|
||||
}
|
||||
.save-menu {
|
||||
.dropdown {
|
||||
z-index: 1000;
|
||||
}
|
||||
.navItem i.fa-power-off {
|
||||
color : red;
|
||||
&.active {
|
||||
color : rgb(0, 182, 52);
|
||||
filter : drop-shadow(0 0 2px rgba(0, 182, 52, 0.765))
|
||||
}
|
||||
}
|
||||
}
|
||||
.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)";
|
||||
scrollbar-color : #666 #333;
|
||||
scrollbar-width : thin;
|
||||
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;
|
||||
overflow : clip;
|
||||
.clear{
|
||||
display : none;
|
||||
position : absolute;
|
||||
top : 50%;
|
||||
transform : translateY(-50%);
|
||||
right : 0px;
|
||||
width : 20px;
|
||||
height : 100%;
|
||||
background-color : #333;
|
||||
opacity : 70%;
|
||||
border-radius : 3px;
|
||||
&:hover {
|
||||
opacity : 100%;
|
||||
}
|
||||
i {
|
||||
text-align : center;
|
||||
font-size : 10px;
|
||||
margin : 0;
|
||||
height :100%;
|
||||
width :100%;
|
||||
}
|
||||
}
|
||||
&:hover{
|
||||
background-color : @blue;
|
||||
|
||||
.clear{
|
||||
display : grid;
|
||||
place-content : center;
|
||||
}
|
||||
}
|
||||
.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,206 +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
|
||||
});
|
||||
},
|
||||
|
||||
removeItem : function(url, evt){
|
||||
evt.preventDefault();
|
||||
|
||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
|
||||
|
||||
edited = edited.filter((item)=>{ return (item.url !== url);});
|
||||
viewed = viewed.filter((item)=>{ return (item.url !== url);});
|
||||
|
||||
localStorage.setItem(EDIT_KEY, JSON.stringify(edited));
|
||||
localStorage.setItem(VIEW_KEY, JSON.stringify(viewed));
|
||||
|
||||
this.setState({
|
||||
edit : edited,
|
||||
view : viewed
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
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>
|
||||
<div className='clear' title='Remove from Recents' onClick={(e)=>{this.removeItem(`${brew.url}`, e);}}><i className='fas fa-times'></i></div>
|
||||
</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,71 +0,0 @@
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const moment = require('moment');
|
||||
|
||||
const UIPage = require('../basePages/uiPage/uiPage.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 NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
|
||||
const AccountPage = createClass({
|
||||
displayName : 'AccountPage',
|
||||
getDefaultProps : function() {
|
||||
return {
|
||||
brew : {},
|
||||
uiItems : {}
|
||||
};
|
||||
},
|
||||
getInitialState : function() {
|
||||
return {
|
||||
uiItems : this.props.uiItems
|
||||
};
|
||||
},
|
||||
|
||||
renderNavItems : function() {
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<NewBrew />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>;
|
||||
},
|
||||
|
||||
renderUiItems : function() {
|
||||
// console.log(this.props.uiItems);
|
||||
return <>
|
||||
<div className='dataGroup'>
|
||||
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||
<p><strong>Username: </strong> {this.props.uiItems.username || 'No user currently logged in'}</p>
|
||||
<p><strong>Last Login: </strong> {moment(this.props.uiItems.issued).format('dddd, MMMM Do YYYY, h:mm:ss a ZZ') || '-'}</p>
|
||||
</div>
|
||||
<div className='dataGroup'>
|
||||
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount || '-'}</p>
|
||||
</div>
|
||||
<div className='dataGroup'>
|
||||
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
||||
{this.props.uiItems.googleId ? <p><strong>Brews on Google Drive: </strong> {this.props.uiItems.fileCount || '-'}</p> : '' }
|
||||
</div>
|
||||
</>;
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <UIPage brew={this.props.brew}>
|
||||
{this.renderUiItems()}
|
||||
</UIPage>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = AccountPage;
|
||||
@@ -1,155 +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'>
|
||||
{brew.thumbnail &&
|
||||
<div className='thumbnail' style={{ backgroundImage: `url(${brew.thumbnail})` }} >
|
||||
</div>
|
||||
}
|
||||
<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)=>{
|
||||
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
|
||||
return <span key={idx} 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,101 +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 6px;
|
||||
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;
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right top;
|
||||
mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
||||
-webkit-mask-image: linear-gradient(80deg, #0000 20%, #050 40%);
|
||||
opacity: 50%;
|
||||
}
|
||||
.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,234 +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 <div className={`sort-option ${(this.state.sortType == sortValue ? 'active' : '')}`}>
|
||||
<button
|
||||
value={`${sortValue}`}
|
||||
onClick={this.state.sortType == sortValue ? this.handleSortDirChange : this.handleSortOptionChange}
|
||||
>
|
||||
{`${sortTitle}`}
|
||||
</button>
|
||||
{this.state.sortType == sortValue &&
|
||||
<i className={`sortDir fas ${this.state.sortDir == 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`}></i>
|
||||
}
|
||||
</div>;
|
||||
},
|
||||
|
||||
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 <div className='filter-option'>
|
||||
<label>
|
||||
<i className='fas fa-search'></i>
|
||||
<input
|
||||
type='search'
|
||||
placeholder='filter title/description'
|
||||
onChange={this.handleFilterTextChange}
|
||||
value={this.state.filterString}
|
||||
/>
|
||||
</label>
|
||||
</div>;
|
||||
},
|
||||
|
||||
renderSortOptions : function(){
|
||||
return <div className='sort-container'>
|
||||
<h6>Sort by :</h6>
|
||||
{this.renderSortOption('Title', 'alpha')}
|
||||
{this.renderSortOption('Created Date', 'created')}
|
||||
{this.renderSortOption('Updated Date', 'updated')}
|
||||
{this.renderSortOption('Views', 'views')}
|
||||
{/* {this.renderSortOption('Latest', 'latest')} */}
|
||||
|
||||
{this.renderFilterOption()}
|
||||
|
||||
|
||||
|
||||
</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/V3/5ePHB/style.css' rel='stylesheet'/>
|
||||
{this.props.navItems}
|
||||
{this.renderSortOptions()}
|
||||
|
||||
<div className='content V3'>
|
||||
<div className='phb page'>
|
||||
{this.renderBrewCollection(this.state.brewCollection)}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ListPage;
|
||||
@@ -1,126 +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{
|
||||
.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;
|
||||
}
|
||||
.brewCollection {
|
||||
h1:hover{
|
||||
cursor: pointer;
|
||||
}
|
||||
.active::before, .inactive::before {
|
||||
font-family: 'Font Awesome 5 Free';
|
||||
font-weight: 900;
|
||||
font-size: 0.6cm;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
.active {
|
||||
color: var(--HB_Color_HeaderText);
|
||||
}
|
||||
.active::before {
|
||||
content: '\f107';
|
||||
}
|
||||
.inactive {
|
||||
color: #707070;
|
||||
}
|
||||
.inactive::before {
|
||||
content: '\f105';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sort-container{
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
position : sticky;
|
||||
top : 0;
|
||||
left : 0;
|
||||
width : 100%;
|
||||
height : 30px;
|
||||
background-color : #555;
|
||||
border-top : 1px solid #666;
|
||||
border-bottom : 1px solid #666;
|
||||
color : white;
|
||||
text-align : center;
|
||||
z-index : 500;
|
||||
display : flex;
|
||||
justify-content : center;
|
||||
align-items : baseline;
|
||||
column-gap : 15px;
|
||||
row-gap : 5px;
|
||||
flex-wrap : wrap;
|
||||
h6{
|
||||
text-transform : uppercase;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-size : 11px;
|
||||
font-weight : bold;
|
||||
}
|
||||
.sort-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
color: #ccc;
|
||||
height: 100%;
|
||||
|
||||
&:hover{
|
||||
background-color : #444;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
background-color: #333;
|
||||
|
||||
button {
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
height: 100%;
|
||||
& + .sortDir {
|
||||
padding-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.filter-option {
|
||||
margin-left: 20px;
|
||||
background-color : transparent !important;
|
||||
font-size : 11px;
|
||||
i{
|
||||
padding-right : 5px;
|
||||
}
|
||||
}
|
||||
button{
|
||||
background-color : transparent;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
text-transform : uppercase;
|
||||
font-weight : normal;
|
||||
font-size : 11px;
|
||||
color : #ccc;
|
||||
padding : 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
require('./uiPage.less');
|
||||
const React = require('react');
|
||||
const createClass = require('create-react-class');
|
||||
|
||||
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 Account = require('../../../navbar/account.navitem.jsx');
|
||||
|
||||
|
||||
const UIPage = createClass({
|
||||
displayName : 'UIPage',
|
||||
|
||||
render : function(){
|
||||
return <div className='uiPage sitePage'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<NewBrewItem />
|
||||
<HelpNavItem />
|
||||
<RecentNavItem />
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UIPage;
|
||||
@@ -1,47 +0,0 @@
|
||||
.uiPage{
|
||||
.content{
|
||||
overflow-y : hidden;
|
||||
width : 90vw;
|
||||
background-color: #f0f0f0;
|
||||
font-family: 'Open Sans';
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 25px;
|
||||
padding: 2% 4%;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.8em;
|
||||
.dataGroup{
|
||||
padding: 6px 20px 15px;
|
||||
border: 2px solid black;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0px;
|
||||
}
|
||||
h1, h2, h3, h4{
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
margin: 0.5em 30% 0.25em 0;
|
||||
border-bottom: 2px solid slategrey;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 2px solid darkslategrey;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
svg {
|
||||
width: 19px;
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,480 +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 : '',
|
||||
autoSave : true,
|
||||
autoSaveWarning : false,
|
||||
unsavedTime : new Date()
|
||||
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.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
||||
if(this.state.autoSave){
|
||||
this.trySave();
|
||||
} else {
|
||||
this.setState({ autoSaveWarning: true });
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
},
|
||||
|
||||
handleStyleChange : function(style){
|
||||
this.setState((prevState)=>({
|
||||
brew : { ...prevState.brew, style: style },
|
||||
handleTitleChange : function(title){
|
||||
this.setState({
|
||||
title : title,
|
||||
isPending : true
|
||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||
});
|
||||
|
||||
(this.hasChanges() ? this.debounceSave() : this.debounceSave.cancel());
|
||||
},
|
||||
|
||||
handleMetaChange : function(metadata){
|
||||
this.setState((prevState)=>({
|
||||
brew : {
|
||||
...prevState.brew,
|
||||
...metadata
|
||||
},
|
||||
isPending : true,
|
||||
}), ()=>{if(this.state.autoSave) 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,
|
||||
unsavedTime : new Date()
|
||||
}));
|
||||
},
|
||||
|
||||
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?template=save_issue.yml&error-code=${encodeURIComponent(errMsg)}`}>
|
||||
here
|
||||
</a>.
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(this.state.autoSaveWarning && this.hasChanges()){
|
||||
this.setAutosaveWarning();
|
||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
|
||||
|
||||
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
|
||||
Reminder...
|
||||
<div className='errorContainer'>
|
||||
{text}
|
||||
</div>
|
||||
</Nav.item>;
|
||||
}
|
||||
|
||||
if(this.state.isSaving){
|
||||
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
|
||||
}
|
||||
if(this.state.isPending && this.hasChanges()){
|
||||
return <Nav.item className='save' onClick={this.save} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
|
||||
}
|
||||
if(!this.state.isPending && !this.state.isSaving && this.state.autoSave){
|
||||
return <Nav.item className='save saved'>auto-saved.</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>
|
||||
}
|
||||
},
|
||||
|
||||
handleAutoSave : function(){
|
||||
if(this.warningTimer) clearTimeout(this.warningTimer);
|
||||
this.setState((prevState)=>({
|
||||
autoSave : !prevState.autoSave,
|
||||
autoSaveWarning : prevState.autoSave
|
||||
}), ()=>{
|
||||
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
|
||||
});
|
||||
},
|
||||
|
||||
setAutosaveWarning : function(){
|
||||
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
|
||||
this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
|
||||
this.warningTimer;
|
||||
},
|
||||
|
||||
renderAutoSaveButton : function(){
|
||||
return <Nav.item onClick={this.handleAutoSave}>
|
||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||
</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()}
|
||||
<Nav.dropdown className='save-menu'>
|
||||
{this.renderSaveButton()}
|
||||
{this.renderAutoSaveButton()}
|
||||
</Nav.dropdown>
|
||||
<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 />
|
||||
{this.renderSaveButton()}
|
||||
<Nav.item newTab={true} href={'/homebrew/share/' + this.props.brew.shareId} color='teal' icon='fa-share-alt'>
|
||||
Share
|
||||
</Nav.item>
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<Nav.item color='red' icon='fa-trash' onClick={this.handleDelete}>
|
||||
Delete
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
</Navbar>;
|
||||
</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} theme={this.state.brew.theme} 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 : 500;
|
||||
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,95 +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(this.state.brew)
|
||||
.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
@@ -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,300 +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',
|
||||
theme : '5ePHB'
|
||||
}
|
||||
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',
|
||||
theme : brew.theme ?? '5ePHB'
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
brew.theme = metaStorage?.theme ?? brew.theme;
|
||||
|
||||
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, 'theme': brew.theme }));
|
||||
},
|
||||
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,
|
||||
'theme' : this.state.brew.theme
|
||||
}));
|
||||
},
|
||||
|
||||
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} theme={this.state.brew.theme} 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,108 +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 Themes = require('themes/themes.json');
|
||||
|
||||
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',
|
||||
theme : metaStorage?.theme || '5ePHB'
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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(){
|
||||
const rendererPath = this.state.brew.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||
const themePath = this.state.brew.theme ?? '5ePHB';
|
||||
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||
|
||||
return <div>
|
||||
<Meta name='robots' content='noindex, nofollow' />
|
||||
<link href={`/themes/${rendererPath}/Blank/style.css`} rel='stylesheet'/>
|
||||
{baseThemePath &&
|
||||
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} rel='stylesheet'/>
|
||||
}
|
||||
<link href={`/themes/${rendererPath}/${themePath}/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} theme={this.props.brew.theme} />
|
||||
<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
|
After Width: | Height: | Size: 327 B |
BIN
client/homebrew/phbStyle/note_border.png
Normal file
|
After Width: | Height: | Size: 530 B |
6
client/homebrew/phbStyle/phb.assets.less
Normal file
59
client/homebrew/phbStyle/phb.fonts.css
Normal file
@@ -1,21 +1,15 @@
|
||||
@import (less) './themes/fonts/5e legacy/fonts.less';
|
||||
@import (less) './themes/assets/assets.less';
|
||||
@import (less) './themes/phb.depricated.less';
|
||||
|
||||
@import (less) 'shared/naturalcrit/styles/reset.less';
|
||||
@import (less) './client/homebrew/phbStyle/phb.fonts.css';
|
||||
@import (less) './client/homebrew/phbStyle/phb.assets.less';
|
||||
//Colors
|
||||
@background : #EEE5CE; // Light parchment
|
||||
@noteGreen : #e0e5c1; // Pastel green
|
||||
@headerUnderline : #c9ad6a; // Gold
|
||||
@horizontalRule : #9c2b1b; // Maroon
|
||||
@headerText : #58180D; // Dark maroon
|
||||
@monsterStatBackground : #FDF1DC; // Lighter parchment
|
||||
@captionText : #766649; // Brown
|
||||
@background : #EEE5CE;
|
||||
@noteGreen : #e0e5c1;
|
||||
@headerUnderline : #c9ad6a;
|
||||
@horizontalRule : #9c2b1b;
|
||||
@headerText : #58180D;
|
||||
@monsterStatBackground : #FDF1DC;
|
||||
@page { margin: 0; }
|
||||
body {
|
||||
counter-reset : phb-page-numbers;
|
||||
}
|
||||
*{
|
||||
-webkit-print-color-adjust : exact;
|
||||
}
|
||||
.useSansSerif(){
|
||||
font-family : ScalySans;
|
||||
em{
|
||||
@@ -28,21 +22,20 @@ body {
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
.useColumns(@multiplier : 1){
|
||||
.useColumns(){
|
||||
column-count : 2;
|
||||
column-fill : auto;
|
||||
column-gap : 1cm;
|
||||
column-width : 8cm * @multiplier;
|
||||
column-width : 8cm;
|
||||
-webkit-column-count : 2;
|
||||
-moz-column-count : 2;
|
||||
-webkit-column-width : 8cm * @multiplier;
|
||||
-moz-column-width : 8cm * @multiplier;
|
||||
-webkit-column-width : 8cm;
|
||||
-moz-column-width : 8cm;
|
||||
-webkit-column-gap : 1cm;
|
||||
-moz-column-gap : 1cm;
|
||||
}
|
||||
.phb{
|
||||
.useColumns();
|
||||
counter-increment : phb-page-numbers;
|
||||
position : relative;
|
||||
z-index : 15;
|
||||
box-sizing : border-box;
|
||||
@@ -58,34 +51,30 @@ body {
|
||||
text-rendering : optimizeLegibility;
|
||||
page-break-before : always;
|
||||
page-break-after : always;
|
||||
contain : size;
|
||||
//*****************************
|
||||
// * BASE
|
||||
// *****************************/
|
||||
p{
|
||||
padding-bottom : 0.8em;
|
||||
line-height : 1.269em;
|
||||
line-height : 1.3em;
|
||||
&+p{
|
||||
margin-top : -0.8em;
|
||||
margin-top : -0.8em;
|
||||
text-indent : 1em;
|
||||
}
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.8em;
|
||||
padding-left : 1.4em;
|
||||
line-height : 1.269em;
|
||||
line-height : 1.3em;
|
||||
list-style-position : outside;
|
||||
list-style-type : disc;
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
ol{
|
||||
margin-bottom : 0.8em;
|
||||
padding-left : 1.4em;
|
||||
line-height : 1.269em;
|
||||
line-height : 1.3em;
|
||||
list-style-position : outside;
|
||||
list-style-type : decimal;
|
||||
}
|
||||
//Indents after p or lists
|
||||
p+p, ul+p, ol+p{
|
||||
text-indent : 1em;
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
img{
|
||||
z-index : -1;
|
||||
@@ -127,7 +116,7 @@ body {
|
||||
font-family : Solberry;
|
||||
font-size : 10em;
|
||||
color : #222;
|
||||
line-height : 0.795em;
|
||||
line-height : 0.8em;
|
||||
}
|
||||
}
|
||||
h2{
|
||||
@@ -156,7 +145,6 @@ body {
|
||||
margin-bottom : 1em;
|
||||
font-size : 10pt;
|
||||
thead{
|
||||
display: table-row-group;
|
||||
font-weight : 800;
|
||||
th{
|
||||
vertical-align : bottom;
|
||||
@@ -181,23 +169,23 @@ body {
|
||||
// *****************************/
|
||||
blockquote{
|
||||
.useSansSerif();
|
||||
box-sizing : border-box;
|
||||
margin-bottom : 1em;
|
||||
padding : 5px 10px;
|
||||
background-color : @noteGreen;
|
||||
border-style : solid;
|
||||
border-width : 11px;
|
||||
border-image : @noteBorderImage 11;
|
||||
border-image-outset : 9px 0px;
|
||||
box-shadow : 1px 4px 14px #888;
|
||||
box-sizing : border-box;
|
||||
margin-bottom : 1em;
|
||||
padding : 5px 10px;
|
||||
background-color : @noteGreen;
|
||||
border-style: solid;
|
||||
border-width: 11px;
|
||||
border-image: @noteBorderImage 11;
|
||||
border-image-outset: 9px 0px;
|
||||
box-shadow : 1px 4px 14px #888;
|
||||
p, ul{
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.083em;
|
||||
line-height : 1.1em;
|
||||
}
|
||||
}
|
||||
//If a note starts a column, give it space at the top to render border
|
||||
pre+blockquote, h2+blockquote, h3+blockquote, h4+blockquote, h5+blockquote {
|
||||
margin-top : 13px;
|
||||
pre+blockquote{
|
||||
margin-top: 11px;
|
||||
}
|
||||
//*****************************
|
||||
// * MONSTER STAT BLOCK
|
||||
@@ -208,7 +196,7 @@ body {
|
||||
background-color : @monsterStatBackground;
|
||||
border-style : solid;
|
||||
border-width : 10px;
|
||||
border-image : @monsterBorderImageLegacy 10;
|
||||
border-image : @monsterBorderImage 10;
|
||||
h2{
|
||||
margin-top : -8px;
|
||||
margin-bottom : 0px;
|
||||
@@ -221,20 +209,22 @@ body {
|
||||
font-weight : 400;
|
||||
border-bottom : 1px solid @headerText;
|
||||
}
|
||||
hr+ul{
|
||||
color : @headerText;
|
||||
}
|
||||
ul{
|
||||
.useSansSerif();
|
||||
padding-left : 1em;
|
||||
font-size : 0.352cm;
|
||||
padding-left : 1em;
|
||||
font-size : 0.352cm;
|
||||
color : @headerText;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
}
|
||||
// Monster Ability table
|
||||
hr+table{
|
||||
margin : 0;
|
||||
column-span : 1;
|
||||
background-color : transparent;
|
||||
border-style : none;
|
||||
border-image : none;
|
||||
border-style : none;
|
||||
-webkit-column-span : 1;
|
||||
tbody{
|
||||
tr:nth-child(odd), tr:nth-child(even){
|
||||
background-color : transparent;
|
||||
@@ -261,7 +251,29 @@ body {
|
||||
}
|
||||
//Full Width
|
||||
hr+hr+blockquote{
|
||||
.useColumns(0.96);
|
||||
.useColumns();
|
||||
}
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
hr+table{
|
||||
margin-top : -5px;
|
||||
margin-bottom : 50px;
|
||||
padding-top : 10px;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 37px 17px;
|
||||
border-image-repeat : round;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
}
|
||||
h5+hr+table{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
//*****************************
|
||||
// * FOOTER
|
||||
@@ -271,7 +283,6 @@ body {
|
||||
position : absolute;
|
||||
bottom : 0px;
|
||||
left : 0px;
|
||||
z-index : 100;
|
||||
height : 50px;
|
||||
width : 100%;
|
||||
background-image : @footerAccentImage;
|
||||
@@ -297,15 +308,11 @@ body {
|
||||
font-size : 0.9em;
|
||||
color : #c9ad6a;
|
||||
text-align : center;
|
||||
&.auto::after {
|
||||
content : counter(phb-page-numbers);
|
||||
}
|
||||
}
|
||||
.footnote{
|
||||
position : absolute;
|
||||
right : 80px;
|
||||
bottom : 32px;
|
||||
z-index : 150;
|
||||
width : 200px;
|
||||
font-size : 0.8em;
|
||||
color : #c9ad6a;
|
||||
@@ -325,19 +332,25 @@ body {
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
}
|
||||
//Double hr for full width elements
|
||||
hr+hr+blockquote{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
//Column Break
|
||||
pre, code{
|
||||
pre{
|
||||
visibility : hidden;
|
||||
-webkit-column-break-after : always;
|
||||
break-after : always;
|
||||
-moz-column-break-after : always;
|
||||
}
|
||||
//Avoid breaking up
|
||||
p,blockquote,table{
|
||||
p,ul,blockquote,table{
|
||||
z-index : 15;
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
column-break-inside : avoid;
|
||||
overflow: hidden; /* Firefox fix */
|
||||
}
|
||||
//Better spacing for spell blocks
|
||||
h4+p+hr+ul{
|
||||
@@ -352,146 +365,12 @@ body {
|
||||
margin-bottom : 0px;
|
||||
margin-left : 1.5em;
|
||||
}
|
||||
li{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * SPELL LIST
|
||||
// * PRINT
|
||||
// *****************************/
|
||||
.phb .spellList{
|
||||
.useSansSerif();
|
||||
column-count : 4;
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
ul+h5{
|
||||
margin-top : 15px;
|
||||
.phb.print{
|
||||
blockquote{
|
||||
box-shadow : none;
|
||||
}
|
||||
p, ul{
|
||||
font-size : 0.352cm;
|
||||
line-height : 1.263em;
|
||||
}
|
||||
ul{
|
||||
margin-bottom : 0.5em;
|
||||
padding-left : 1em;
|
||||
text-indent : -1em;
|
||||
list-style-type : none;
|
||||
-webkit-column-break-inside : auto;
|
||||
page-break-inside : auto;
|
||||
break-inside : auto;
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * WIDE
|
||||
// *****************************/
|
||||
.phb .wide{
|
||||
column-span : all;
|
||||
-webkit-column-span : all;
|
||||
-moz-column-span : all;
|
||||
}
|
||||
//*****************************
|
||||
// * CLASS TABLE
|
||||
// *****************************/
|
||||
.phb .classTable{
|
||||
margin-top : 25px;
|
||||
margin-bottom : 40px;
|
||||
border-collapse : separate;
|
||||
background-color : white;
|
||||
border : initial;
|
||||
border-style : solid;
|
||||
border-image-outset : 25px 17px;
|
||||
border-image-repeat : stretch;
|
||||
border-image-slice : 150 200 150 200;
|
||||
border-image-source : @frameBorderImage;
|
||||
border-image-width : 47px;
|
||||
h5{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
}
|
||||
//************************************
|
||||
// * DESCRIPTIVE TEXT BOX
|
||||
// ************************************/
|
||||
.phb .descriptive{
|
||||
margin-bottom : 1em;
|
||||
background-color : #faf7ea;
|
||||
font-family : ScalySans;
|
||||
border-style : solid;
|
||||
border-width : 7px;
|
||||
border-image : @descriptiveBoxImage 12 stretch;
|
||||
border-image-outset : 4px;
|
||||
box-shadow : 0px 0px 6px #faf7ea;
|
||||
p{
|
||||
display : block;
|
||||
padding-bottom : 0px;
|
||||
line-height : 1.47em;
|
||||
}
|
||||
p + p {
|
||||
padding-top : .8em;
|
||||
}
|
||||
em {
|
||||
font-family : ScalySans;
|
||||
font-style : italic;
|
||||
}
|
||||
strong {
|
||||
font-family : ScalySans;
|
||||
font-weight : 800;
|
||||
letter-spacing : -0.02em;
|
||||
}
|
||||
}
|
||||
.phb pre+.descriptive{
|
||||
margin-top : 8px;
|
||||
}
|
||||
|
||||
//*****************************
|
||||
// * ARTIST CREDIT BLOCK
|
||||
// *****************************/
|
||||
.phb {
|
||||
.artist {
|
||||
position : absolute;
|
||||
text-align : center;
|
||||
font-family : WalterTurncoat;
|
||||
font-size : 0.27cm;
|
||||
color : @captionText;
|
||||
p, p + p {
|
||||
margin : unset;
|
||||
text-indent : unset;
|
||||
line-height : 0.941em;
|
||||
}
|
||||
h5 {
|
||||
font-size : 1.3em;
|
||||
font-family : WalterTurncoat;
|
||||
}
|
||||
a{
|
||||
color : inherit;
|
||||
text-decoration : unset;
|
||||
&:hover {
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//*****************************
|
||||
// * TABLE OF CONTENTS
|
||||
// *****************************/
|
||||
.phb .toc{
|
||||
-webkit-column-break-inside : avoid;
|
||||
page-break-inside : avoid;
|
||||
break-inside : avoid;
|
||||
a{
|
||||
color : black;
|
||||
text-decoration : none;
|
||||
&:hover{
|
||||
text-decoration : underline;
|
||||
}
|
||||
}
|
||||
ul{
|
||||
padding-left : 0;
|
||||
list-style-type : none;
|
||||
}
|
||||
&>ul>li{
|
||||
margin-bottom : 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 864 B After Width: | Height: | Size: 864 B |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
86
client/main/main.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
|
||||
var Router = require('pico-router');
|
||||
|
||||
var NaturalCritIcon = require('naturalcrit/svg/naturalcrit.svg.jsx');
|
||||
var HomebrewIcon = require('naturalcrit/svg/homebrew.svg.jsx');
|
||||
|
||||
var Main = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
tools : [
|
||||
{
|
||||
id : 'homebrew',
|
||||
path : '/homebrew',
|
||||
name : 'The Homebrewery',
|
||||
icon : <HomebrewIcon />,
|
||||
desc : 'Make authentic-looking 5e homebrews using Markdown',
|
||||
|
||||
show : true,
|
||||
beta : false
|
||||
},
|
||||
{
|
||||
id : 'homebrew2',
|
||||
path : '/homebrew',
|
||||
name : 'The Homebrewery',
|
||||
icon : <HomebrewIcon />,
|
||||
desc : 'Make authentic-looking 5e homebrews using Markdown',
|
||||
|
||||
show : false,
|
||||
beta : true
|
||||
},
|
||||
{
|
||||
id : 'homebrewfg2',
|
||||
path : '/homebrew',
|
||||
name : 'The Homebrewery',
|
||||
icon : <HomebrewIcon />,
|
||||
desc : 'Make authentic-looking 5e homebrews using Markdown',
|
||||
|
||||
show : false,
|
||||
beta : false
|
||||
}
|
||||
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
renderTool : function(tool){
|
||||
if(!tool.show) return null;
|
||||
|
||||
return <a href={tool.path} className={cx('tool', tool.id, {beta : tool.beta})} key={tool.id}>
|
||||
<div className='content'>
|
||||
{tool.icon}
|
||||
<h2>{tool.name}</h2>
|
||||
<p>{tool.desc}</p>
|
||||
</div>
|
||||
</a>;
|
||||
},
|
||||
|
||||
renderTools : function(){
|
||||
return _.map(this.props.tools, (tool)=>{
|
||||
return this.renderTool(tool);
|
||||
});
|
||||
},
|
||||
|
||||
render : function(){
|
||||
return <div className='main'>
|
||||
<div className='top'>
|
||||
<div className='logo'>
|
||||
<NaturalCritIcon />
|
||||
<span className='name'>
|
||||
Natural
|
||||
<span className='crit'>Crit</span>
|
||||
</span>
|
||||
</div>
|
||||
<p>Top-tier tools for the discerning DM</p>
|
||||
</div>
|
||||
<div className='tools'>
|
||||
{this.renderTools()}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Main;
|
||||
136
client/main/main.less
Normal file
@@ -0,0 +1,136 @@
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
.main{
|
||||
height : 100vh;
|
||||
background-color : white;
|
||||
.top{
|
||||
.fadeInTop(1s);
|
||||
.delay(0.5);
|
||||
margin-bottom : 100px;
|
||||
padding-top : 100px;
|
||||
text-align : center;
|
||||
.logo{
|
||||
font-size : 4em;
|
||||
color : black;
|
||||
svg{
|
||||
height : .9em;
|
||||
margin-right : .2em;
|
||||
cursor : pointer;
|
||||
fill : black;
|
||||
}
|
||||
.name{
|
||||
font-family : 'CodeLight';
|
||||
.crit{
|
||||
font-family : 'CodeBold';
|
||||
}
|
||||
}
|
||||
}
|
||||
p{
|
||||
margin-top : 10px;
|
||||
font-size : 1.3em;
|
||||
font-style : italic;
|
||||
color : @grey;
|
||||
}
|
||||
}
|
||||
.tools{
|
||||
width : 100%;
|
||||
text-align : center;
|
||||
.tool{
|
||||
.sequentialDelay(0.5s, 1s);
|
||||
.fadeInDown(1s);
|
||||
.keep();
|
||||
display : inline-block;
|
||||
cursor : pointer;
|
||||
opacity : 0;
|
||||
color : black;
|
||||
text-align : center;
|
||||
text-decoration : none;
|
||||
&+.tool{
|
||||
border-left : 1px solid #666;
|
||||
}
|
||||
.content{
|
||||
.addSketch(360px);
|
||||
.animateAll(0.5s);
|
||||
position : relative;
|
||||
width : 320px;
|
||||
padding : 35px;
|
||||
&:hover{
|
||||
svg, h2{
|
||||
.transform(scale(1.3));
|
||||
}
|
||||
}
|
||||
h2{
|
||||
.animateAll(0.5s);
|
||||
font-family : 'CodeBold';
|
||||
font-size : 2em;
|
||||
}
|
||||
p{
|
||||
max-width : 300px;
|
||||
margin : 20px auto;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
svg{
|
||||
.animateAll(0.5s);
|
||||
height : 10em;
|
||||
}
|
||||
}
|
||||
.content:hover{
|
||||
background-color : fade(@teal, 20%);
|
||||
}
|
||||
//Beta styles
|
||||
&.beta{
|
||||
cursor : initial;
|
||||
.content{
|
||||
&:hover{
|
||||
svg, h2{
|
||||
.transform(scale(1.0));
|
||||
}
|
||||
}
|
||||
svg, h2{
|
||||
opacity : 0.3;
|
||||
}
|
||||
&:after{
|
||||
.animateAll();
|
||||
content : "beta!";
|
||||
position : absolute;
|
||||
display : block;
|
||||
top : 120px;
|
||||
left : 0px;
|
||||
width : 100%;
|
||||
padding : 10px 0px;
|
||||
//opacity : 0;
|
||||
background-color : fade(@grey, 50%);
|
||||
font-size : 2em;
|
||||
font-weight : 800;
|
||||
text-align : center;
|
||||
text-transform : uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.addSketch(@length, @color : black){
|
||||
path, line, polyline, circle, rect, polygon {
|
||||
.sketch(@length, @color, 4s);
|
||||
stroke-dasharray : @length;
|
||||
stroke-dashoffset : 0px;
|
||||
stroke : @color;
|
||||
stroke-width : 0.5px;
|
||||
fill : @color;
|
||||
//.animateAll(3s);
|
||||
}
|
||||
}
|
||||
.sketch(@length, @color : black, @duration : 3s, @easing : @defaultEasing){
|
||||
.createAnimation(sketch, @duration, @easing);
|
||||
.sketchKeyFrames(){
|
||||
0% { stroke-dashoffset : @length; fill: transparent;}
|
||||
50% { stroke-dashoffset : @length; fill: transparent;}
|
||||
80% { stroke-dashoffset : 0px; fill: transparent;}
|
||||
100% { stroke-dashoffset : 0px; fill:@color;}
|
||||
}
|
||||
@-webkit-keyframes sketch {.sketchKeyFrames();}
|
||||
@-moz-keyframes sketch {.sketchKeyFrames();}
|
||||
@-ms-keyframes sketch {.sketchKeyFrames();}
|
||||
@-o-keyframes sketch {.sketchKeyFrames();}
|
||||
@keyframes sketch {.sketchKeyFrames();}
|
||||
}
|
||||
30
client/template.dot
Normal file
@@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>global=window</script>
|
||||
<link href="//netdna.bootstrapcdn.com/font-awesome/4.6.2/css/font-awesome.min.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link rel="icon" href="/assets/main/favicon.ico" type="image/x-icon" />
|
||||
{{=vitreum.css}}
|
||||
{{=vitreum.globals}}
|
||||
<title>Natural Crit - D&D Tools</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="reactContainer">{{=vitreum.component}}</div>
|
||||
</body>
|
||||
{{=vitreum.libs}}
|
||||
{{=vitreum.js}}
|
||||
{{=vitreum.reactRender}}
|
||||
|
||||
{{? vitreum.inProduction}}
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','http://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', 'UA-72212009-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
</script>
|
||||
{{?}}
|
||||
</html>
|
||||
@@ -1,31 +0,0 @@
|
||||
const template = async function(name, title='', props = {}){
|
||||
const ogTags = [];
|
||||
const ogMeta = props.ogMeta ?? {};
|
||||
Object.entries(ogMeta).forEach(([key, value])=>{
|
||||
if(!value) return;
|
||||
const tag = `<meta property="og:${key}" content="${value}">`;
|
||||
ogTags.push(tag);
|
||||
});
|
||||
const ogMetaTags = ogTags.join('\n');
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
|
||||
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
|
||||
<link href=${`/${name}/bundle.css`} rel='stylesheet' />
|
||||
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon" />
|
||||
${ogMetaTags}
|
||||
<meta name="twitter:card" content="summary">
|
||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||
</head>
|
||||
<body>
|
||||
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||
<script src=${`/${name}/bundle.js`}></script>
|
||||
<script>start_app(${JSON.stringify(props)})</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
};
|
||||
|
||||
module.exports = template;
|
||||