0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-24 14:13:02 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Trevor Buckner
844b6ef563 3.3.2 2022-11-28 11:31:46 -05:00
252 changed files with 21863 additions and 19296 deletions

View File

@@ -5,12 +5,12 @@
version: 2.1
orbs:
node: circleci/node@5.1.0
node: circleci/node@3.0.0
jobs:
build:
docker:
- image: cimg/node:20.8.0
- image: cimg/node:16.11.0
- image: mongo:4.4
working_directory: ~/homebrewery
@@ -27,7 +27,7 @@ jobs:
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run: sudo npm install -g npm@10.2.0
- run: sudo npm install -g npm@8.10.0
- node/install-packages:
app-dir: ~/homebrewery
cache-path: node_modules
@@ -45,37 +45,25 @@ jobs:
test:
docker:
- image: cimg/node:20.8.0
- image: cimg/node:16.11.0
working_directory: ~/homebrewery
parallelism: 1
parallelism: 4
steps:
- attach_workspace:
at: .
# run tests!
- run:
name: Test - API Unit Tests
command: npm run test:api-unit
- run:
name: Test - Basic
command: npm run test:basic
- run:
name: Test - Mustache Spans
command: npm run test:mustache-syntax
- run:
name: Test - Definition Lists
command: npm run test:definition-lists
- run:
name: Test - Variables
command: npm run test:variables
command: npm run test:mustache-span
- run:
name: Test - Routes
command: npm run test:route
- run:
name: Test - Coverage
command: npm run test:coverage
workflows:
build_and_test:

View File

@@ -11,11 +11,11 @@ module.exports = {
browser : true,
node : true
},
plugins : ['react', 'jest'],
plugins : ['react'],
rules : {
/** Errors **/
'camelcase' : ['error', { properties: 'never' }],
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
'no-array-constructor' : 'error',
'no-iterator' : 'error',
'no-nested-ternary' : 'error',
@@ -24,7 +24,6 @@ module.exports = {
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
'react/jsx-uses-react' : 'error',
'react/prefer-es6-class' : ['error', 'never'],
'jest/valid-expect' : ['error', { maxArgs: 3 }],
/** Warnings **/
'max-lines' : ['warn', {

View File

@@ -1,103 +0,0 @@
name: Limit pull requests
description: >
Limit the number of open pull requests to the repository created by a user
author: ZhongRuoyu (from Homebrew repository)
branding:
icon: alert-triangle
color: yellow
inputs:
token:
description: GitHub token
required: false
default: ${{ github.token }}
except-users:
description: The users exempted from the limit, one per line
required: false
# https://docs.github.com/en/graphql/reference/enums#commentauthorassociation
except-author-associations:
description: The author associations exempted from the limit, one per line
required: false
comment-limit:
description: >
Post the comment when the user's number of open pull requests exceeds this
number and `comment` is not empty
required: true
default: "10"
comment:
description: The comment to post when the limit is reached
required: false
close-limit:
description: >
Close the pull request when the user's number of open pull requests
exceeds this number and `close` is set to `true`
required: true
default: "50"
close:
description: Whether to close the pull request when the limit is reached
required: true
default: "false"
runs:
using: composite
steps:
- name: Check the number of pull requests
id: count-pull-requests
run: |
# If the user is exempted, assume they have no pull requests.
if grep -Fiqx '${{ github.actor }}' <<<"$EXCEPT_USERS"; then
echo "::notice::@${{ github.actor }} is exempted from the limit."
echo "count=0" >>"$GITHUB_OUTPUT"
exit 0
fi
if grep -Fiqx '${{ github.event.pull_request.author_association }}' <<<"$EXCEPT_AUTHOR_ASSOCIATIONS"; then
echo "::notice::@{{ github.actor }} is a ${{ github.event.pull_request.author_association }} exempted from the limit."
echo "count=0" >>"$GITHUB_OUTPUT"
exit 0
fi
count="$(
gh api \
--method GET \
--header 'Accept: application/vnd.github+json' \
--header 'X-GitHub-Api-Version: 2022-11-28' \
--field state=open \
--paginate \
'/repos/{owner}/{repo}/pulls' |
jq \
--raw-output \
--arg USER '${{ github.actor }}' \
'map(select(.user.login == $USER)) | length'
)"
echo "::notice::@${{ github.actor }} has $count open pull request(s)."
echo "count=$count" >>"$GITHUB_OUTPUT"
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
EXCEPT_USERS: ${{ inputs.except-users }}
EXCEPT_AUTHOR_ASSOCIATIONS: ${{ inputs.except-author-associations }}
shell: bash
- name: Comment on pull request
if: >
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.comment-limit) &&
inputs.comment != ''
run: |
gh pr comment '${{ github.event.pull_request.number }}' \
--body="${COMMENT_BODY}"
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
COMMENT_BODY: ${{ inputs.comment }}
shell: bash
- name: Close pull request
if: >
fromJSON(steps.count-pull-requests.outputs.count) > fromJSON(inputs.close-limit) &&
inputs.close == 'true'
run: |
gh pr close '${{ github.event.pull_request.number }}'
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ inputs.token }}
shell: bash

View File

@@ -1,29 +0,0 @@
name: PR Check
on: pull_request_target
env:
GH_REPO: ${{ github.repository }}
GH_NO_UPDATE_NOTIFIER: 1
GH_PROMPT_DISABLED: 1
permissions:
contents: read
issues: write
pull-requests: write
statuses: write
jobs:
limit-pull-requests:
if: always() && github.repository_owner == 'naturalcrit'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name : Run limit-pull-requests action
uses: ./.github/actions/limit-pull-requests
with:
except-users: |
dependabot
comment-limit: 3
comment: |
Hi, thanks for your contribution to the Homebrewery! You already have >=3 open pull requests. Consider completing some of your existing PRs before opening new ones. Thanks!
close-limit: 5
close: false

2
.gitignore vendored
View File

@@ -12,5 +12,3 @@ todo.md
startDB.bat
startMViewer.bat
.vscode
coverage

View File

@@ -1,48 +0,0 @@
{
"extends": [
"stylelint-config-recess-order",
"stylelint-config-recommended"],
"plugins": [
"stylelint-stylistic",
"./stylelint_plugins/declaration-colon-align.js",
"./stylelint_plugins/declaration-colon-min-space-before",
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
],
"customSyntax": "postcss-less",
"rules": {
"no-descending-specificity" : null,
"at-rule-no-unknown" : null,
"function-no-unknown" : null,
"font-family-no-missing-generic-family-keyword" : null,
"font-weight-notation" : "named-where-possible",
"font-family-name-quotes" : "always-unless-keyword",
"stylistic/indentation" : "tab",
"no-duplicate-selectors" : true,
"stylistic/color-hex-case" : "upper",
"color-hex-length" : "long",
"stylistic/selector-combinator-space-after" : "always",
"stylistic/selector-combinator-space-before" : "always",
"stylistic/selector-attribute-operator-space-before" : "never",
"stylistic/selector-attribute-operator-space-after" : "never",
"stylistic/selector-attribute-brackets-space-inside" : "never",
"selector-attribute-quotes" : "always",
"selector-pseudo-element-colon-notation" : "double",
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
"stylistic/block-opening-brace-space-before" : "always",
"naturalcrit/declaration-colon-min-space-before" : 1,
"stylistic/declaration-block-trailing-semicolon" : "always",
"stylistic/declaration-colon-space-after" : "always",
"stylistic/number-leading-zero" : "always",
"function-url-quotes" : ["always", { "except": ["empty"] }],
"function-url-scheme-disallowed-list" : ["data","http"],
"comment-whitespace-inside" : "always",
"stylistic/string-quotes" : "single",
"stylistic/media-feature-range-operator-space-before" : "always",
"stylistic/media-feature-range-operator-space-after" : "always",
"stylistic/media-feature-parentheses-space-inside" : "never",
"stylistic/media-feature-colon-space-before" : "always",
"stylistic/media-feature-colon-space-after" : "always",
"naturalcrit/declaration-colon-align" : true,
"naturalcrit/declaration-block-multi-line-min-declarations": 1
}
}

View File

@@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:16.11-alpine
RUN apk --no-cache add git
ENV NODE_ENV=docker
@@ -10,11 +10,11 @@ WORKDIR /usr/src/app
# 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 npm install --ignore-scripts
RUN yarn install --ignore-scripts
# Bundle app source and build application
COPY . .
RUN npm run build
RUN yarn build
EXPOSE 8000
CMD [ "npm", "start" ]
CMD [ "yarn", "start" ]

View File

@@ -21,29 +21,24 @@ below.
First, install three programs that The Homebrewery requires to run and retrieve
updates:
1. install [node](https://nodejs.org/en/), version v16 or higher.
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. If you install any version over 6.0, you will have to install [MongoDB Shell](https://www.mongodb.com/try/download/shell).
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\6.0\bin).
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. 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.insertOne({"title":"test"})` to create a blank document. You should see `{
acknowledged: true,
insertedId: ObjectId("63c2fce9e5ac5a94fe2410cf")
}`
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]):
@@ -56,19 +51,11 @@ git clone https://github.com/naturalcrit/homebrewery.git
Second, you will need to add the environment variable `NODE_ENV=local` to allow
the project to run locally.
You can set this **temporarily** (until you close the terminal) in your shell of choice with admin privileges:
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`
If you want to add this variable **permanently** the steps are as follows:
1. Search in Windows for "Advanced system settings" and open it.
1. Click "Environment variables".
1. In System Variables, click "New"
1. Click "New" and write `NODE_ENV` as a name and `local` as the value.
1. Click "OK" three times to close all the windows.
This can be undone at any time if needed.
Third, you will need to install the Node dependencies, compile the app, and run
it using the two commands:
@@ -78,13 +65,6 @@ it using the two commands:
You should now be able to go to [http://localhost:8000](http://localhost:8000)
in your browser and use The Homebrewery offline.
If you had any issue at all, here are some links that may be useful:
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
- [Mongo community forums](https://www.mongodb.com/community/forums/)
- Useful Stack Overflow links for your most probable errors: [1](https://stackoverflow.com/questions/44962540/mongod-and-mongo-commands-not-working-on-windows-10), [2](https://stackoverflow.com/questions/15053893/mongo-command-not-recognized-when-trying-to-connect-to-a-mongodb-server/41507803#41507803), [3](https://stackoverflow.com/questions/51224959/mongo-is-not-recognized-as-an-internal-or-external-command-operable-program-o)
If you still have problems, post in [Our Subreddit](https://www.reddit.com/r/homebrewery/) and we will help you.
### Running the application via Docker
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)

View File

@@ -1,36 +1,13 @@
```css
.beta {
color : white;
padding : 4px 6px;
line-height : 1em;
background : grey;
border-radius : 12px;
font-family : monospace;
font-size : 10px;
font-weight : 800;
margin-top : -5px;
margin-bottom : -5px;
}
.fac {
height: 1em;
line-height: 2em;
margin-bottom: -0.05cm
}
h5 {
font-size: .35cm !important;
margin-top: 0.3cm;
}
.page ul ul {
margin-left: 0px;
}
.page .taskList {
display:block;
break-inside:auto;
}
.taskList li input {
list-style-type : none;
margin-left : -0.52cm;
@@ -59,702 +36,15 @@ pre {
margin-top : 0.1cm;
}
.page ul + h5 {
margin-top: 0.25cm;
}
.page p + h5 {
margin-top: 0.25cm;
}
.page .openSans {
font-family: 'Open Sans';
font-size: 0.9em;
}
.page {
padding-bottom: 1.5cm;
}
.varSyntaxTable th:first-of-type {
width:6cm;
}
```
## changelog
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
### Saturday 6/7/2024 - v3.13.1
{{taskList
##### calculuschild, G-Ambatte
* [x] Hotfixes for issues with v3.13.0
Fixes issues [#3559](https://github.com/naturalcrit/homebrewery/issues/3559), [#3552](https://github.com/naturalcrit/homebrewery/issues/3552), [#3554](https://github.com/naturalcrit/homebrewery/issues/3554)
}}
### Friday 28/6/2024 - v3.13.0
{{taskList
##### calculuschild
* [x] Add `:emoji:` Markdown syntax, with autosuggest; start typing after the first `:` for matching emojis from
:fab_font_awesome: FontAwesome, :df_d20: DiceFont, :ei_action: ElderberryInn, and a subset of :gi_broadsword: GameIcons
* [x] Fix `{curly injection}` to append to, rather than erase and replace target CSS
* [x] {{openSans **GET PDF**}} {{fa,fa-file-pdf}} now opens the print dialog directly, rather than redirecting to a separate page
##### Gazook
* [x] Several small style tweaks to the UI
* [x] Cleaning and refactoring several large pieces of code
##### 5e-Cleric
* [x] For error pages, add links to user account and `/share` page if available
Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
* [x] Change FrontCover title to use stroke outline instead of faking it with dozens of shadows
* [x] Cleaning and refactoring several large pieces of CSS
##### abquintic
* [x] Added additional {{openSans **TABLE OF CONTENTS**}} snippet options. Explicitly include or exclude items from the ToC generation via CSS properties
`--TOC:exclude` or `--TOC:include`, or change the included header depth from 3 to 6 (default 3) with `tocDepthH6`
##### MurdoMaclachlan *(new contributor!)*
* [x] Added "proficiency bonus" to Monster Stat Block snippet.
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
}}
\column
### Monday 18/3/2024 - v3.12.0
{{taskList
##### 5e-Cleric
* [x] Fix language-specific hyphenation on print page
Fixes issue [#3294](https://github.com/naturalcrit/homebrewery/issues/3294)
* [x] Upgrade Font-Awesome to v6.51
* [x] Allow downloaded files to be uploaded via {{openSans **NEW {{fa,fa-plus-square}} → FROM UPLOAD {{fa,fa-upload}}**}}
##### G-Ambatte
* [x] Fix an edge case crash with empty documents
Fixes issue [#3315](https://github.com/naturalcrit/homebrewery/issues/3315)
* [x] Brews on the user page can be searched by tag; clicking a tag adds it to the filter
Fixes issue [#3164](https://github.com/naturalcrit/homebrewery/issues/3164)
* [x] Add *DiceFont* icons {{df,d20-20}} `{{df,icon-name}}`
##### abquintic
* [x] Fix ^super^ and ^^sub^^ highlighting in the text editor
* [x] Add new syntax for multiline Definition Lists:
```
Term
::Definition 1
::Definition 2
with more text
```
produces:
Term
::Definition 1
::Definition 2
with more text
Fixes issue [#2340](https://github.com/naturalcrit/homebrewery/issues/2340)
##### RKuerten :
* [x] Fix monster stat block backgrounds on print page
Fixes issue [#3275](https://github.com/naturalcrit/homebrewery/issues/3275)
* [x] Added new text editor theme: "Darkvision".
##### calculuschild, G-Ambatte, 5e-Cleric
* [x] Codebase and UI cleanup
}}
\page
### Friday 21/2/2024 - v3.11.0
{{taskList
##### Gazook89
* [x] Brew view count no longer increases when viewed by owner
Fixes issue [#3037](https://github.com/naturalcrit/homebrewery/issues/3037)
* [x] Small tweak to PHB H3 sizing
Fixes issue [#2989](https://github.com/naturalcrit/homebrewery/issues/2989)
* [x] Add **Fold/Unfold All** {{fas,fa-compress-alt}} / {{fas,fa-expand-alt}} buttons to editor bar
Fixes issue [#2965](https://github.com/naturalcrit/homebrewery/issues/2965)
##### G-Ambatte
* [x] Share link added to Editor Access error page
Fixes issue [#3086](https://github.com/naturalcrit/homebrewery/issues/3086)
* [x] Add Darkbrewery theme to Editor theme selector {{fas,fa-palette}}
Fixes issue [#3034](https://github.com/naturalcrit/homebrewery/issues/3034)
* [x] Fix Firefox prints with alternating blank pages
Fixes issue [#3115](https://github.com/naturalcrit/homebrewery/issues/3115)
* [x] Admin page working again
Fixes issue [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
##### 5e-Cleric
* [x] Fix indenting issue with Monster Blocks and italics in Class Feature
Fixes issues [#527](https://github.com/naturalcrit/homebrewery/issues/527),
[#3247](https://github.com/naturalcrit/homebrewery/issues/3247)
* [x] Allow CSS vars in curly syntax to be formatted as strings using single quotes
`{{--customVar:"'a string'"}}`
Fixes issue [#3066](https://github.com/naturalcrit/homebrewery/issues/3066)
* [x] Add *Elderberry Inn* icons {{ei,action}} `{{ei,icon-name}}`
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
* [x] New {{openSans **{{fas,fa-keyboard}} FONTS** }} snippets!
Fixes issue [#3171](https://github.com/naturalcrit/homebrewery/issues/3171)
* [x] New page now opens in a new tab
##### abquintic (new contributor!)
* [x] Add ^super^ `^abc^` and ^^sub^^ `^^abc^^` syntax.
Fixes issue [#2171](https://github.com/naturalcrit/homebrewery/issues/2171)
* [x] Add HTML tag assignment to curly syntax `{{tag=value}}`
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
* [x] {{openSans **Brew → Clone to New**}} now clones tags
Fixes issue [1488](https://github.com/naturalcrit/homebrewery/issues/1488)
##### calculuschild
* [x] Better error messages for "Out of Google Drive Storage" and "Not logged in to edit"
Fixes issues [2510](https://github.com/naturalcrit/homebrewery/issues/2510),
[2975](https://github.com/naturalcrit/homebrewery/issues/2975)
* [x] Brew Variables
}}
\
{{wide
### Brew Variable Syntax
You may already be familiar with `[link](url)` and `![image](url)` synax. We have expanded this to include a third `$[variable](text)` syntax. All three of these syntaxes now share a common set of features:
{{varSyntaxTable
| syntax | description |
|:-------|-------------|
| `[var]:content` | Assigns a variable (must start on a line by itself, and ends at the next blank line) |
| `[var](content)` | Assigns a variable and outputs it (can be inline) |
| `[var]` | Outputs the variable contents as a link, if formatted as a valid link |
| `![var]` | Outputs as an image, if formatted as a valid image |
| `$[var]` | Outputs as Markdown |
| `$[var1 + var2 - 2 * var3]` | Performs math operations and outputs result if all variables are valid numbers |
}}
}}
{{wide,margin-top:0,margin-bottom:0
### Examples
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
[first]: Bob
[last]: Jones
My name is $[first] $[last].
```
\column
[first]: Bob
[last]: Jones
My name is $[first] $[last].
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
[myTable]:
| h1 | h2 |
|----|----|
| c1 | c2 |
Here is my table:
$[myTable]
```
\column
[myTable]:
| h1 | h2 |
|----|----|
| c1 | c2 |
Here is my table:
$[myTable]
}}
{{wide,columns:2,margin-top:0,margin-bottom:0
```
There are $[TableNum] tables total.
#### Table $[TableNum](1): Horses
#### Table $[TableNum]($[TableNum + 1]): Cows
```
\column
There are $[TableNum] tables in this document. *(note: final value of `$[TableNum]` gets hoisted up if available)*
#### Table $[TableNum](1): Horses
#### Table $[TableNum]($[TableNum + 1]): Cows
}}
\page
### Friday 13/10/2023 - v3.10.0
{{taskList
##### G-Ambatte
* [x] Fix user preferred save location being ignored
Fixes issue [#2993](https://github.com/naturalcrit/homebrewery/issues/2993)
* [x] Fix crash to white screen when starting new brews while not signed in
Fixes issue [#2999](https://github.com/naturalcrit/homebrewery/issues/2999)
* [x] Fix FreeBSD install script
Fixes issue [#3005](https://github.com/naturalcrit/homebrewery/issues/3005)
* [x] Fix *"This brew has been changed on another device"* triggering when manually saving during auto-save
Fixes issue [#2641](https://github.com/naturalcrit/homebrewery/issues/2641)
* [x] Fix Firefox different column-flow behavior
Fixes issue [#2982](https://github.com/naturalcrit/homebrewery/issues/2982)
* [x] Fix brew titles being mis-sorted on user page
Fixes issue [#2775](https://github.com/naturalcrit/homebrewery/issues/2775)
* [x] Text Editor themes now available via new drop-down
Fixes issue [#362](https://github.com/naturalcrit/homebrewery/issues/362)
##### 5e-Cleric
* [x] New {{openSans **PHB → {{fas,fa-quote-right}} QUOTE** }} snippet for V3!
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
* [x] Several updates and fixes to FAQ and Welcome page
Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
[#2787](https://github.com/naturalcrit/homebrewery/issues/2787)
##### Gazook89
* [x] Add syntax highlighting for Definition Lists <code>:\:</code>
}}
### Thursday 17/08/2023 - v3.9.2
{{taskList
##### Calculuschild
* [x] Fix links to certain old Google Drive files
Fixes issue [#2917](https://github.com/naturalcrit/homebrewery/issues/2917)
##### G-Ambatte
* [x] Menus now open on click, and internally consistent
Fixes issue [#2702](https://github.com/naturalcrit/homebrewery/issues/2702), [#2782](https://github.com/naturalcrit/homebrewery/issues/2782)
* [x] Add smarter footer snippet
Fixes issue [#2289](https://github.com/naturalcrit/homebrewery/issues/2289)
* [x] Add sanitization in Style editor
Fixes issue [#1437](https://github.com/naturalcrit/homebrewery/issues/1437)
* [x] Rework class table snippets to remove unnecessary randomness
Fixes issue [#2964](https://github.com/naturalcrit/homebrewery/issues/2964)
* [x] Add User Page link to Google Drive file for file owners, add icons for additional storage locations
Fixes issue [#2954](https://github.com/naturalcrit/homebrewery/issues/2954)
* [x] Add default save location selection to Account Page
Fixes issue [#2943](https://github.com/naturalcrit/homebrewery/issues/2943)
##### 5e-Cleric
* [x] Exclude cover pages from Table of Content generation (editing on mobile is still not recommended)
Fixes issue [#2920](https://github.com/naturalcrit/homebrewery/issues/2920)
##### Gazook89
* [x] Adjustments to improve mobile viewing
}}
### Wednesday 28/06/2023 - v3.9.1
{{taskList
##### G-Ambatte
* [x] Better error pages with more useful information
Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
}}
### Friday 02/06/2023 - v3.9.0
{{taskList
##### Calculuschild
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
Fixes issue [#2408](https://github.com/naturalcrit/homebrewery/issues/2408)
* [x] Pressing tab now indents with spaces instead of tab character; fixes several issues with Markdown lists
Fixes issues [#2092](https://github.com/naturalcrit/homebrewery/issues/2092), [#1556](https://github.com/naturalcrit/homebrewery/issues/1556)
* [x] Rename `naturalCritLogo.svg` to `naturalCritLogoRed.svg`. Those using the {{beta BETA}} coverPage snippet may need to update that text to make the NaturalCrit logo appear again.
##### G-Ambatte
* [x] Fix strange animation of image masks
Fixes issue [#2790](https://github.com/naturalcrit/homebrewery/issues/2790)
##### 5e-Cleric
* [x] New {{openSans **PHB → {{fac,book-part-cover}} PART COVER PAGE** }} snippet for V3!
* [x] New {{openSans **PHB → {{fac,book-back-cover}} BACK COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
* [x] New {{openSans **TEXT EDITOR → {{fas,fa-bars}} INDEX** }} snippet for V3!
* [x] Fix highlighting of curly braces inside comments
Fixes issue [#2784](https://github.com/naturalcrit/homebrewery/issues/2784)
}}
\page
### Wednesday 12/04/2023 - v3.8.0
{{taskList
##### calculuschild
* [x] Rename `{{coverPage}}` to `{{frontCover}}`. Those using this {{beta BETA}} feature will need to update that text to make the cover page appear again.
* [x] Several background fixes to test scripts
##### Jeddai
* [X] Add content negotiation to exclude image requests from our API calls
Fixes issue [#2595](https://github.com/naturalcrit/homebrewery/issues/2595)
##### G-Ambatte
* [x] Update server build scripts to fix Admin page
Fixes issues [#2657](https://github.com/naturalcrit/homebrewery/issues/2657)
* [x] Fix internal links inside `<\div>` blocks not receiving the `target=_self` attribute
Fixes issues [#2680](https://github.com/naturalcrit/homebrewery/issues/2680)
* [x] See brew details on `/share` pages by clicking the brew title (author, last update, tags, etc.)
Fixes issues [#1679](https://github.com/naturalcrit/homebrewery/issues/1679)
* [x] Add local Windows install script via Chocolatey
##### 5e-Cleric
* [x] New {{openSans **TABLES → {{fas,fa-language}} RUNE TABLE**}} snippets for V3. Adds an alphabetic script translation table.
* [x] New {{openSans **IMAGES → {{fac,mask-center}} WATERCOLOR CENTER** }} snippets for V3, which adds a stylish watercolor texture to the center of your images!
* [x] New {{openSans **PHB → {{fac,book-inside-cover}} INSIDE COVER PAGE** }} snippet for V3! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
* [x] Add some missing characters {{font-family:scalySansRemake Ñ ñ ç Ç Ý ý # ^ ¿ ' " ¡ ·}} to the "scalySansRemake" font in V3.
Fixes issues [#2280](https://github.com/naturalcrit/homebrewery/issues/2280)
##### Gazook89
* [x] Add "Language" selector in {{fa,fa-info-circle}} **Properties** menu. Sets the HTML Lang attribute for your brew to better handle hyphenation or spellcheck.
Fixes issues [#1343](https://github.com/naturalcrit/homebrewery/issues/1343)
* [x] Fix a crash when multiple `{injection}` tags appear in sequence
Fixes issues [#2712](https://github.com/naturalcrit/homebrewery/issues/2712)
##### MichielDeMey
* [x] Remove all-caps display on Account button since usernames are case-sensitive.
Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
}}
### Monday 13/03/2023 - v3.7.2
{{taskList
##### Calculuschild
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
}}
### Thursday 09/03/2023 - v3.7.1
{{taskList
##### Lucastucious (new contributor!)
* [x] Changed `filter: drop-shadow` to `box-shadow` on text boxes, making PDF text selectable
Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
{{note
**NOTE:** If you create your PDF on a computer with an old version of Mac Preview (v10 or older) you may see shadows appear as solid gray.
}}
##### MichielDeMey
* [x] Updated the Google Drive icon
* [x] Backend fix to unit tests failing intermittently
##### Calculuschild
* [x] Fix PDF pixelation on CoverPage text outlines
}}
### Tuesday 28/02/2023 - v3.7.0
{{taskList
{{note
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
}}
##### Calculuschild
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
* [x] Fix site not displaying on iOS devices
##### 5e-Cleric
* [x] New {{openSans **PHB → COVER PAGE** {{fac,book-front-cover}} }} snippet for V3, which adds a stylish coverpage to your brew! (Thanks to /u/Kaiburr_Kath-Hound on Reddit for providing some of these resources!)
##### MichielDeMey (new contribuor!)
* [x] Fix typo in testing scripts
* [x] Fix "mug" image not using HTTPS
Fixes issues [#2687](https://github.com/naturalcrit/homebrewery/issues/2687)
}}
### Saturday 18/02/2023 - v3.6.1
{{taskList
##### G-Ambatte
* [x] Fix users not being removed from Authors list
Fixes issues [#2674](https://github.com/naturalcrit/homebrewery/issues/2674)
}}
### Monday 23/01/2023 - v3.6.0
{{taskList
##### calculuschild
* [x] Fix Google Drive brews sometimes duplicating
Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
##### Jeddai
* [x] Add unit tests with full coverage for the Homebrewery API
* [x] Add message to refresh the browser if the user is missing an update to the Homebrewery
Fixes issues [#2583](https://github.com/naturalcrit/homebrewery/issues/2583)
}}
\page
{{taskList
##### G-Ambatte
* [x] Auto-compile Themes CSS on development server
##### 5e-Cleric
* [x] Fix cloned brews inheriting the parent view count
}}
### Friday 23/12/2022 - v3.5.0
{{taskList
##### Jeddai
* [x] Only brew owners or invited authors can edit a brew
- Visiting an `/edit` page of a brew that does not list you as an author will result in an error page. Authors can be added to any brew by opening its {{fa,fa-info-circle}} **Properties** menu and typing the author's username (case-sensitive) into the **Invited Authors** bubble.
- Warn user if a newer brew version has been saved on another device
Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
}}
### Saturday 10/12/2022 - v3.4.2
{{taskList
##### Jeddai
* [x] Fix broken tags editor
* [x] Reduce server load to fix some saving issues
Fixes issues [#2322](https://github.com/naturalcrit/homebrewery/issues/2322)
##### G-Ambatte
* [x] Account page help link for Google Drive errors
Fixes issues [#2520](https://github.com/naturalcrit/homebrewery/issues/2520)
}}
### Monday 05/12/2022 - v3.4.1
{{taskList
##### G-Ambatte
* [x] Fix Account page incorrect last login time
Fixes issues [#2521](https://github.com/naturalcrit/homebrewery/issues/2521)
##### Gazook
* [x] Fix crashing on iOS and Safari browsers
Fixes issues [#2531](https://github.com/naturalcrit/homebrewery/issues/2531)
}}
### Monday 28/11/2022 - v3.4.0
{{taskList
##### G-Ambatte
* [x] Fix for Chrome v108 handling of page size
Fixes issues [#2445](https://github.com/naturalcrit/homebrewery/issues/2445), [#2516](https://github.com/naturalcrit/homebrewery/issues/2516)
* [x] New account page with some user info, at {{openSans **USERNAME {{fa,fa-user}} → ACCOUNT {{fa,fa-user}}**}}
Fixes issues [#2049](https://github.com/naturalcrit/homebrewery/issues/2049), [#2043](https://github.com/naturalcrit/homebrewery/issues/2043)
* [x] Fix "Published/Private Brews" buttons on userpage
Fixes issues [#2449](https://github.com/naturalcrit/homebrewery/issues/2449)
##### Gazook
* [x] Make autosave default on for new users
* [x] Added link to our FAQ at {{openSans **NEED HELP? {{fa,fa-question-circle}} → FAQ {{fa,fa-question-circle}}**}}
* [x] Fix curly blocks freezing with long property lists
Fixes issues [#2393](https://github.com/naturalcrit/homebrewery/issues/2393)
* [x] Items can now be removed from {{openSans **RECENT BREWS** {{fas,fa-history}} }}
Fixes issues [#1918](https://github.com/naturalcrit/homebrewery/issues/1918)
* [x] Curly injector syntax `{blue}` highlighting in editor
Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
}}
### Thursday 28/10/2022 - v3.3.1
{{taskList
@@ -800,6 +90,7 @@ Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
##### Gazook:
* [x] Several updates to bug reporting and error popups
@@ -849,10 +140,6 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
Fixes issues: [#1797](https://github.com/naturalcrit/homebrewery/issues/1797), [#2315](https://github.com/naturalcrit/homebrewery/issues/2315), [#2326](https://github.com/naturalcrit/homebrewery/issues/2326), [#2328](https://github.com/naturalcrit/homebrewery/issues/2328)
}}
\page
### Wednesday 31/08/2022 - v3.2.1
{{taskList
@@ -879,6 +166,8 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
Fixes issues: [#2301](https://github.com/naturalcrit/homebrewery/issues/2301), [#2303](https://github.com/naturalcrit/homebrewery/issues/2303), [#2121](https://github.com/naturalcrit/homebrewery/issues/2121)
}}
\page
### Saturday 27/08/2022 - v3.2.0
{{taskList
@@ -1626,7 +915,7 @@ myStyle {color: black}
### Sunday, 29/05/2016 - v2.1.0
- Finally added a syntax for doing spell lists. A bit in-depth about why this took so long. Essentially I'm running out of syntax to use in stardard Markdown. There are too many unique elements in the PHB-style to be mapped. I solved this earlier by stacking certain elements together (eg. an `<hr>` before a `blockquote` turns it into moster state block), but those are getting unweildly. I would like to simply wrap these in `div`s with classes, but unfortunately Markdown stops processing when within HTML blocks. To get around this I wrote my own override to the Markdown parser and lexer to process Markdown within a simple div class wrapper. This should open the door for more unique syntaxes in the future. Big step!
- Override Ctrl+P (and cmd+P) to launch to the print page. Many people try to just print either the editing or share page to get a PDF. While this dones;t make much sense, I do get a ton of issues about it. So now if you try to do this, it'll just bring you imediately to the print page. Everybody wins!
- The onboarding flow has also been confusing a few users (Homepage new save edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
- The onboarding flow has also been confusing a few users (Homepage -> new -> save -> edit page). If you edit the Homepage text now, a Call to Action to save your work will pop-up.
- Added a 'Recently Edited' and 'Recently Viewed' nav item to the edit and share page respectively. Each will remember the last 8 items you edited or viewed and when you viewed it. Makes use of the new title attribute of brews to easy navigatation.
- Paragraphs now indent properly after lists (thanks u/slitjen!)
@@ -1753,4 +1042,4 @@ Massive changelog incoming:
* Added `phb.standalone.css` plus a build system for creating it
* Added page numbers and footer text
* Page accent now flips each page
* Page accent now flips each page

View File

@@ -1,6 +1,7 @@
require('./brewCleanup.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const request = require('superagent');

View File

@@ -1,6 +1,7 @@
require('./brewCompress.less');
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const request = require('superagent');

View File

@@ -1,128 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
require('./combobox.less');
const Combobox = createClass({
displayName : 'Combobox',
getDefaultProps : function() {
return {
className : '',
trigger : 'hover',
default : '',
placeholder : '',
autoSuggest : {
clearAutoSuggestOnClick : true,
suggestMethod : 'includes',
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
},
};
},
getInitialState : function() {
return {
showDropdown : false,
value : '',
options : [...this.props.options],
inputFocused : false
};
},
componentDidMount : function() {
if(this.props.trigger == 'click')
document.addEventListener('click', this.handleClickOutside);
this.setState({
value : this.props.default
});
},
componentWillUnmount : function() {
if(this.props.trigger == 'click')
document.removeEventListener('click', this.handleClickOutside);
},
handleClickOutside : function(e){
// Close dropdown when clicked outside
if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) {
this.handleDropdown(false);
}
},
handleDropdown : function(show){
this.setState({
showDropdown : show,
inputFocused : this.props.autoSuggest.clearAutoSuggestOnClick ? show : false
});
},
handleInput : function(e){
e.persist();
this.setState({
value : e.target.value,
inputFocused : false
}, ()=>{
this.props.onEntry(e);
});
},
handleSelect : function(e){
this.setState({
value : e.currentTarget.getAttribute('data-value')
}, ()=>{this.props.onSelect(this.state.value);});
;
},
renderTextInput : function(){
return (
<div className='dropdown-input item'
onMouseEnter={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(true);} : undefined}
onClick= {this.props.trigger == 'click' ? ()=>{this.handleDropdown(true);} : undefined}>
<input
type='text'
onChange={(e)=>this.handleInput(e)}
value={this.state.value || ''}
placeholder={this.props.placeholder}
onBlur={(e)=>{
if(!e.target.checkValidity()){
this.setState({
value : this.props.default
}, ()=>this.props.onEntry(e));
}
}}
/>
</div>
);
},
renderDropdown : function(dropdownChildren){
if(!this.state.showDropdown) return null;
if(this.props.autoSuggest && !this.state.inputFocused){
const suggestMethod = this.props.autoSuggest.suggestMethod;
const filterOn = _.isString(this.props.autoSuggest.filterOn) ? [this.props.autoSuggest.filterOn] : this.props.autoSuggest.filterOn;
const filteredArrays = filterOn.map((attr)=>{
const children = dropdownChildren.filter((item)=>{
if(suggestMethod === 'includes'){
return item.props[attr]?.toLowerCase().includes(this.state.value.toLowerCase());
} else if(suggestMethod === 'startsWith'){
return item.props[attr]?.toLowerCase().startsWith(this.state.value.toLowerCase());
}
});
return children;
});
dropdownChildren = _.uniq(filteredArrays.flat(1));
}
return (
<div className='dropdown-options'>
{dropdownChildren}
</div>
);
},
render : function () {
const dropdownChildren = this.state.options.map((child, i)=>{
const clone = React.cloneElement(child, { onClick: (e)=>this.handleSelect(e) });
return clone;
});
return (
<div className={`dropdown-container ${this.props.className}`}
ref='dropdown'
onMouseLeave={this.props.trigger == 'hover' ? ()=>{this.handleDropdown(false);} : undefined}>
{this.renderTextInput()}
{this.renderDropdown(dropdownChildren)}
</div>
);
}
});
module.exports = Combobox;

View File

@@ -1,50 +0,0 @@
.dropdown-container {
position:relative;
input {
width: 100%;
}
.dropdown-options {
position:absolute;
background-color: white;
z-index: 100;
width: 100%;
border: 1px solid gray;
overflow-y: auto;
max-height: 200px;
&::-webkit-scrollbar {
width: 14px;
}
&::-webkit-scrollbar-track {
background: #ffffff;
}
&::-webkit-scrollbar-thumb {
background-color: #949494;
border-radius: 10px;
border: 3px solid #ffffff;
}
.item {
position:relative;
font-size: 11px;
font-family: Open Sans;
padding: 5px;
cursor: default;
margin: 0 3px;
//border-bottom: 1px solid darkgray;
&:hover {
filter: brightness(120%);
background-color: rgb(163, 163, 163);
}
.detail {
width:100%;
text-align: left;
color: rgb(124, 124, 124);
font-style:italic;
font-size: 9px;
}
}
}
}

View File

@@ -1,29 +0,0 @@
// Dialog box, for popups and modal blocking messages
const React = require('react');
const { useRef, useEffect } = React;
function Dialog({ dismissKey, closeText = 'Close', blocking = false, ...rest }) {
const dialogRef = useRef(null);
useEffect(()=>{
if(!dismissKey || !localStorage.getItem(dismissKey)) {
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
}
}, []);
const dismiss = ()=>{
dismissKey && localStorage.setItem(dismissKey, true);
dialogRef.current?.close();
};
return (
<dialog ref={dialogRef} onCancel={dismiss} {...rest}>
{rest.children}
<button className='dismiss' onClick={dismiss}>
{closeText}
</button>
</dialog>
);
};
export default Dialog;

View File

@@ -1,8 +1,9 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
require('./brewRenderer.less');
const React = require('react');
const { useState, useRef, useEffect } = React;
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
const Markdown = require('naturalcrit/markdown.js');
@@ -12,236 +13,227 @@ const ErrorBar = require('./errorBar/errorBar.jsx');
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
const NotificationPopup = require('./notificationPopup/notificationPopup.jsx');
const Frame = require('react-frame-component').default;
const dedent = require('dedent-tabs').default;
const { printCurrentBrew } = require('../../../shared/helpers.js');
const DOMPurify = require('dompurify');
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
const Themes = require('themes/themes.json');
const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50;
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' type="text/css" rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`;
const BrewRenderer = createClass({
displayName : 'BrewRenderer',
getDefaultProps : function() {
return {
text : '',
style : '',
renderer : 'legacy',
theme : '5ePHB',
errors : []
};
},
getInitialState : function() {
let pages;
if(this.props.renderer == 'legacy') {
pages = this.props.text.split('\\page');
} else {
pages = this.props.text.split(/^\\page$/gm);
}
//v=====----------------------< Brew Page Component >---------------------=====v//
const BrewPage = (props)=>{
props = {
contents : '',
index : 0,
...props
};
const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig);
return <div className={props.className} id={`p${props.index + 1}`} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
</div>;
};
return {
viewablePageNumber : 0,
height : 0,
isMounted : false,
pages : pages,
usePPR : pages.length >= PPR_THRESHOLD,
visibility : 'hidden',
initialContent : `<!DOCTYPE html><html><head>
<link href="//use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" />
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
<link href='/homebrew/bundle.css' rel='stylesheet' />
<base target=_blank>
</head><body style='overflow: hidden'><div></div></body></html>`
};
},
height : 0,
lastRender : <div></div>,
//v=====--------------------< Brew Renderer Component >-------------------=====v//
const renderedPages = [];
let rawPages = [];
componentWillUnmount : function() {
window.removeEventListener('resize', this.updateSize);
},
const BrewRenderer = (props)=>{
props = {
text : '',
style : '',
renderer : 'legacy',
theme : '5ePHB',
lang : '',
errors : [],
currentEditorPage : 0,
...props
};
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
});
}
},
const [state, setState] = useState({
viewablePageNumber : 0,
height : PAGE_HEIGHT,
isMounted : false,
visibility : 'hidden',
});
updateSize : function() {
this.setState({
height : this.refs.main.parentNode.clientHeight,
});
},
const mainRef = useRef(null);
if(props.renderer == 'legacy') {
rawPages = props.text.split('\\page');
} else {
rawPages = props.text.split(/^\\page$/gm);
}
useEffect(()=>{ // Unmounting steps
return ()=>{window.removeEventListener('resize', updateSize);};
}, []);
const updateSize = ()=>{
setState((prevState)=>({
...prevState,
height : mainRef.current.parentNode.clientHeight,
}));
};
const handleScroll = (e)=>{
handleScroll : function(e){
const target = e.target;
setState((prevState)=>({
...prevState,
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
this.setState((prevState)=>({
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * prevState.pages.length)
}));
};
},
const isInView = (index)=>{
if(!state.isMounted)
return false;
shouldRender : function(pageText, index){
if(!this.state.isMounted) return false;
if(index == props.currentEditorPage) //Already rendered before this step
return false;
const viewIndex = this.state.viewablePageNumber;
if(index == viewIndex - 3) return true;
if(index == viewIndex - 2) return true;
if(index == viewIndex - 1) return true;
if(index == viewIndex) return true;
if(index == viewIndex + 1) return true;
if(index == viewIndex + 2) return true;
if(index == viewIndex + 3) return true;
if(Math.abs(index - state.viewablePageNumber) <= 3)
return true;
//Check for style tages
if(pageText.indexOf('<style>') !== -1) return true;
return false;
};
},
const renderPageInfo = ()=>{
return <div className='pageInfo' ref={mainRef}>
renderPageInfo : function(){
return <div className='pageInfo' ref='main'>
<div>
{props.renderer}
{this.props.renderer}
</div>
<div>
{state.viewablePageNumber + 1} / {rawPages.length}
{this.state.viewablePageNumber + 1} / {this.state.pages.length}
</div>
</div>;
};
},
const renderDummyPage = (index)=>{
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>;
};
},
const renderStyle = ()=>{
if(!props.style) return;
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
};
renderStyle : function() {
if(!this.props.style) return;
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />;
},
const renderPage = (pageText, index)=>{
if(props.renderer == 'legacy') {
const html = MarkdownLegacy.render(pageText);
return <BrewPage className='page phb' index={index} key={index} contents={html} />;
} else {
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&nbsp;\n\\column\n&nbsp;`; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
const html = Markdown.render(pageText, index);
return <BrewPage className='page' index={index} key={index} contents={html} />;
return (
<div className='page' id={`p${index + 1}`} key={index} >
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: Markdown.render(pageText) }} />
</div>
);
}
};
},
const renderPages = ()=>{
if(props.errors && props.errors.length)
return renderedPages;
if(rawPages.length != renderedPages.length) // Re-render all pages when page count changes
renderedPages.length = 0;
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
_.forEach(rawPages, (page, index)=>{
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
renderedPages[index] = renderPage(page, index); // Render any page not yet rendered, but only re-render those in PPR range
renderPages : function(){
if(this.state.usePPR){
return _.map(this.state.pages, (page, index)=>{
if(this.shouldRender(page, index) && typeof window !== 'undefined'){
return this.renderPage(page, index);
} else {
return this.renderDummyPage(index);
}
});
}
if(this.props.errors && this.props.errors.length) return this.lastRender;
this.lastRender = _.map(this.state.pages, (page, index)=>{
if(typeof window !== 'undefined') {
return this.renderPage(page, index);
} else {
return this.renderDummyPage(index);
}
});
return renderedPages;
};
return this.lastRender;
},
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
const P_KEY = 80;
if(e.keyCode == P_KEY && props.allowPrint) printCurrentBrew();
if(e.keyCode == P_KEY) {
e.stopPropagation();
e.preventDefault();
}
};
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
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
updateSize();
window.addEventListener('resize', updateSize);
renderPages(); //Make sure page is renderable before showing
setState((prevState)=>({
...prevState,
this.updateSize();
window.addEventListener('resize', this.updateSize);
this.renderPages(); //Make sure page is renderable before showing
this.setState({
isMounted : true,
visibility : 'visible'
}));
});
}, 100);
};
},
const emitClick = ()=>{ // Allow clicks inside iFrame to interact with dropdowns, etc. from outside
if(!window || !document) return;
document.dispatchEvent(new MouseEvent('click'));
};
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;
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
const themePath = props.theme ?? '5ePHB';
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
return (
<>
{/*render dummy page while iFrame is mounting.*/}
{!state.isMounted
? <div className='brewRenderer' onScroll={handleScroll}>
<div className='pages'>
{renderDummyPage(1)}
return (
<React.Fragment>
{!this.state.isMounted
? <div className='brewRenderer' onScroll={this.handleScroll}>
<div className='pages' ref='pages'>
{this.renderDummyPage(1)}
</div>
</div>
</div>
: null}
: null}
<ErrorBar errors={props.errors} />
<div className='popups'>
<RenderWarnings />
<NotificationPopup />
</div>
<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 }}>
{/*render in iFrame so broken code doesn't crash the site.*/}
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
style={{ width: '100%', height: '100%', visibility: state.visibility }}
contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
>
<div className={'brewRenderer'}
onScroll={handleScroll}
onKeyDown={handleControlKeys}
tabIndex={-1}
style={{ height: state.height }}>
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
{baseThemePath &&
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>
}
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type='text/css' rel='stylesheet'/>
{/* Apply CSS from Style tab and render pages from Markdown tab */}
{state.isMounted
&&
<>
{renderStyle()}
<div className='pages' lang={`${props.lang || 'en'}`}>
{renderPages()}
</div>
</>
}
</div>
</Frame>
{renderPageInfo()}
</>
);
};
<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>
);
}
});
module.exports = BrewRenderer;

View File

@@ -1,79 +1,46 @@
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
.brewRenderer {
.brewRenderer{
will-change : transform;
overflow-y : scroll;
:where(.pages) {
.pages{
margin : 30px 0px;
& > :where(.page) {
width : 215.9mm;
height : 279.4mm;
&>.page{
margin-right : auto;
margin-bottom : 30px;
margin-left : auto;
box-shadow : 1px 4px 14px #000000;
box-shadow : 1px 4px 14px #000;
}
}
&::-webkit-scrollbar {
width: 20px;
&:horizontal{
height: 20px;
width:auto;
}
&-thumb {
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
&:horizontal{
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
}
}
&-corner {
visibility: hidden;
}
}
}
.pane { position : relative; }
.pageInfo {
.pane{
position : relative;
}
.pageInfo{
position : absolute;
right : 17px;
bottom : 0;
z-index : 1000;
background-color : #333;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
div {
display : inline-block;
display: inline-block;
padding : 8px 10px;
&:not(:last-child) { border-right : 1px solid #666666; }
}
}
.ppr_msg {
position : absolute;
bottom : 0;
left : 0px;
z-index : 1000;
padding : 8px 10px;
font-size : 10px;
font-weight : 800;
color : white;
background-color : #333333;
}
@media print {
.brewRenderer {
height: 100%;
overflow-y: unset;
.pages {
margin: 0px;
&>.page {
box-shadow: unset;
}
&: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;
}

View File

@@ -2,6 +2,7 @@ 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',

View File

@@ -1,45 +1,92 @@
require('./notificationPopup.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames'); //Unused variable
import Dialog from '../../../components/dialog.jsx';
const DISMISS_KEY = 'dismiss_notification08-27-22';
const DISMISS_KEY = 'dismiss_notification12-04-23';
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
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>
const NotificationPopup = ()=>{
return <Dialog className='notificationPopup' dismissKey={DISMISS_KEY} closeText={DISMISS_BUTTON} >
<div className='header'>
<i className='fas fa-info-circle info'></i>
<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>
<li key='psa'>
<em>Don't store IMAGES in Google Drive</em><br />
Google Drive is not an image service, and will block images from being used
in brews if they get more views than expected. Google has confirmed they won't fix
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
</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='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!&nbsp;
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
See the FAQ
</a> to learn how to avoid losing your work!
</li>
</ul>
</Dialog>;
};
<li key='faq'>
<em>Protect your work! </em> <br />
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!&nbsp;
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
See the FAQ
</a> to learn how to avoid losing your work!
</li>
</>
);
}
},
checkNotifications : function(){
const hideDismiss = localStorage.getItem(DISMISS_KEY);
if(hideDismiss) return this.setState({ notifications: {} });
this.setState({
notifications : _.mapValues(this.notifications, (fn)=>{ return fn(); }) //Convert notification functions into their return text value
});
},
dismiss : function(){
localStorage.setItem(DISMISS_KEY, true);
this.checkNotifications();
},
render : function(){
if(_.isEmpty(this.state.notifications)) return null;
return <div className='notificationPopup'>
<i className='fas fa-times dismiss' onClick={this.dismiss}/>
<i className='fas fa-info-circle info' />
<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;

View File

@@ -1,60 +1,64 @@
.popups {
.popups{
position : fixed;
top : @navbarHeight;
right : 24px;
right : 15px;
z-index : 10001;
width : 450px;
}
.notificationPopup {
.notificationPopup{
position : relative;
display : inline-block;
width : 100%;
padding : 15px;
padding-bottom : 10px;
padding-left : 25px;
color : white;
background-color : @blue;
border : none;
&[open] { display : inline-block; }
a {
color : white;
a{
color : #e0e5c1;
font-weight : 800;
color : #E0E5C1;
}
i.info {
i.info{
position : absolute;
top : 12px;
left : 12px;
font-size : 2.5em;
opacity : 0.8;
font-size : 2.5em;
}
button.dismiss {
position : absolute;
top : 10px;
right : 10px;
cursor : pointer;
background-color : transparent;
opacity : 0.6;
&:hover { opacity : 1; }
i.dismiss{
position : absolute;
top : 10px;
right : 10px;
cursor : pointer;
opacity : 0.6;
&:hover{
opacity : 1;
}
}
.header { padding-left : 50px; }
small {
font-size : 0.6em;
.header {
padding-left : 50px;
}
small{
opacity : 0.7;
font-size : 0.6em;
}
h3 {
h3{
font-size : 1.1em;
font-weight : 800;
}
ul {
ul{
margin-top : 15px;
font-size : 0.8em;
list-style-position : outside;
list-style-type : disc;
li {
margin-top : 1.4em;
li{
font-size : 0.8em;
line-height : 1.4em;
em { font-weight : 800; }
margin-top : 1.4em;
em{
font-weight : 800;
}
}
}
}

View File

@@ -5,14 +5,11 @@ const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const dedent = require('dedent-tabs').default;
const Markdown = require('../../../shared/naturalcrit/markdown.js');
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const SNIPPETBAR_HEIGHT = 25;
const DEFAULT_STYLE_TEXT = dedent`
/*=======--- Example CSS styling ---=======*/
@@ -35,22 +32,16 @@ const Editor = createClass({
onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
reportError : ()=>{},
editorTheme : 'default',
renderer : 'legacy'
renderer : 'legacy'
};
},
getInitialState : function() {
return {
editorTheme : this.props.editorTheme,
view : 'text' //'text', 'style', 'meta'
view : 'text' //'text', 'style', 'meta'
};
},
editor : React.createRef(null),
codeEditor : React.createRef(null),
isText : function() {return this.state.view == 'text';},
isStyle : function() {return this.state.view == 'style';},
isMeta : function() {return this.state.view == 'meta';},
@@ -59,13 +50,6 @@ const Editor = createClass({
this.updateEditorSize();
this.highlightCustomMarkdown();
window.addEventListener('resize', this.updateEditorSize);
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
if(editorTheme) {
this.setState({
editorTheme : editorTheme
});
}
},
componentWillUnmount : function() {
@@ -83,15 +67,15 @@ const Editor = createClass({
},
updateEditorSize : function() {
if(this.codeEditor.current) {
let paneHeight = this.editor.current.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT;
this.codeEditor.current.codeMirror.setSize(null, paneHeight);
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.codeEditor.current?.injectText(injectText, false);
this.refs.codeEditor?.injectText(injectText, false);
},
handleViewChange : function(newView){
@@ -102,7 +86,7 @@ const Editor = createClass({
},
getCurrentPage : function(){
const lines = this.props.brew.text.split('\n').slice(0, this.codeEditor.current.getCursorPosition().line + 1);
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)
@@ -114,9 +98,9 @@ const Editor = createClass({
},
highlightCustomMarkdown : function(){
if(!this.codeEditor.current) return;
if(!this.refs.codeEditor) return;
if(this.state.view === 'text') {
const codeMirror = this.codeEditor.current.codeMirror;
const codeMirror = this.refs.codeEditor.codeMirror;
codeMirror.operation(()=>{ // Batch CodeMirror styling
//reset custom text styles
@@ -153,53 +137,17 @@ const Editor = createClass({
codeMirror.addLineClass(lineNumber, 'text', 'columnSplit');
}
// definition lists
if(line.includes('::')){
if(/^:*$/.test(line) == true){ return };
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
let match;
while ((match = regex.exec(line)) != null){
codeMirror.markText({ line: lineNumber, ch: match.indices[0][0] }, { line: lineNumber, ch: match.indices[0][1] }, { className: 'dl-highlight' });
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
const ddIndex = match.indices[2][0];
let colons = /::/g;
let colonMatches = colons.exec(match[2]);
if(colonMatches !== null){
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
}
}
}
// Subscript & Superscript
if(line.includes('^')) {
let startIndex = line.indexOf('^');
const superRegex = /\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^/gy;
const subRegex = /\^\^(?!\s)(?=([^\n\^]*[^\s\^]))\1\^\^/gy;
while (startIndex >= 0) {
superRegex.lastIndex = subRegex.lastIndex = startIndex;
let isSuper = false;
let match = subRegex.exec(line) || superRegex.exec(line);
if (match) {
isSuper = !subRegex.lastIndex;
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
}
startIndex = line.indexOf('^', Math.max(startIndex + 1, subRegex.lastIndex, superRegex.lastIndex));
}
}
// Highlight injectors {style}
if(line.includes('{') && line.includes('}')){
const regex = /(?:^|[^{\n])({(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\2})/gm;
const regex = /(?<!{){(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
let match;
while ((match = regex.exec(line)) != null) {
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
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;
const regex = /{{(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1 *|}}/g;
let match;
let blockCount = 0;
while ((match = regex.exec(line)) != null) {
@@ -218,39 +166,11 @@ const Editor = createClass({
// Highlight block divs {{\n Content \n}}
let endCh = line.length+1;
const match = line.match(/^ *{{(?=((?:[:=](?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':={}\s]*)*))\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' });
}
// Emojis
if(line.match(/:[^\s:]+:/g)) {
let startIndex = line.indexOf(':');
const emojiRegex = /:[^\s:]+:/gy;
while (startIndex >= 0) {
emojiRegex.lastIndex = startIndex;
let match = emojiRegex.exec(line);
if (match) {
let tokens = Markdown.marked.lexer(match[0]);
tokens = tokens[0].tokens.filter(t => t.type == 'emoji')
if (!tokens.length)
return;
let startPos = { line: lineNumber, ch: match.index };
let endPos = { line: lineNumber, ch: match.index + match[0].length };
// Iterate over conflicting marks and clear them
var marks = codeMirror.findMarks(startPos, endPos);
marks.forEach(function(marker) {
marker.clear();
});
codeMirror.markText(startPos, endPos, { className: 'emoji' });
}
startIndex = line.indexOf(':', Math.max(startIndex + 1, emojiRegex.lastIndex));
}
}
}
});
});
@@ -305,23 +225,23 @@ const Editor = createClass({
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
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.codeEditor.current.codeMirror.scrollTo(null, currentY);
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.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
targetY = this.refs.codeEditor.codeMirror.heightAtLine(targetLine, 'local', true);
// End when close enough
if(Math.abs(targetY - currentY) < 1) {
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
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);
@@ -331,14 +251,7 @@ const Editor = createClass({
//Called when there are changes to the editor's dimensions
update : function(){
this.codeEditor.current?.updateSize();
},
updateEditorTheme : function(newTheme){
window.localStorage.setItem(EDITOR_THEME_KEY, newTheme);
this.setState({
editorTheme : newTheme
});
this.refs.codeEditor?.updateSize();
},
//Called by CodeEditor after document switch, so Snippetbar can refresh UndoHistory
@@ -350,25 +263,23 @@ const Editor = createClass({
if(this.isText()){
return <>
<CodeEditor key='codeEditor'
ref={this.codeEditor}
ref='codeEditor'
language='gfm'
view={this.state.view}
value={this.props.brew.text}
onChange={this.props.onTextChange}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent} />
</>;
}
if(this.isStyle()){
return <>
<CodeEditor key='codeEditor'
ref={this.codeEditor}
ref='codeEditor'
language='css'
view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange}
enableFolding={true}
editorTheme={this.state.editorTheme}
enableFolding={false}
rerenderParent={this.rerenderParent} />
</>;
}
@@ -380,35 +291,26 @@ const Editor = createClass({
rerenderParent={this.rerenderParent} />
<MetadataEditor
metadata={this.props.brew}
onChange={this.props.onMetaChange}
reportError={this.props.reportError}/>
onChange={this.props.onMetaChange} />
</>;
}
},
redo : function(){
return this.codeEditor.current?.redo();
return this.refs.codeEditor?.redo();
},
historySize : function(){
return this.codeEditor.current?.historySize();
return this.refs.codeEditor?.historySize();
},
undo : function(){
return this.codeEditor.current?.undo();
},
foldCode : function(){
return this.codeEditor.current?.foldAllCode();
},
unfoldCode : function(){
return this.codeEditor.current?.unfoldAllCode();
return this.refs.codeEditor?.undo();
},
render : function(){
return (
<div className='editor' ref={this.editor}>
<div className='editor' ref='main'>
<SnippetBar
brew={this.props.brew}
view={this.state.view}
@@ -419,12 +321,7 @@ const Editor = createClass({
theme={this.props.brew.theme}
undo={this.undo}
redo={this.redo}
foldCode={this.foldCode}
unfoldCode={this.unfoldCode}
historySize={this.historySize()}
currentEditorTheme={this.state.editorTheme}
updateEditorTheme={this.updateEditorTheme}
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
historySize={this.historySize()} />
{this.renderEditor()}
</div>

View File

@@ -1,105 +1,65 @@
@import 'themes/codeMirror/customEditorStyles.less';
.editor {
.editor{
position : relative;
width : 100%;
.codeEditor {
.codeEditor{
height : 100%;
.pageLine {
.pageLine{
background : #33333328;
border-top : #333399 solid 1px;
border-top : #339 solid 1px;
}
.editor-page-count {
float : right;
.editor-page-count{
color : grey;
float : right;
}
.columnSplit {
font-style : italic;
color : grey;
background-color : fade(#229999, 15%);
border-bottom : #229999 solid 1px;
.columnSplit{
font-style : italic;
color : grey;
background-color : fade(#299, 15%);
border-bottom : #299 solid 1px;
}
.define {
&:not(.term):not(.definition) {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
&.term { color : rgb(96, 117, 143); }
&.definition { color : rgb(97, 57, 178); }
}
.block:not(.cm-comment) {
.block{
color : purple;
font-weight : bold;
color : purple;
//font-style: italic;
}
.inline-block:not(.cm-comment) {
.inline-block{
color : red;
font-weight : bold;
color : red;
//font-style: italic;
}
.injection:not(.cm-comment) {
font-weight : bold;
.injection{
color : green;
font-weight : bold;
}
.emoji:not(.cm-comment) {
margin-left : 2px;
color : #360034;
background : #ffc8ff;
border-radius : 6px;
font-weight : bold;
padding-bottom : 1px;
outline-offset : -2px;
outline : solid 2px #ff96fc;
}
.superscript:not(.cm-comment) {
font-weight : bold;
color : goldenrod;
vertical-align : super;
font-size : 0.9em;
}
.subscript:not(.cm-comment) {
font-weight : bold;
color : rgb(123, 123, 15);
vertical-align : sub;
font-size : 0.9em;
}
.dl-highlight {
&.dl-colon-highlight {
font-weight : bold;
color : #949494;
background : #E5E5E5;
border-radius : 3px;
}
&.dt-highlight { color : rgb(96, 117, 143); }
&.dd-highlight { color : rgb(97, 57, 178); }
}
}
.brewJump {
position : absolute;
right : 20px;
bottom : 20px;
z-index : 1000000;
display : flex;
align-items : center;
justify-content : center;
width : 30px;
height : 30px;
cursor : pointer;
background-color : @teal;
.tooltipLeft('Jump to brew page');
.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%;
z-index : 9;
font-size : 13px;
color : black;
span { padding : 2px 5px; }
.editorToolbar{
position: absolute;
top: 5px;
left: 50%;
color: black;
font-size: 13px;
z-index: 9;
span {
padding: 2px 5px;
}
}
}

View File

@@ -3,24 +3,18 @@ require('./metadataEditor.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const request = require('../../utils/request-middleware.js');
const cx = require('classnames');
const request = require('superagent');
const Nav = require('naturalcrit/nav/nav.jsx');
const Combobox = require('client/components/combobox.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
const Themes = require('themes/themes.json');
const validations = require('./validations.js');
const validations = require('./validations.js')
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
const homebreweryThumbnail = require('../../thumbnail.png');
const callIfExists = (val, fn, ...args)=>{
if(val[fn]) {
val[fn](...args);
}
};
const MetadataEditor = createClass({
displayName : 'MetadataEditor',
getDefaultProps : function() {
@@ -35,11 +29,9 @@ const MetadataEditor = createClass({
authors : [],
systems : [],
renderer : 'legacy',
theme : '5ePHB',
lang : 'en'
theme : '5ePHB'
},
onChange : ()=>{},
reportError : ()=>{}
onChange : ()=>{}
};
},
@@ -61,26 +53,28 @@ const MetadataEditor = createClass({
},
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){
callIfExists(e.target, 'setCustomValidity', '');
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');
callIfExists(e.target, 'setCustomValidity', errMessage);
callIfExists(e.target, 'reportValidity');
}
e.target.setCustomValidity(errMessage);
e.target.reportValidity();
};
},
handleSystem : function(system, e){
@@ -113,11 +107,6 @@ const MetadataEditor = createClass({
this.props.onChange(this.props.metadata);
},
handleLanguage : function(languageCode){
this.props.metadata.lang = languageCode;
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;
@@ -129,12 +118,8 @@ const MetadataEditor = createClass({
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
.send()
.end((err, res)=>{
if(err) {
this.props.reportError(err);
} else {
window.location.href = '/';
}
.end(function(err, res){
window.location.href = '/';
});
},
@@ -196,10 +181,6 @@ const MetadataEditor = createClass({
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
{`${theme.renderer} : ${theme.name}`}
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
<div className='preview'>
<h6>{`${theme.name}`} preview</h6>
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
</div>
</div>;
});
};
@@ -209,14 +190,14 @@ const MetadataEditor = createClass({
if(this.props.metadata.renderer == 'legacy') {
dropdown =
<Nav.dropdown className='disabled value' trigger='disabled'>
<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 className='value' trigger='click'>
<Nav.dropdown trigger='click'>
<div>
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
</div>
@@ -231,47 +212,6 @@ const MetadataEditor = createClass({
</div>;
},
renderLanguageDropdown : function(){
const langCodes = ['en', 'de', 'de-ch', 'fr', 'ja', 'es', 'it', 'sv', 'ru', 'zh-Hans', 'zh-Hant'];
const listLanguages = ()=>{
return _.map(langCodes.sort(), (code, index)=>{
const localName = new Intl.DisplayNames([code], { type: 'language' });
const englishName = new Intl.DisplayNames('en', { type: 'language' });
return <div className='item' title={`${englishName.of(code)}`} key={`${index}`} data-value={`${code}`} data-detail={`${localName.of(code)}`}>
{`${code}`}
<div className='detail'>{`${localName.of(code)}`}</div>
</div>;
});
};
const debouncedHandleFieldChange = _.debounce(this.handleFieldChange, 500);
return <div className='field language'>
<label>language</label>
<div className='value'>
<Combobox trigger='click'
className='language-dropdown'
default={this.props.metadata.lang || ''}
placeholder='en'
onSelect={(value)=>this.handleLanguage(value)}
onEntry={(e)=>{
e.target.setCustomValidity(''); //Clear the validation popup while typing
debouncedHandleFieldChange('lang', e);
}}
options={listLanguages()}
autoSuggest={{
suggestMethod : 'startsWith',
clearAutoSuggestOnClick : true,
filterOn : ['data-value', 'data-detail', 'title']
}}
>
</Combobox>
<small>Sets the HTML Lang property for your brew. May affect hyphenation or spellcheck.</small>
</div>
</div>;
},
renderRenderOptions : function(){
if(!global.enable_v3) return;
@@ -307,8 +247,6 @@ const MetadataEditor = createClass({
render : function(){
return <div className='metadataEditor'>
<h1 className='sectionHead'>Brew</h1>
<div className='field title'>
<label>title</label>
<input type='text' className='value'
@@ -342,6 +280,8 @@ const MetadataEditor = createClass({
values={this.props.metadata.tags}
onChange={(e)=>this.handleFieldChange('tags', e)}/>
{this.renderAuthors()}
<div className='field systems'>
<label>systems</label>
<div className='value'>
@@ -349,29 +289,10 @@ const MetadataEditor = createClass({
</div>
</div>
{this.renderLanguageDropdown()}
{this.renderThemeDropdown()}
{this.renderRenderOptions()}
<hr/>
<h1 className='sectionHead'>Authors</h1>
{this.renderAuthors()}
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true}
values={this.props.metadata.invitedAuthors}
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
<hr/>
<h1 className='sectionHead'>Privacy</h1>
<div className='field publish'>
<label>publish</label>
<div className='value'>

View File

@@ -2,7 +2,7 @@
.metadataEditor{
position : absolute;
z-index : 5;
z-index : 10000;
box-sizing : border-box;
width : 100%;
padding : 25px;
@@ -10,15 +10,6 @@
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
overflow-y : auto;
.sectionHead {
font-weight: 1000;
margin: 20px 0;
&:first-of-type {
margin-top: 0;
}
}
& > div {
margin-bottom: 10px;
}
@@ -35,16 +26,12 @@
flex-direction: column;
flex: 5 0 200px;
gap: 10px;
}
.field{
display : flex;
flex-wrap : wrap;
width : 100%;
min-width : 200px;
position : relative;
&>label{
width : 80px;
font-size : 11px;
@@ -61,9 +48,6 @@
}
input[type='text'], textarea {
border : 1px solid gray;
&:focus {
outline: 1px solid #444;
}
}
&.thumbnail{
height : 1.4em;
@@ -94,17 +78,6 @@
font-size : 0.8em;
}
}
&.language .language-dropdown {
max-width : 150px;
z-index : 200;
}
small {
font-size : 0.6em;
font-style : italic;
line-height : 1.4em;
display : inline-block;
}
}
@@ -155,6 +128,10 @@
button.unpublish{
.button(@silver);
}
small{
font-size : 0.6em;
font-style : italic;
}
}
.delete.field .value{
@@ -171,8 +148,9 @@
font-size : 13.33px;
.navDropdownContainer {
background-color : white;
width : 100%;
position : relative;
z-index : 100;
z-index : 500;
&.disabled {
font-style :italic;
font-style : italic;
@@ -193,51 +171,24 @@
}
.navDropdown {
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
position : absolute;
width : 100%;
position : absolute;
width : 100%;
.item {
padding : 3px 3px;
border-top : 1px solid rgb(118, 118, 118);
position : relative;
overflow : visible;
padding : 3px 3px;
border-top : 1px solid rgb(118, 118, 118);
position : relative;
overflow : hidden;
background-color : white;
.preview {
display : flex;
flex-direction: column;
background : #ccc;
border-radius : 5px;
box-shadow : 0 0 5px black;
width : 200px;
color :black;
position : absolute;
top : 0;
right : 0;
opacity : 0;
transition : opacity 250ms ease;
z-index : 1;
overflow :hidden;
h6 {
font-weight : 900;
padding-inline:1em;
padding-block :.5em;
border-bottom :2px solid hsl(0,0%,40%);
}
}
&:hover {
background-color : @blue;
color : white;
color : white;
}
&:hover > .preview {
opacity: 1;
}
>img {
mask-image : linear-gradient(90deg, transparent, black 20%);
img {
mask-image : linear-gradient(90deg, transparent, black 20%);
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
position : absolute;
right : 0;
top : 0px;
width : 50%;
height : 100%;
position : absolute;
left : ~"max(100px, 100% - 300px)";
top : 0px;
}
}
}
@@ -245,7 +196,6 @@
}
.field .list {
display: flex;
flex: 1 0;
flex-wrap: wrap;
> * {

View File

@@ -23,9 +23,9 @@ module.exports = {
}
}
],
lang : [
language : [
(value)=>{
return new RegExp(/^([a-zA-Z]{2,3})(-[a-zA-Z]{4})?(-(?:[0-9]{3}|[a-zA-Z]{2}))?$/).test(value) === false && (value.length > 0) ? 'Invalid language code.' : null;
return new RegExp(/[a-z]{2,3}(-.*)?/).test(value || '') === false ? 'Invalid language code.' : null;
}
]
};

View File

@@ -1,4 +1,3 @@
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
require('./snippetbar.less');
const React = require('react');
const createClass = require('create-react-class');
@@ -16,10 +15,8 @@ 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 EditorThemes = require('build/homebrew/codeMirror/editorThemes.json');
const execute = function(val, props){
if(_.isFunction(val)) return val(props);
const execute = function(val, brew){
if(_.isFunction(val)) return val(brew);
return val;
};
@@ -27,28 +24,23 @@ const Snippetbar = createClass({
displayName : 'SnippetBar',
getDefaultProps : function() {
return {
brew : {},
view : 'text',
onViewChange : ()=>{},
onInject : ()=>{},
onToggle : ()=>{},
showEditButtons : true,
renderer : 'legacy',
undo : ()=>{},
redo : ()=>{},
historySize : ()=>{},
foldCode : ()=>{},
unfoldCode : ()=>{},
updateEditorTheme : ()=>{},
cursorPos : {}
brew : {},
view : 'text',
onViewChange : ()=>{},
onInject : ()=>{},
onToggle : ()=>{},
showEditButtons : true,
renderer : 'legacy',
undo : ()=>{},
redo : ()=>{},
historySize : ()=>{}
};
},
getInitialState : function() {
return {
renderer : this.props.renderer,
themeSelector : false,
snippets : []
renderer : this.props.renderer,
snippets : []
};
},
@@ -74,7 +66,6 @@ const Snippetbar = createClass({
}
},
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
@@ -103,33 +94,6 @@ const Snippetbar = createClass({
this.props.onInject(injectedText);
},
toggleThemeSelector : function(e){
if(e.target.tagName != 'SELECT'){
this.setState({
themeSelector : !this.state.themeSelector
});
}
},
changeTheme : function(e){
if(e.target.value == this.props.currentEditorTheme) return;
this.props.updateEditorTheme(e.target.value);
this.setState({
showThemeSelector : false,
});
},
renderThemeSelector : function(){
return <div className='themeSelector'>
<select value={this.props.currentEditorTheme} onChange={this.changeTheme} >
{EditorThemes.map((theme, key)=>{
return <option key={key} value={theme}>{theme}</option>;
})}
</select>
</div>;
},
renderSnippetGroups : function(){
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
@@ -141,7 +105,6 @@ const Snippetbar = createClass({
snippets={snippetGroup.snippets}
key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick}
cursorPos={this.props.cursorPos}
/>;
});
},
@@ -149,22 +112,6 @@ const Snippetbar = createClass({
renderEditorButtons : function(){
if(!this.props.showEditButtons) return;
let foldButtons;
if(this.props.view == 'text'){
foldButtons =
<>
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
onClick={this.props.foldCode} >
<i className='fas fa-compress-alt' />
</div>
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
onClick={this.props.unfoldCode} >
<i className='fas fa-expand-alt' />
</div>
</>;
}
return <div className='editors'>
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
onClick={this.props.undo} >
@@ -174,14 +121,6 @@ const Snippetbar = createClass({
onClick={this.props.redo} >
<i className='fas fa-redo' />
</div>
<div className='divider'></div>
{foldButtons}
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
onClick={this.toggleThemeSelector} >
<i className='fas fa-palette' />
{this.state.themeSelector && this.renderThemeSelector()}
</div>
<div className='divider'></div>
<div className={cx('text', { selected: this.props.view === 'text' })}
onClick={()=>this.props.onViewChange('text')}>
@@ -224,23 +163,15 @@ const SnippetGroup = createClass({
onSnippetClick : function(){},
};
},
handleSnippetClick : function(e, snippet){
e.stopPropagation();
this.props.onSnippetClick(execute(snippet.gen, this.props));
handleSnippetClick : function(snippet){
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
},
renderSnippets : function(snippets){
return _.map(snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
<i className={snippet.icon} />
<span className='name'title={snippet.name}>{snippet.name}</span>
{snippet.experimental && <span className='beta'>beta</span>}
{snippet.subsnippets && <>
<i className='fas fa-caret-right'></i>
<div className='dropdown side'>
{this.renderSnippets(snippet.subsnippets)}
</div></>}
{snippet.name}
</div>;
});
},
@@ -251,8 +182,9 @@ const SnippetGroup = createClass({
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets(this.props.snippets)}
{this.renderSnippets()}
</div>
</div>;
},
});

View File

@@ -1,192 +1,113 @@
@import (less) './client/icons/customIcons.less';
@import (less) '././././themes/fonts/5e/fonts.less';
.snippetBar {
.snippetBar{
@menuHeight : 25px;
position : relative;
height : @menuHeight;
color : black;
background-color : #DDDDDD;
.editors {
background-color : #ddd;
.editors{
position : absolute;
display : flex;
top : 0px;
right : 0px;
display : flex;
justify-content : space-between;
height : @menuHeight;
& > div {
width : @menuHeight;
width : 125px;
justify-content : space-between;
&>div{
height : @menuHeight;
width : @menuHeight;
cursor : pointer;
line-height : @menuHeight;
text-align : center;
cursor : pointer;
&:hover,&.selected { background-color : #999999; }
&.text {
&:hover,&.selected{
background-color : #999;
}
&.text{
.tooltipLeft('Brew Editor');
}
&.style {
&.style{
.tooltipLeft('Style Editor');
}
&.meta {
&.meta{
.tooltipLeft('Properties');
}
&.undo {
&.undo{
.tooltipLeft('Undo');
font-size : 0.75em;
color : grey;
&.active { color : inherit; }
&.active{
color : black;
}
}
&.redo {
&.redo{
.tooltipLeft('Redo');
font-size : 0.75em;
color : grey;
&.active { color : inherit; }
}
&.foldAll {
.tooltipLeft('Fold All');
font-size : 0.75em;
color : inherit;
}
&.unfoldAll {
.tooltipLeft('Unfold All');
font-size : 0.75em;
color : inherit;
}
&.editorTheme {
.tooltipLeft('Editor Themes');
font-size : 0.75em;
color : black;
&.active {
position : relative;
background-color : #999999;
&.active{
color : black;
}
}
&.divider {
width : 5px;
background : linear-gradient(currentColor, currentColor) no-repeat center/1px 100%;
&:hover { background-color : inherit; }
background: linear-gradient(#000, #000) no-repeat center/1px 100%;
width: 5px;
&:hover{
background-color: inherit;
}
}
}
.themeSelector {
position : absolute;
top : 25px;
right : 0;
z-index : 10;
display : flex;
align-items : center;
justify-content : center;
width : 170px;
height : inherit;
background-color : inherit;
}
}
.snippetBarButton {
display : inline-block;
.snippetBarButton{
height : @menuHeight;
padding : 0px 5px;
font-size : 0.625em;
font-weight : 800;
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 : #999999; }
i {
&:hover, &.selected{
background-color : #999;
}
i{
vertical-align : middle;
margin-right : 3px;
font-size : 1.4em;
vertical-align : middle;
}
}
.toggleMeta {
position : absolute;
top : 0px;
right : 0px;
border-left : 1px solid black;
.tooltipLeft('Edit Brew Properties');
.toggleMeta{
position : absolute;
top : 0px;
right : 0px;
border-left : 1px solid black;
.tooltipLeft("Edit Brew Properties");
}
.snippetGroup {
border-right : 1px solid currentColor;
&:hover {
& > .dropdown { visibility : visible; }
.snippetGroup{
border-right : 1px solid black;
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown {
.dropdown{
position : absolute;
top : 100%;
z-index : 1000;
padding : 0px;
margin-left : -5px;
visibility : hidden;
background-color : #DDDDDD;
.snippet {
position : relative;
display : flex;
align-items : center;
min-width : max-content;
padding : 5px;
font-size : 10px;
cursor : pointer;
z-index : 1000;
margin-left : -5px;
padding : 0px;
background-color : #ddd;
.snippet{
.animate(background-color);
i {
height : 1.2em;
padding : 5px;
cursor : pointer;
font-size : 10px;
i{
margin-right : 8px;
font-size : 1.2em;
min-width: 25px;
text-align: center;
& ~ i {
margin-right : 0;
margin-left : 5px;
}
/* Fonts */
&.font {
height : auto;
&::before {
font-size : 1em;
content : 'ABC';
}
&.OpenSans {font-family : 'OpenSans';}
&.CodeBold {font-family : 'CodeBold';}
&.CodeLight {font-family : 'CodeLight';}
&.ScalySansRemake {font-family : 'ScalySansRemake';}
&.BookInsanityRemake {font-family : 'BookInsanityRemake';}
&.MrEavesRemake {font-family : 'MrEavesRemake';}
&.SolberaImitationRemake {font-family : 'SolberaImitationRemake';}
&.ScalySansSmallCapsRemake {font-family : 'ScalySansSmallCapsRemake';}
&.WalterTurncoat {font-family : 'WalterTurncoat';}
&.Lato {font-family : 'Lato';}
&.Courier {font-family : 'Courier';}
&.NodestoCapsCondensed {font-family : 'NodestoCapsCondensed';}
&.Overpass {font-family : 'Overpass';}
&.Davek {font-family : 'Davek';}
&.Iokharic {font-family : 'Iokharic';}
&.Rellanic {font-family : 'Rellanic';}
&.TimesNewRoman {font-family : 'Times New Roman';}
}
}
.name { margin-right : auto; }
.beta {
align-self : center;
padding : 4px 6px;
margin-left : 5px;
font-family : monospace;
line-height : 1em;
color : white;
background : grey;
border-radius : 12px;
}
&:hover {
background-color : #999999;
& > .dropdown {
visibility : visible;
&.side {
top : 0%;
left : 100%;
margin-left : 0;
box-shadow : -1px 1px 2px 0px #999999;
}
}
&:hover{
background-color : #999;
}
}
}
}
}
}

View File

@@ -9,9 +9,7 @@ const StringArrayEditor = createClass({
label : '',
values : [],
valuePatterns : null,
validators : [],
placeholder : '',
notes : [],
unique : false,
cannotEdit : [],
onChange : ()=>{}
@@ -85,8 +83,7 @@ const StringArrayEditor = createClass({
}
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
const uniqueIfSet = !this.props.unique || !values.includes(value);
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
return matchesPatterns && uniqueIfSet && passesValidators;
return matchesPatterns && uniqueIfSet;
},
handleValueInputKeyDown : function(event, index) {
@@ -126,21 +123,17 @@ const StringArrayEditor = createClass({
</div>
);
return <div className='field'>
return <div className='field values'>
<label>{this.props.label}</label>
<div style={{ flex: '1 0' }}>
<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 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>
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
</div>
</div>;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

View File

@@ -1,8 +0,0 @@
<svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg">
<path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/>
<path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
<path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
<path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
<path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
<path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
</svg>

Before

Width:  |  Height:  |  Size: 755 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -9,7 +9,8 @@ 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 ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx');
const AccountPage = require('./pages/accountPage/accountPage.jsx');
const WithRoute = (props)=>{
@@ -46,7 +47,6 @@ const Homebrew = createClass({
editId : null,
createdAt : null,
updatedAt : null,
lang : ''
}
};
},
@@ -71,13 +71,14 @@ const Homebrew = createClass({
<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} accountDetails={this.props.brew.accountDetails} />} />
<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='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
<Route path='/' 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} />} />
<Route path='/*' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
</Routes>
</div>
</Router>
@@ -85,4 +86,15 @@ const Homebrew = createClass({
}
});
module.exports = Homebrew;
module.exports = Homebrew;
//TODO: Nicer Error page instead of just "cant get that"
// '/share/:id' : (args)=>{
// if(!this.props.brew.shareId){
// return <ErrorPage errorId={args.id}/>;
// }
//
// return <SharePage
// id={args.id}
// brew={this.props.brew} />;
// },

View File

@@ -15,23 +15,6 @@
}
&.listPage .content {
overflow-y : scroll;
&::-webkit-scrollbar {
width: 20px;
&:horizontal{
height: 20px;
width:auto;
}
&-thumb {
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
&:horizontal{
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
}
}
&-corner {
visibility: hidden;
}
}
}
}
}

View File

@@ -63,7 +63,7 @@ const Account = createClass({
if(global.account){
return <Nav.dropdown>
<Nav.item
className='account username'
className='account'
color='orange'
icon='fas fa-user'
>

View File

@@ -1,119 +0,0 @@
require('./error-navitem.less');
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const createClass = require('create-react-class');
const ErrorNavItem = createClass({
getDefaultProps : function() {
return {
error : '',
parent : null
};
},
render : function() {
const clearError = ()=>{
const state = {
error : null
};
if(this.props.parent.state.isSaving) {
state.isSaving = false;
}
this.props.parent.setState(state);
};
const error = this.props.error;
const response = error.response;
const status = response.status;
const HBErrorCode = response.body?.HBErrorCode;
const message = response.body?.message;
let errMsg = '';
try {
errMsg += `${error.toString()}\n\n`;
errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
{message ?? 'Conflict: please refresh to get latest changes'}
</div>
</Nav.item>;
}
if(status === 412) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
{message ?? 'Your client is out of date. Please save your changes elsewhere and refresh.'}
</div>
</Nav.item>;
}
if(HBErrorCode === '04') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
You are no longer signed in as an author of
this brew! Were you signed out from a different
window? Visit our log in page, then try again!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
</div>
</Nav.item>;
}
if(response.body?.errors?.[0].reason == 'storageQuotaExceeded') {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
Can't save because your Google Drive seems to be full!
</div>
</Nav.item>;
}
if(response.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer' onClick={clearError}>
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!
<br></br>
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<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>;
}
});
module.exports = ErrorNavItem;

View File

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

View File

@@ -1,4 +1,6 @@
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');

View File

@@ -1,89 +0,0 @@
const React = require('react');
const createClass = require('create-react-class');
const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx');
const MetadataNav = createClass({
displayName : 'MetadataNav',
getDefaultProps : function() {
return {
};
},
getInitialState : function() {
return {
showMetaWindow : false
};
},
componentDidMount : function() {
},
toggleMetaWindow : function(){
this.setState((prevProps)=>({
showMetaWindow : !prevProps.showMetaWindow
}));
},
getAuthors : function(){
if(!this.props.brew.authors || this.props.brew.authors.length == 0) return 'No authors';
return <>
{this.props.brew.authors.map((author, idx, arr)=>{
const spacer = arr.length - 1 == idx ? <></> : <span>, </span>;
return <span key={idx}><a className='userPageLink' href={`/user/${author}`}>{author}</a>{spacer}</span>;
})}
</>;
},
getTags : function(){
if(!this.props.brew.tags || this.props.brew.tags.length == 0) return 'No tags';
return <>
{this.props.brew.tags.map((tag, idx)=>{
return <span className='tag' key={idx}>{tag}</span>;
})}
</>;
},
getSystems : function(){
if(!this.props.brew.systems || this.props.brew.systems.length == 0) return 'No systems';
return this.props.brew.systems.join(', ');
},
renderMetaWindow : function(){
return <div className={`window ${this.state.showMetaWindow ? 'active' : 'inactive'}`}>
<div className='row'>
<h4>Description</h4>
<p>{this.props.brew.description || 'No description.'}</p>
</div>
<div className='row'>
<h4>Authors</h4>
<p>{this.getAuthors()}</p>
</div>
<div className='row'>
<h4>Tags</h4>
<p>{this.getTags()}</p>
</div>
<div className='row'>
<h4>Systems</h4>
<p>{this.getSystems()}</p>
</div>
<div className='row'>
<h4>Updated</h4>
<p>{Moment(this.props.brew.updatedAt).fromNow()}</p>
</div>
</div>;
},
render : function(){
return <Nav.item icon='fas fa-info-circle' color='grey' className='metadata'
onClick={()=>this.toggleMetaWindow()}>
{this.props.children}
{this.renderMetaWindow()}
</Nav.item>;
}
});
module.exports = MetadataNav;

View File

@@ -1,351 +1,190 @@
@import 'naturalcrit/styles/colors.less';
@navbarHeight : 28px;
@keyframes pinkColoring {
0% { color : pink; }
50% { color : pink; }
75% { color : red; }
100% { color : pink; }
}
@keyframes glideDropDown {
0% {
background-color : #333333;
opacity : 0;
transform : translate(0px, -100%);
}
100% {
background-color : #333333;
opacity : 1;
transform : translate(0px, 0px);
}
}
.homebrew nav {
background-color : #333333;
.navContent {
position : relative;
z-index : 2;
display : flex;
justify-content : space-between;
}
.navSection {
display : flex;
align-items : center;
&:last-child .navItem { border-left : 1px solid #666666; }
}
// "NaturalCrit" logo
.navLogo {
display : block;
margin-top : 0px;
margin-right : 8px;
margin-left : 8px;
color : white;
text-decoration : none;
&:hover {
.name { color : @orange; }
svg { fill : @orange; }
}
svg {
height : 13px;
margin-right : 0.2em;
cursor : pointer;
fill : white;
}
span.name {
font-family : 'CodeLight';
font-size : 15px;
span.crit { font-family : 'CodeBold'; }
small {
font-family : 'Open Sans';
font-size : 0.3em;
font-weight : 800;
text-transform : uppercase;
}
}
}
.navItem {
#backgroundColorsHover;
.animate(background-color);
padding : 8px 12px;
font-size : 10px;
font-weight : 800;
line-height : 13px;
color : white;
text-decoration : none;
text-transform : uppercase;
cursor : pointer;
background-color : #333333;
i {
float : right;
margin-left : 5px;
font-size : 13px;
}
&.patreon {
border-right : 1px solid #666666;
border-left : 1px solid #666666;
&:hover i { color : red; }
i {
color : pink;
.animate(color);
animation-name : pinkColoring;
animation-duration : 2s;
}
}
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
padding : 2px 12px;
input {
width : 250px;
padding : 2px;
margin : 0;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
background-color : transparent;
border : 1px solid @blue;
outline : none;
}
.charCount {
display : inline-block;
margin-left : 8px;
color : #666666;
text-align : right;
vertical-align : bottom;
&.max { color : @red; }
}
}
&.brewTitle {
flex-grow : 1;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
text-transform : initial;
background-color : transparent;
}
// "The Homebrewery" logo
&.homebrewLogo {
.animate(color);
font-family : 'CodeBold';
font-size : 12px;
color : white;
div {
margin-top : 2px;
margin-bottom : -2px;
}
&:hover { color : @blue; }
}
&.metadata {
position : relative;
display : flex;
flex-grow : 1;
align-items : center;
height : 100%;
padding : 0;
i { margin-right : 10px;}
.window {
position : absolute;
bottom : 0;
left : 50%;
z-index : -1;
display : flex;
flex-flow : row wrap;
align-content : baseline;
justify-content : flex-start;
width : 440px;
max-height : ~'calc(100vh - 28px)';
padding : 0 10px 5px;
margin : 0 auto;
background-color : #333333;
border : 3px solid #444444;
border-top : unset;
border-radius : 0 0 5px 5px;
box-shadow : inset 0 7px 9px -7px #111111;
transition : transform 0.4s, opacity 0.4s;
&.active {
opacity : 1;
transform : translateX(-50%) translateY(100%);
}
&.inactive {
opacity : 0;
transform : translateX(-50%) translateY(0%);
}
.row {
display : flex;
flex-flow : row wrap;
width : 100%;
h4 {
box-sizing : border-box;
display : block;
flex-basis : 20%;
flex-grow : 1;
min-width : 76px;
padding : 5px 0;
color : #BBBBBB;
text-align : center;
}
p {
flex-basis : 80%;
flex-grow : 1;
padding : 5px 0;
font-family : 'Open Sans', sans-serif;
font-size : 10px;
font-weight : normal;
text-transform : initial;
.tag {
display : inline-block;
padding : 2px;
margin : 2px 2px;
background-color : #444444;
border : 2px solid grey;
border-radius : 5px;
}
a.userPageLink {
color : white;
text-decoration : none;
&:hover { text-decoration : underline; }
}
}
&:nth-of-type(even) { background-color : #555555; }
}
}
}
&.warning {
position : relative;
color : white;
background-color : @orange;
&:hover > .dropdown { visibility : visible; }
.dropdown {
position : absolute;
top : 28px;
left : 0;
z-index : 10000;
box-sizing : border-box;
display : block;
width : 100%;
padding : 13px 5px;
text-align : center;
visibility : hidden;
background-color : #333333;
}
}
&.account {
min-width : 100px;
&.username { text-transform : none;}
}
}
.navDropdownContainer {
position : relative;
.navDropdown {
position: absolute;
top: 28px;
right: 0px;
z-index: 10000;
width: max-content;
min-width:100%;
max-height: calc(100vh - 28px);
overflow: hidden auto;
display: flex;
flex-direction: column;
align-items: flex-end;
.navItem {
position : relative;
display : flex;
justify-content : space-between;
align-items : center;
width : 100%;
border : 1px solid #888888;
border-bottom : 0;
animation-name : glideDropDown;
animation-duration : 0.4s;
}
}
&.recent {
position : relative;
.navDropdown .navItem {
#backgroundColorsHover;
.animate(background-color);
position : relative;
box-sizing : border-box;
display : block;
max-width : 15em;
max-height : ~'calc(100vh - 28px)';
padding : 8px 5px 13px;
overflow : hidden auto;
color : white;
text-decoration : none;
background-color : #333333;
border-top : 1px solid #888888;
scrollbar-color : #666666 #333333;
scrollbar-width : thin;
.clear {
position : absolute;
top : 50%;
right : 0;
display : none;
width : 20px;
height : 100%;
background-color : #333333;
border-radius : 3px;
opacity : 70%;
transform : translateY(-50%);
&:hover { opacity : 100%; }
i {
width : 100%;
height : 100%;
margin : 0;
font-size : 10px;
text-align : center;
}
}
&:hover {
background-color : @blue;
.clear {
display : grid;
place-content : center;
}
}
.title {
display : inline-block;
width : 100%;
overflow : hidden auto;
text-overflow : ellipsis;
white-space : nowrap;
}
.time {
position : absolute;
right : 2px;
bottom : 2px;
font-size : 0.7em;
color : #888888;
}
&.header {
box-sizing : border-box;
display : block;
padding : 5px 0;
color : #BBBBBB;
text-align : center;
background-color : #333333;
border-top : 1px solid #888888;
&:nth-of-type(1) { background-color : darken(@teal, 20%); }
&:nth-of-type(2) { background-color : darken(@purple, 30%); }
}
}
}
}
}
// this should likely be refactored into .navDropdownContainer
.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));
}
}
}
@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;
}
}

View File

@@ -1,64 +1,11 @@
const React = require('react');
const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx');
const { splitTextStyleAndMetadata } = require('../../../shared/helpers.js'); // Importing the function from helpers.js
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const NewBrew = ()=>{
const handleFileChange = (e)=>{
const file = e.target.files[0];
if(file) {
const reader = new FileReader();
reader.onload = (e)=>{
const fileContent = e.target.result;
const newBrew = {
text : fileContent,
style : ''
};
if(fileContent.startsWith('```metadata')) {
splitTextStyleAndMetadata(newBrew); // Modify newBrew directly
localStorage.setItem(BREWKEY, newBrew.text);
localStorage.setItem(STYLEKEY, newBrew.style);
localStorage.setItem(METAKEY, JSON.stringify(_.pick(newBrew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang'])));
window.location.href = '/new';
} else {
alert('This file is invalid, please, enter a valid file');
}
};
reader.readAsText(file);
}
};
return (
<Nav.dropdown>
<Nav.item
className='new'
color='purple'
icon='fa-solid fa-plus-square'>
new
</Nav.item>
<Nav.item
className='fromBlank'
href='/new'
newTab={true}
color='purple'
icon='fa-solid fa-file'>
from blank
</Nav.item>
<Nav.item
className='fromFile'
color='purple'
icon='fa-solid fa-upload'
onClick={()=>{ document.getElementById('uploadTxt').click(); }}>
<input id='uploadTxt' className='newFromLocal' type='file' onChange={handleFileChange} style={{ display: 'none' }} />
from file
</Nav.item>
</Nav.dropdown>
);
module.exports = function(props){
return <Nav.item
href='/new'
color='purple'
icon='fas fa-plus-square'>
new
</Nav.item>;
};
module.exports = NewBrew;

View File

@@ -1,9 +1,9 @@
const React = require('react');
const createClass = require('create-react-class');
const Nav = require('naturalcrit/nav/nav.jsx');
const { printCurrentBrew } = require('../../../shared/helpers.js');
module.exports = function(){
return <Nav.item onClick={printCurrentBrew} color='purple' icon='far fa-file-pdf'>
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>;
};

View File

@@ -121,7 +121,6 @@ const RecentItems = createClass({
removeItem : function(url, evt){
evt.preventDefault();
evt.stopPropagation();
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
let viewed = JSON.parse(localStorage.getItem(VIEW_KEY) || '[]');
@@ -140,11 +139,11 @@ const RecentItems = createClass({
},
renderDropdown : function(){
// if(!this.state.showDropdown) return null;
if(!this.state.showDropdown) return null;
const makeItems = (brews)=>{
return _.map(brews, (brew, i)=>{
return <a className='navItem' href={brew.url} key={`${brew.id}-${i}`} target='_blank' rel='noopener noreferrer' title={brew.title || '[ no title ]'}>
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>
@@ -152,25 +151,25 @@ const RecentItems = createClass({
});
};
return <>
return <div className='dropdown'>
{(this.props.showEdit && this.props.showView) ?
<Nav.item className='header'>edited</Nav.item> : null }
<h4>edited</h4> : null }
{this.props.showEdit ?
makeItems(this.state.edit) : null }
{(this.props.showEdit && this.props.showView) ?
<Nav.item className='header'>viewed</Nav.item> : null }
<h4>viewed</h4> : null }
{this.props.showView ?
makeItems(this.state.view) : null }
</>;
</div>;
},
render : function(){
return <Nav.dropdown className='recent'>
<Nav.item icon='fas fa-history' color='grey' >
{this.props.text}
</Nav.item>
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.dropdown>;
</Nav.item>;
}
});

View File

@@ -1,7 +1,9 @@
const React = require('react');
const createClass = require('create-react-class');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const MAX_URL_SIZE = 2083;
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';

View File

@@ -1,82 +1,71 @@
const React = require('react');
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');
let SAVEKEY = '';
const AccountPage = createClass({
displayName : 'AccountPage',
getDefaultProps : function() {
return {
brew : {},
uiItems : {}
};
},
getInitialState : function() {
return {
uiItems : this.props.uiItems
};
},
const AccountPage = (props)=>{
// destructure props and set state for save location
const { accountDetails, brew } = props;
const [saveLocation, setSaveLocation] = React.useState('');
renderNavItems : function() {
return <Navbar>
<Nav.section>
<NewBrew />
<HelpNavItem />
<RecentNavItem />
<Account />
</Nav.section>
</Navbar>;
},
// initialize save location from local storage based on user id
React.useEffect(()=>{
if(!saveLocation && accountDetails.username) {
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');
setActiveSaveLocation(saveLocation);
}
}, []);
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>
</>;
},
const setActiveSaveLocation = (newSelection)=>{
if(saveLocation === newSelection) return;
window.localStorage.setItem(SAVEKEY, newSelection);
setSaveLocation(newSelection);
};
// todo: should this be a set of radio buttons (well styled) since it's either/or choice?
const renderSaveLocationButton = (name, key, shouldRender = true)=>{
if(!shouldRender) return null;
return (
<button className={saveLocation === key ? 'active' : ''} onClick={()=>{setActiveSaveLocation(key);}}>
{name}
</button>
);
};
// render the entirety of the account page content
const renderAccountPage = ()=>{
return (
<>
<div className='dataGroup'>
<h1>Account Information <i className='fas fa-user'></i></h1>
<p><strong>Username: </strong>{accountDetails.username || 'No user currently logged in'}</p>
<p><strong>Last Login: </strong>{moment(accountDetails.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>{accountDetails.mongoCount}</p>
</div>
<div className='dataGroup'>
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
<p><strong>Linked to Google: </strong>{accountDetails.googleId ? 'YES' : 'NO'}</p>
{accountDetails.googleId && (
<p>
<strong>Brews on Google Drive: </strong>{accountDetails.googleCount ?? (
<>
Unable to retrieve files - <a href='https://github.com/naturalcrit/homebrewery/discussions/1580'>follow these steps to renew your Google credentials.</a>
</>
)}
</p>
)}
</div>
<div className='dataGroup'>
<h4>Default Save Location</h4>
{renderSaveLocationButton('Homebrewery', 'HOMEBREWERY')}
{renderSaveLocationButton('Google Drive', 'GOOGLE-DRIVE', accountDetails.googleId)}
</div>
</>
);
};
// return the account page inside the base layout wrapper (with navbar etc).
return (
<UIPage brew={brew}>
{renderAccountPage()}
</UIPage>);
};
render : function(){
return <UIPage brew={this.props.brew}>
{this.renderUiItems()}
</UIPage>;
}
});
module.exports = AccountPage;

View File

@@ -1,11 +1,12 @@
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('../../../../utils/request-middleware.js');
const request = require('superagent');
const googleDriveIcon = require('../../../../googleDrive.svg');
const homebreweryIcon = require('../../../../thumbnail.png');
const googleDriveIcon = require('../../../../googleDrive.png');
const dedent = require('dedent-tabs').default;
const BrewItem = createClass({
@@ -17,9 +18,7 @@ const BrewItem = createClass({
description : '',
authors : [],
stubbed : true
},
updateListFilter : ()=>{},
reportError : ()=>{}
}
};
},
@@ -34,19 +33,11 @@ const BrewItem = createClass({
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
.send()
.end((err, res)=>{
if(err) {
this.props.reportError(err);
} else {
location.reload();
}
.end(function(err, res){
location.reload();
});
},
updateFilter : function(type, term){
this.props.updateListFilter(type, term);
},
renderDeleteBrewLink : function(){
if(!this.props.brew.editId) return;
@@ -94,17 +85,11 @@ const BrewItem = createClass({
</a>;
},
renderStorageIcon : function(){
if(this.props.brew.googleId) {
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
<a href={this.props.brew.webViewLink} target='_blank'>
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
</a>
</span>;
}
renderGoogleDriveIcon : function(){
if(!this.props.brew.googleId) return;
return <span title='Homebrewery Storage'>
<img className='homebreweryIcon' src={homebreweryIcon} alt='homebreweryIcon' />
return <span>
<img className='googleDriveIcon' src={googleDriveIcon} alt='googleDriveIcon' />
</span>;
},
@@ -112,9 +97,6 @@ const BrewItem = createClass({
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
brew.tags.sort((a, b)=>{
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
});
}
const dateFormatString = 'YYYY-MM-DD HH:mm:ss';
@@ -131,21 +113,17 @@ const BrewItem = createClass({
<div className='info'>
{brew.tags?.length ? <>
<div className='brewTags' title={`${brew.tags.length} tags:\n${brew.tags.join('\n')}`}>
<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]} onClick={()=>{this.updateFilter(tag);}}>{matches[2]}</span>;
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?.map((author, index)=>(
<>
<a key={index} href={`/user/${author}`}>{author}</a>
{index < brew.authors.length - 1 && ', '}
</>))}
<i className='fas fa-user'/> {brew.authors?.join(', ')}
</span>
<br />
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
@@ -161,7 +139,7 @@ const BrewItem = createClass({
Last updated: ${moment(brew.updatedAt).local().format(dateFormatString)}`}>
<i className='fas fa-sync-alt' /> {moment(brew.updatedAt).fromNow()}
</span>
{this.renderStorageIcon()}
{this.renderGoogleDriveIcon()}
</div>
<div className='links'>

View File

@@ -48,10 +48,6 @@
&>span{
margin-right : 12px;
line-height : 1.5em;
a {
color:inherit;
}
}
}
.brewTags span {
@@ -63,41 +59,6 @@
white-space: nowrap;
display: inline-block;
font-weight: bold;
border-color: currentColor;
cursor : pointer;
&:before {
font-family: 'Font Awesome 5 Free';
font-size: 12px;
margin-right: 3px;
}
&.type {
background-color: #0080003b;
color: #008000;
&:before{
content: '\f0ad';
}
}
&.group {
background-color: #5050503b;
color: #000000;
&:before{
content: '\f500';
}
}
&.meta {
background-color: #0000803b;
color: #000080;
&:before{
content: '\f05a';
}
}
&.system {
background-color: #8000003b;
color: #800000;
&:before{
content: '\f518';
}
}
}
&:hover{
.links{
@@ -119,12 +80,11 @@
text-align : center;
a{
.animate(opacity);
display : block;
margin : 8px 0px;
opacity : 0.6;
font-size : 1.3em;
color : white;
text-decoration : unset;
display : block;
margin : 8px 0px;
opacity : 0.6;
font-size : 1.3em;
color : white;
&:hover{
opacity : 1;
}
@@ -134,15 +94,8 @@
}
}
.googleDriveIcon {
height : 18px;
height : 20px;
padding : 0px;
margin : -5px;
}
.homebreweryIcon {
mix-blend-mode : darken;
height : 24px;
position : relative;
top : 5px;
left : -5px;
}
}

View File

@@ -23,8 +23,7 @@ const ListPage = createClass({
brews : []
}
],
navItems : <></>,
reportError : null
navItems : <></>
};
},
getInitialState : function() {
@@ -36,7 +35,6 @@ const ListPage = createClass({
return {
filterString : this.props.query?.filter || '',
filterTags : [],
sortType : this.props.query?.sort || null,
sortDir : this.props.query?.dir || null,
query : this.props.query,
@@ -83,14 +81,14 @@ const ListPage = createClass({
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
return _.map(brews, (brew, idx)=>{
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError} updateListFilter={ (tag)=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}/>;
return <BrewItem brew={brew} key={idx}/>;
});
},
sortBrewOrder : function(brew){
if(!brew.title){brew.title = 'No Title';}
const mapping = {
'alpha' : _.deburr(brew.title.trim().toLowerCase()),
'alpha' : _.deburr(brew.title.toLowerCase()),
'created' : moment(brew.createdAt).format(),
'updated' : moment(brew.updatedAt).format(),
'views' : brew.views,
@@ -137,33 +135,13 @@ const ListPage = createClass({
return;
},
updateUrl : function(filterTerm, sortType, sortDir, filterTag=''){
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);
let filterTags = urlParams.getAll('tag');
if(filterTag != '') {
if(filterTags.findIndex((tag)=>{return tag.toLowerCase()==filterTag.toLowerCase();}) == -1){
filterTags.push(filterTag);
} else {
filterTags = filterTags.filter((tag)=>{ return tag.toLowerCase() != filterTag.toLowerCase(); });
}
}
urlParams.delete('tag');
// Add tags to URL in the order they were clicked
filterTags.forEach((tag)=>{ urlParams.append('tag', tag); });
// Sort tags before updating state
filterTags.sort((a, b)=>{
return a.indexOf(':') - b.indexOf(':') != 0 ? a.indexOf(':') - b.indexOf(':') : a.toLowerCase().localeCompare(b.toLowerCase());
});
this.setState({
filterTags
});
if(!filterTerm)
urlParams.delete('filter');
else
@@ -187,16 +165,6 @@ const ListPage = createClass({
</div>;
},
renderTagsOptions : function(){
if(this.state.filterTags?.length == 0) return;
return <div className='tags-container'>
{_.map(this.state.filterTags, (tag, idx)=>{
const matches = tag.match(/^(?:([^:]+):)?([^:]+)$/);
return <span key={idx} className={matches[1]} onClick={()=>{ this.updateUrl(this.state.filterString, this.state.sortType, this.state.sortDir, tag); }}>{matches[2]}</span>;
})}
</div>;
},
renderSortOptions : function(){
return <div className='sort-container'>
<h6>Sort by :</h6>
@@ -207,6 +175,9 @@ const ListPage = createClass({
{/* {this.renderSortOption('Latest', 'latest')} */}
{this.renderFilterOption()}
</div>;
},
@@ -214,28 +185,14 @@ const ListPage = createClass({
const testString = _.deburr(this.state.filterString).toLowerCase();
brews = _.filter(brews, (brew)=>{
// Filter by user entered text
const brewStrings = _.deburr([
brew.title,
brew.description,
brew.tags].join('\n')
.toLowerCase());
const filterTextTest = brewStrings.includes(testString);
// Filter by user selected tags
let filterTagTest = true;
if(this.state.filterTags.length > 0){
filterTagTest = Array.isArray(brew.tags) && this.state.filterTags?.every((tag)=>{
return brew.tags.findIndex((brewTag)=>{
return brewTag.toLowerCase() == tag.toLowerCase();
}) >= 0;
});
}
return filterTextTest && filterTagTest;
return brewStrings.includes(testString);
});
return _.orderBy(brews, (brew)=>{ return this.sortBrewOrder(brew); }, this.state.sortDir);
},
@@ -261,15 +218,12 @@ const ListPage = createClass({
render : function(){
return <div className='listPage sitePage'>
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
<link href='/themes/V3/Blank/style.css' type='text/css' rel='stylesheet'/>
<link href='/themes/V3/5ePHB/style.css' type='text/css' rel='stylesheet'/>
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
{this.props.navItems}
{this.renderSortOptions()}
{this.renderTagsOptions()}
<div className='content V3'>
<div className='page'>
<div className='phb page'>
{this.renderBrewCollection(this.state.brewCollection)}
</div>
</div>

View File

@@ -2,24 +2,22 @@
.noColumns(){
column-count : auto;
column-fill : auto;
column-gap : normal;
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 : normal;
-moz-column-gap : normal;
height : auto;
min-height : 279.4mm;
margin : 20px auto;
contain : unset;
-webkit-column-gap : auto;
-moz-column-gap : auto;
}
.listPage{
.content{
z-index : 1;
.page{
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
.phb{
.noColumns();
height : auto;
min-height : 279.4mm;
margin : 20px auto;
&::after{
display : none;
}
@@ -53,7 +51,7 @@
}
}
}
.sort-container {
.sort-container{
font-family : 'Open Sans', sans-serif;
position : sticky;
top : 0;
@@ -65,7 +63,7 @@
border-bottom : 1px solid #666;
color : white;
text-align : center;
z-index : 1;
z-index : 500;
display : flex;
justify-content : center;
align-items : baseline;
@@ -125,66 +123,4 @@
}
.tags-container {
height : 30px;
background-color : #555;
border-top : 1px solid #666;
border-bottom : 1px solid #666;
color : white;
display : flex;
justify-content : center;
align-items : center;
column-gap : 15px;
row-gap : 5px;
flex-wrap : wrap;
span {
font-family : 'Open Sans', sans-serif;
font-size : 11px;
font-weight : bold;
border : 1px solid;
border-radius : 3px;
padding : 3px;
cursor : pointer;
color: #dfdfdf;
&:before {
font-family: 'Font Awesome 5 Free';
font-size: 12px;
margin-right: 3px;
}
&:after {
content: '\f00d';
font-family: 'Font Awesome 5 Free';
font-size: 12px;
margin-left: 3px;
}
&.type {
background-color: #008000;
border-color: #00a000;
&:before{
content: '\f0ad';
}
}
&.group {
background-color: #505050;
border-color: #000000;
&:before{
content: '\f500';
}
}
&.meta {
background-color: #000080;
border-color: #0000a0;
&:before{
content: '\f05a';
}
}
&.system {
background-color: #800000;
border-color: #a00000;
&:before{
content: '\f518';
}
}
}
}
}

View File

@@ -1,69 +1,47 @@
.homebrew {
.uiPage.sitePage {
.content {
width : ~"min(90vw, 1000px)";
padding : 2% 4%;
margin-top : 25px;
margin-right : auto;
margin-left : auto;
overflow-y : scroll;
font-family : 'Open Sans';
font-size : 0.8em;
line-height : 1.8em;
background-color : #F0F0F0;
.dataGroup {
padding : 6px 20px 15px;
margin : 5px 0px;
border : 2px solid black;
border-radius : 5px;
button {
background-color : transparent;
border : 1px solid black;
border-radius : 5px;
width : 125px;
color : black;
margin-right : 5px;
&.active {
background-color: #0007;
color: white;
&:before {
content: '\f00c';
font-family: 'FONT AWESOME 5 FREE';
margin-right: 5px;
}
}
}
}
h1, h2, h3, h4 {
width : 100%;
margin : 0.5em 30% 0.25em 0;
font-weight : 900;
text-transform : uppercase;
border-bottom : 2px solid slategrey;
}
h1 {
margin-right : 0;
margin-bottom : 0.5em;
font-size : 2em;
border-bottom : 2px solid darkslategrey;
}
h2 { font-size : 1.75em; }
h3 {
font-size : 1.5em;
svg { width : 19px; }
}
h4 { font-size : 1.25em; }
strong { font-weight : bold; }
em { font-style : italic; }
ul {
padding-left : 1.25em;
list-style : square;
}
.blank {
height : 1em;
margin-top : 0;
& + * { margin-top : 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;
}
}
}
}

View File

@@ -3,7 +3,7 @@ require('./editPage.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const request = require('../../utils/request-middleware.js');
const request = require('superagent');
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
@@ -11,8 +11,7 @@ const Navbar = require('../../navbar/navbar.jsx');
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const PrintNavItem = require('../../navbar/print.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
@@ -20,14 +19,10 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const LockNotification = require('./lockNotification/lockNotification.jsx');
const Markdown = require('naturalcrit/markdown.js');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew } = require('../../../../shared/helpers.js');
const googleDriveIcon = require('../../googleDrive.svg');
const googleDriveActive = require('../../googleDrive.png');
const googleDriveInactive = require('../../googleDriveMono.png');
const SAVE_TIMEOUT = 3000;
@@ -35,7 +30,24 @@ const EditPage = createClass({
displayName : 'EditPage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW_LOAD
brew : {
text : '',
style : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
gDrive : false,
trashed : false,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : [],
renderer : 'legacy'
}
};
},
@@ -48,18 +60,14 @@ const EditPage = createClass({
alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
error : null,
errors : null,
htmlErrors : Markdown.validate(this.props.brew.text),
url : '',
autoSave : true,
autoSaveWarning : false,
unsavedTime : new Date(),
currentEditorPage : 0,
displayLockMessage : this.props.brew.lock || false
unsavedTime : new Date()
};
},
editor : React.createRef(null),
savedBrew : null,
componentDidMount : function(){
@@ -67,6 +75,7 @@ const EditPage = createClass({
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 }, ()=>{
@@ -98,8 +107,8 @@ const EditPage = createClass({
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.trySave(true);
if(e.keyCode == P_KEY) printCurrentBrew();
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();
@@ -107,7 +116,7 @@ const EditPage = createClass({
},
handleSplitMove : function(){
this.editor.current.update();
this.refs.editor.update();
},
handleTextChange : function(text){
@@ -116,10 +125,9 @@ const EditPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
isPending : true,
htmlErrors : htmlErrors,
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
brew : { ...prevState.brew, text: text },
isPending : true,
htmlErrors : htmlErrors
}), ()=>{if(this.state.autoSave) this.trySave();});
},
@@ -145,14 +153,13 @@ const EditPage = createClass({
return !_.isEqual(this.state.brew, this.savedBrew);
},
trySave : function(immediate=false){
trySave : function(){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){
this.debounceSave();
} else {
this.debounceSave.cancel();
}
if(immediate) this.debounceSave.flush();
},
handleGoogleClick : function(){
@@ -165,10 +172,7 @@ const EditPage = createClass({
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}));
this.setState({
error : null,
isSaving : false
});
this.clearErrors();
},
closeAlerts : function(event){
@@ -184,16 +188,24 @@ const EditPage = createClass({
this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
isSaving : false,
error : null
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,
error : null,
errors : null,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
@@ -208,9 +220,8 @@ const EditPage = createClass({
.send(brew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ error: err });
this.setState({ errors: err });
});
if(!res) return;
this.savedBrew = res.body;
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
@@ -219,8 +230,7 @@ const EditPage = createClass({
brew : { ...prevState.brew,
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
editId : this.savedBrew.editId,
shareId : this.savedBrew.shareId,
version : this.savedBrew.version
shareId : this.savedBrew.shareId
},
isPending : false,
isSaving : false,
@@ -230,7 +240,10 @@ const EditPage = createClass({
renderGoogleDriveIcon : function(){
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
{this.state.saveGoogle
? <img src={googleDriveActive} alt='googleDriveActive'/>
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
}
{this.state.confirmGoogleTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
@@ -263,19 +276,71 @@ const EditPage = createClass({
</div>
</div>
}
{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.item>;
},
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);
@@ -319,12 +384,6 @@ const EditPage = createClass({
this.warningTimer;
},
errorReported : function(error) {
this.setState({
error
});
},
renderAutoSaveButton : function(){
return <Nav.item onClick={this.handleAutoSave}>
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
@@ -353,19 +412,26 @@ const EditPage = createClass({
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>
</Nav.section>
<Nav.section>
{this.renderGoogleDriveIcon()}
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
<Nav.dropdown className='save-menu'>
{this.renderSaveButton()}
{this.renderAutoSaveButton()}
</Nav.dropdown>
}
<Nav.dropdown className='save-menu'>
{this.renderSaveButton()}
{this.renderAutoSaveButton()}
</Nav.dropdown>
<NewBrew />
<HelpNavItem/>
<Nav.dropdown>
@@ -382,7 +448,7 @@ const EditPage = createClass({
post to reddit
</Nav.item>
</Nav.dropdown>
<PrintNavItem />
<PrintLink shareId={this.processShareId()} />
<RecentNavItem brew={this.state.brew} storageKey='edit' />
<Account />
</Nav.section>
@@ -396,27 +462,16 @@ const EditPage = createClass({
{this.renderNavbar()}
<div className='content'>
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
<SplitPane onDragFinish={this.handleSplitMove}>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref={this.editor}
ref='editor'
brew={this.state.brew}
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onMetaChange={this.handleMetaChange}
reportError={this.errorReported}
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}
lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage}
allowPrint={true}
/>
<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} />
</SplitPane>
</div>
</div>;

View File

@@ -13,17 +13,87 @@
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
}
}
.googleDriveStorage {
position : relative;
}
.googleDriveStorage img{
height : 18px;
height : 20px;
padding : 0px;
margin : -5px;
&.inactive {
filter: grayscale(1);
}
.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;
}
}
}
}

View File

@@ -1,30 +0,0 @@
require('./lockNotification.less');
const React = require('react');
import Dialog from '../../../../components/dialog.jsx';
function LockNotification(props) {
props = {
shareId : 0,
disableLock : ()=>{},
message : '',
...props
};
const removeLock = ()=>{
alert(`Not yet implemented - ID ${props.shareId}`);
};
return <Dialog className='lockNotification' blocking closeText='CONTINUE TO EDITOR' >
<h1>BREW LOCKED</h1>
<p>This brew been locked by the Administrators. It will not be accessible by any method other than the Editor until the lock is removed.</p>
<hr />
<h3>LOCK REASON</h3>
<p>{props.message || 'Unable to retrieve Lock Message'}</p>
<hr />
<p>Once you have resolved this issue, click REQUEST LOCK REMOVAL to notify the Administrators for review.</p>
<p>Click CONTINUE TO EDITOR to temporarily hide this notification; it will reappear the next time the page is reloaded.</p>
<button onClick={removeLock}>REQUEST LOCK REMOVAL</button>
</Dialog>;
};
module.exports = LockNotification;

View File

@@ -1,27 +0,0 @@
.lockNotification {
z-index : 1;
width : 80%;
padding : 10px;
margin : 5% 10%;
line-height : 1.5em;
color : black;
text-align : center;
background-color : #CCCCCC;
&::backdrop { background-color : #000000AA; }
button {
margin : 10px;
color : white;
background-color : #333333;
&:hover { background-color : #777777; }
}
h1, h3 {
font-family : 'Open Sans', sans-serif;
font-weight : 800;
}
h1 { font-size : 24px; }
h3 { font-size : 18px; }
}

View File

@@ -1,25 +1,48 @@
require('./errorPage.less');
const React = require('react');
const UIPage = require('../basePages/uiPage/uiPage.jsx');
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
const ErrorIndex = require('./errors/errorIndex.js');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const ErrorPage = ({ brew })=>{
// Retrieving the error text based on the brew's error code from ErrorIndex
const errorText = ErrorIndex({ brew })[brew.HBErrorCode.toString()] || '';
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');
return (
<UIPage brew={{ title: 'Crit Fail!' }}>
<div className='dataGroup'>
<div className='errorTitle'>
<h1>{`Error ${brew?.status || '000'}`}</h1>
<h4>{brew?.text || 'No error text'}</h4>
</div>
<hr />
<div dangerouslySetInnerHTML={{ __html: Markdown.render(errorText) }} />
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>
</UIPage>
);
};
</div>;
}
});
module.exports = ErrorPage;

View File

@@ -1,13 +1,5 @@
.homebrew {
.uiPage.sitePage {
.errorTitle {
//background-color: @orange;
color : #D02727;
text-align : center;
}
.content {
h1, h2, h3, h4 { border-bottom : none; }
hr { border-bottom : 2px solid slategrey; }
}
.errorPage{
.errorTitle{
background-color: @orange;
}
}

View File

@@ -1,153 +0,0 @@
const dedent = require('dedent-tabs').default;
const loginUrl = 'https://www.naturalcrit.com/login';
const errorIndex = (props)=>{
return {
// Default catch all
'00' : dedent`
## An unknown error occurred!
We aren't sure what happened, but our server wasn't able to find what you
were looking for.`,
// General Google load error
'01' : dedent`
## An error occurred while retrieving this brew from Google Drive!
Google reported an error while attempting to retrieve a brew from this link.`,
// Google Drive - 404 : brew deleted or access denied
'02' : dedent`
## We can't find this brew in Google Drive!
This file was saved on Google Drive, but this link doesn't work anymore.
${props.brew.authors?.length > 0
? `Note that this brew belongs to the Homebrewery account **${props.brew.authors[0]}**,
${props.brew.account
? `which is
${props.brew.authors[0] == props.brew.account
? `your account.`
: `not your account (you are currently signed in as **${props.brew.account}**).`
}`
: 'and you are not currently signed in to any account.'
}`
: ''
}
The Homebrewery cannot delete files from Google Drive on its own, so there
are three most likely possibilities:
:
- **The Google Drive files may have been accidentally deleted.** Look in
the Google Drive account that owns this brew (or ask the owner to do so),
and make sure the Homebrewery folder is still there, and that it holds your brews
as text files.
- **You may have changed the sharing settings for your files.** If the files
are still on Google Drive, change all of them to be shared *with everyone who has
the link* so the Homebrewery can access them.
- **The Google Account may be closed.** Google may have removed the account
due to inactivity or violating a Google policy. Make sure the owner can
still access Google Drive normally and upload/download files to it.
:
If the file isn't found, Google Drive usually puts your file in your Trash folder for
30 days. Assuming the trash hasn't been emptied yet, it might be worth checking.
You can also find the Activity tab on the right side of the Google Drive page, which
shows the recent activity on Google Drive. This can help you pin down the exact date
the brew was deleted or moved, and by whom.
:
If the brew still isn't found, some people have had success asking Google to recover
accidentally deleted files at this link:
https://support.google.com/drive/answer/1716222?hl=en&ref_topic=7000946.
At the bottom of the page there is a button that says *Send yourself an Email*
and you will receive instructions on how to request the files be restored.
:
Also note, if you prefer not to use your Google Drive for storage, you can always
change the storage location of a brew by clicking the Google drive icon by the
brew title and choosing *transfer my brew to/from Google Drive*.`,
// User is not Authors list
'03' : dedent`
## Current signed-in user does not have editor access to this brew.
If you believe you should have access to this brew, ask one of its authors to invite you
as an author by opening the Edit page for the brew, viewing the {{fa,fa-info-circle}}
**Properties** tab, and adding your username to the "invited authors" list. You can
then try to access this document again.
:
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
// User is not signed in; must be a user on the Authors List
'04' : dedent`
## Sign-in required to edit this brew.
You must be logged in to one of the accounts listed as an author of this brew.
User is not logged in. Please log in [here](${loginUrl}).
:
**Brew Title:** ${props.brew.brewTitle || 'Unable to show title'}
**Current Authors:** ${props.brew.authors?.map((author)=>{return `[${author}](/user/${author})`;}).join(', ') || 'Unable to list authors'}
[Click here to be redirected to the brew's share page.](/share/${props.brew.shareId})`,
// Brew load error
'05' : dedent`
## No Homebrewery document could be found.
The server could not locate the Homebrewery document. It was likely deleted by
its owner.
:
**Requested access:** ${props.brew.accessType}
**Brew ID:** ${props.brew.brewId}`,
// Brew save error
'06' : dedent`
## Unable to save Homebrewery document.
An error occurred wil attempting to save the Homebrewery document.`,
// Brew delete error
'07' : dedent`
## Unable to delete Homebrewery document.
An error occurred while attempting to remove the Homebrewery document.
:
**Brew ID:** ${props.brew.brewId}`,
// Author delete error
'08' : dedent`
## Unable to remove user from Homebrewery document.
An error occurred while attempting to remove the user from the Homebrewery document author list!
:
**Brew ID:** ${props.brew.brewId}`,
// Brew locked by Administrators error
'100' : dedent`
## This brew has been locked.
Only an author may request that this lock is removed.
:
**Brew ID:** ${props.brew.brewId}
**Brew Title:** ${props.brew.brewTitle}`,
};
};
module.exports = errorIndex;

View File

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

View File

@@ -3,7 +3,7 @@ const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('../../utils/request-middleware.js');
const request = require('superagent');
const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
@@ -12,62 +12,50 @@ 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');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const HomePage = createClass({
displayName : 'HomePage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW,
ver : '0.0.0'
brew : {
text : '',
},
ver : '0.0.0'
};
},
getInitialState : function() {
return {
brew : this.props.brew,
welcomeText : this.props.brew.text,
error : undefined,
currentEditorPage : 0
brew : this.props.brew,
welcomeText : this.props.brew.text
};
},
editor : React.createRef(null),
handleSave : function(){
request.post('/api')
.send(this.state.brew)
.end((err, res)=>{
if(err) {
this.setState({ error: err });
return;
}
if(err) return;
const brew = res.body;
window.location = `/edit/${brew.editId}`;
});
},
handleSplitMove : function(){
this.editor.current.update();
this.refs.editor.update();
},
handleTextChange : function(text){
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
brew : { ...prevState.brew, text: text }
}));
},
renderNavbar : function(){
return <Navbar ver={this.props.ver}>
<Nav.section>
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
null
}
<NewBrewItem />
<HelpNavItem />
<RecentNavItem />
@@ -82,20 +70,15 @@ const HomePage = createClass({
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove}>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref={this.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}
currentEditorPage={this.state.currentEditorPage}
/>
<BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer}/>
</SplitPane>
</div>

View File

@@ -40,11 +40,4 @@
right : 350px;
}
}
.navItem.save{
background-color: @orange;
&:hover{
background-color: @green;
}
}
}

View File

@@ -16,9 +16,9 @@ The Homebrewery makes the creation and sharing of authentic looking Fifth-Editio
**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 a new homebrew document ("brew"), your document will be given a *edit link* and a *share link*.
When you create your own homebrew, you will be given a *edit url* and a *share url*.
The *edit link* is where you write your brew. If you edit a brew while logged in, you are added as one of the brew's authors, and no one else can edit that brew until you add them as a new author via the {{fa,fa-info-circle}} **Properties** tab. Brews without any author can still be edited by anyone with the *edit link*, so be careful about who you share it with if you prefer to work without an account.
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.
@@ -36,7 +36,7 @@ After clicking the "Print" item in the navbar a new page will open and a print d
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
}}
![homebrew mug](https://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px}
![homebrew mug](http://i.imgur.com/hMna6G0.png) {position:absolute,bottom:20px,left:130px,width:220px}
{{artist,bottom:160px,left:100px
##### Homebrew Mug
@@ -48,63 +48,57 @@ If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,
\column
## V3 vs Legacy
The Homebrewery has two renderers: Legacy and V3. The V3 renderer is recommended for all users because it is more powerful, more customizable, and continues to receive new feature updates while Legacy does not. However Legacy mode will remain available for older brews and veteran users.
At any time, any individual brew can be changed to your renderer of choice via the {{fa,fa-info-circle}} **Properties** tab on your brew. However, converting between Legacy and V3 may require heavily tweaking the document; while both renderers can use raw HTML, V3 prefers a streamlined curly bracket syntax that avoids the complex HTML structures required by Legacy.
## 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!
Scroll down to the next page for a brief summary of the changes and 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? Head over to our [Patreon](https://www.patreon.com/Naturalcrit) to help us keep the servers running.
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 we will never offer any "premium" features or whatever.
This tool will **always** be free, never have ads, and I will never offer any "premium" features or whatever.
### Bugs, Issues, Suggestions?
- Check the [Frequently Asked Questions](/faq) page first for quick answers.
- Get help or the right look for your brew by posting on [r/Homebrewery](https://www.reddit.com/r/homebrewery/submit?selftext=true&title=%5BIssue%5D%20Describe%20Your%20Issue%20Here) or joining the [Discord Of Many Things](https://discord.gg/by3deKx).
- Report technical issues or provide feedback on the [GitHub Repo](https://github.com/naturalcrit/homebrewery/).
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 Us
If you'd like to credit us in your brew, we'd be flattered! Just reference that you made it with The Homebrewery.
#### 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
[![Discord](/assets/discordOfManyThings.svg){width:50px,float:right,padding-left:10px}](https://discord.gg/by3deKx)
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 [Discord Of Many Things](https://discord.gg/by3deKx) is another great resource to connect with fellow homebrewers for help and feedback.
<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
[![Discord](/assets/discord.png){height:30px}](https://discord.gg/by3deKx)
[![Github](/assets/github.png){height:30px}](https://github.com/naturalcrit/homebrewery)
[![Patreon](/assets/patreon.png){height:30px}](https://patreon.com/NaturalCrit)
[![Reddit](/assets/reddit.png){height:30px}](https://www.reddit.com/r/homebrewery/)
<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.
From 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.
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
Standard Markdown lacks several equivalences to HTML. Hence, we have introduced `{{ }}` as a replacement for `<span></span>` and `<div></div>` 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:
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.
@@ -132,18 +126,16 @@ A blank line can be achieved with a run of one or more `:` alone on a line. More
::
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.
@@ -171,13 +163,13 @@ Using *Curly Injection* you can assign an id, classes, or inline CSS properties
![alt-text](https://s-media-cache-ak0.pinimg.com/736x/4a/81/79/4a8179462cfdf39054a418efd4cb743e.jpg) {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 interface.*
\* *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}} Usually overlooked or unused by some users, the **Style Editor** tab is 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.
{{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}}

View File

@@ -2,15 +2,14 @@
require('./newPage.less');
const React = require('react');
const createClass = require('create-react-class');
const request = require('../../utils/request-middleware.js');
const _ = require('lodash');
const request = require('superagent');
const Markdown = require('naturalcrit/markdown.js');
const Nav = require('naturalcrit/nav/nav.jsx');
const PrintNavItem = require('../../navbar/print.navitem.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const HelpNavItem = require('../../navbar/help.navitem.jsx');
@@ -18,38 +17,49 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew } = require('../../../../shared/helpers.js');
const BREWKEY = 'homebrewery-new';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
let SAVEKEY;
const METAKEY = 'homebrewery-new-meta';
const NewPage = createClass({
displayName : 'NewPage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW
brew : {
text : '',
style : undefined,
title : '',
description : '',
renderer : 'V3',
theme : '5ePHB'
}
};
},
getInitialState : function() {
const brew = this.props.brew;
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),
error : null,
htmlErrors : Markdown.validate(brew.text),
currentEditorPage : 0
brew : brew,
isSaving : false,
saveGoogle : (global.account && global.account.googleId ? true : false),
errors : null,
htmlErrors : Markdown.validate(brew.text)
};
},
editor : React.createRef(null),
componentDidMount : function() {
document.addEventListener('keydown', this.handleControlKeys);
@@ -66,21 +76,15 @@ const NewPage = createClass({
// brew.description = metaStorage?.description || this.state.brew.description;
brew.renderer = metaStorage?.renderer ?? brew.renderer;
brew.theme = metaStorage?.theme ?? brew.theme;
brew.lang = metaStorage?.lang ?? brew.lang;
this.setState({
brew : brew
});
}
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
this.setState({
brew : brew,
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
});
localStorage.setItem(BREWKEY, brew.text);
if(brew.style)
localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
localStorage.setItem(STYLEKEY, brew.style);
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme }));
},
componentWillUnmount : function() {
document.removeEventListener('keydown', this.handleControlKeys);
@@ -91,7 +95,7 @@ const NewPage = createClass({
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save();
if(e.keyCode == P_KEY) printCurrentBrew();
if(e.keyCode == P_KEY) this.print();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
@@ -99,7 +103,7 @@ const NewPage = createClass({
},
handleSplitMove : function(){
this.editor.current.update();
this.refs.editor.update();
},
handleTextChange : function(text){
@@ -108,9 +112,8 @@ const NewPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors,
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors
}));
localStorage.setItem(BREWKEY, text);
},
@@ -125,16 +128,21 @@ const NewPage = createClass({
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,
'lang' : this.state.brew.lang
}));
}));
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(){
this.setState({
errors : null,
isSaving : false
});
;
},
save : async function(){
@@ -159,7 +167,7 @@ const NewPage = createClass({
.send(brew)
.catch((err)=>{
console.log(err);
this.setState({ isSaving: false, error: err });
this.setState({ isSaving: false, errors: err });
});
if(!res) return;
@@ -171,6 +179,67 @@ const NewPage = createClass({
},
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'>
save...
@@ -182,6 +251,16 @@ const NewPage = createClass({
}
},
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>
@@ -190,11 +269,8 @@ const NewPage = createClass({
</Nav.section>
<Nav.section>
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
this.renderSaveButton()
}
<PrintNavItem />
{this.renderSaveButton()}
{this.renderLocalPrintButton()}
<HelpNavItem />
<RecentNavItem />
<AccountNavItem />
@@ -206,25 +282,16 @@ const NewPage = createClass({
return <div className='newPage sitePage'>
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove}>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor
ref={this.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}
lang={this.state.brew.lang}
currentEditorPage={this.state.currentEditorPage}
allowPrint={true}
/>
<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}/>
</SplitPane>
</div>
</div>;

View File

@@ -4,5 +4,79 @@
&: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;
}
}
}
}

View File

@@ -0,0 +1,108 @@
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&nbsp;\n\\column\n&nbsp;`; //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;

View File

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

View File

@@ -5,20 +5,28 @@ const { Meta } = require('vitreum/headtags');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
const MetadataNav = require('../../navbar/metadata.navitem.jsx');
const PrintNavItem = require('../../navbar/print.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew } = require('../../../../shared/helpers.js');
const SharePage = createClass({
displayName : 'SharePage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW_LOAD
brew : {
title : '',
text : '',
style : '',
shareId : null,
createdAt : null,
updatedAt : null,
views : 0,
renderer : ''
}
};
},
@@ -34,7 +42,7 @@ const SharePage = createClass({
if(!(e.ctrlKey || e.metaKey)) return;
const P_KEY = 80;
if(e.keyCode == P_KEY){
if(e.keyCode == P_KEY) printCurrentBrew();
window.open(`/print/${this.processShareId()}?dialog=true`, '_blank').focus();
e.stopPropagation();
e.preventDefault();
}
@@ -46,44 +54,28 @@ const SharePage = createClass({
this.props.brew.shareId;
},
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 <Nav.item color='orange' icon='fas fa-pencil-alt' href={`/edit/${editLink}`}>
edit
</Nav.item>;
},
render : function(){
return <div className='sharePage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
<Navbar>
<Nav.section className='titleSection'>
<MetadataNav brew={this.props.brew}>
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
</MetadataNav>
<Nav.section>
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.props.brew.shareId && <>
<PrintNavItem/>
<PrintLink shareId={this.processShareId()} />
<Nav.dropdown>
<Nav.item color='red' icon='fas fa-code'>
source
</Nav.item>
<Nav.item color='blue' icon='fas fa-eye' href={`/source/${this.processShareId()}`}>
<Nav.item color='blue' href={`/source/${this.processShareId()}`}>
view
</Nav.item>
{this.renderEditLink()}
<Nav.item color='blue' icon='fas fa-download' href={`/download/${this.processShareId()}`}>
<Nav.item color='blue' href={`/download/${this.processShareId()}`}>
download
</Nav.item>
<Nav.item color='blue' icon='fas fa-clone' href={`/new/${this.processShareId()}`}>
<Nav.item color='blue' href={`/new/${this.processShareId()}`}>
clone to new
</Nav.item>
</Nav.dropdown>
@@ -94,13 +86,7 @@ const SharePage = createClass({
</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}
allowPrint={true}
/>
<BrewRenderer text={this.props.brew.text} style={this.props.brew.style} renderer={this.props.brew.renderer} theme={this.props.brew.theme} />
</div>
</div>;
}

View File

@@ -1,8 +1,4 @@
.sharePage{
.navContent .navSection.titleSection {
flex-grow: 1;
justify-content: center;
}
.content{
overflow-y : hidden;
}

View File

@@ -1,6 +1,7 @@
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const ListPage = require('../basePages/listPage/listPage.jsx');
@@ -11,7 +12,6 @@ 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 ErrorNavItem = require('../../navbar/error-navitem.jsx');
const UserPage = createClass({
displayName : 'UserPage',
@@ -19,8 +19,7 @@ const UserPage = createClass({
return {
username : '',
brews : [],
query : '',
error : null
query : ''
};
},
getInitialState : function() {
@@ -51,19 +50,10 @@ const UserPage = createClass({
brewCollection : brewCollection
};
},
errorReported : function(error) {
this.setState({
error
});
},
navItems : function() {
return <Navbar>
<Nav.section>
{this.state.error ?
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
null
}
<NewBrew />
<HelpNavItem />
<RecentNavItem />
@@ -73,7 +63,7 @@ const UserPage = createClass({
},
render : function(){
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>;
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
}
});

View File

@@ -1,12 +0,0 @@
const request = require('superagent');
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
const requestMiddleware = {
get : (path)=>addHeader(request.get(path)),
put : (path)=>addHeader(request.put(path)),
post : (path)=>addHeader(request.post(path)),
delete : (path)=>addHeader(request.delete(path)),
};
module.exports = requestMiddleware;

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 791.04 953.29"><title>Davek</title><g id="Layer_2" data-name="Layer 2"><g id="Davek"><path d="M178.41,13.46a19.33,19.33,0,0,0-4.71,5.38q8.07,6.07,13.46,6.07a8.27,8.27,0,0,0,4.71-1.35,130.23,130.23,0,0,0,16.83-7.07,74.55,74.55,0,0,1,18.85-6.39h2.7q8.07,0,14.81,8.74a944.19,944.19,0,0,0,95.6,4.72q19.5,0,38.37-.67,69.33-2,139.68-5.72t139.7-5.06q16.82-.64,34.34-.66,50.49,0,98.29,3.36-17.5,12.12-22.55,31.64t-5,33.66q.64,22.89.66,45.1,0,47.13-3.36,97-6.07,74.05-9.78,148.11t-5,146.09v17.51a766.1,766.1,0,0,0,8.75,118.48,38.57,38.57,0,0,0-4,17.51,30.94,30.94,0,0,0,.67,6.06q2,12.12,3.36,23.22c.9,7.42,1.57,14.92,2,22.55v3.37a57.93,57.93,0,0,1-3.36,19.52c.43,4.5.67,8.77.67,12.8a260.65,260.65,0,0,1-2.7,37,344.26,344.26,0,0,0-4,52.52,133.5,133.5,0,0,0,8.09,45.44q8.07,22.57,33,36.68-6.06,8.78-20.19,8.77H762.1c-4.5-.45-8.53-.69-12.12-.69a78.11,78.11,0,0,0-21.54,2.7,579.1,579.1,0,0,0-63.64,3.71q-33.31,3.71-67.65,6.39t-68.66,3.37h-4a188.05,188.05,0,0,1-59.92-9.43q20.19-4,39.06-23.22t20.19-47.46q11.44-22.21,11.45-49.82a320.44,320.44,0,0,1,3.36-49.15q-9.45-4.69-10.09-8.75v-2.7a73,73,0,0,1,.66-8.74,105.81,105.81,0,0,0,3.37-12.8,7.49,7.49,0,0,0,.68-3.37q0-4.7-4.05-10.09c.45-4.93.69-10.1.69-15.48a311.71,311.71,0,0,0-3.37-46.45,207.31,207.31,0,0,1-1.35-24.25,274.58,274.58,0,0,1,4-45.1l15.5,6.73q-3.37-17.49-3.37-41.07,0-24.89,8.75-44.44a27.73,27.73,0,0,0,2-9.43,15.32,15.32,0,0,0-3.36-10.09,60.75,60.75,0,0,1-10.1-15.48l-7.39,6.73q2.67-47.79,8.74-99,3.35-33.63,3.37-65.29,0-14.81-.69-29a205.09,205.09,0,0,1-4-41.74,190.26,190.26,0,0,1,2-26.92q4-37,14.81-67.33a25.14,25.14,0,0,1-2.68-11.43,31.13,31.13,0,0,1,.66-6.07V140q0-6.72-8.74-10.09-3.37-16.83-5.73-31.3T521.07,77.41q-55.2,2.7-115.78,4.71-19.55.7-39.72.69-38.38,0-74.06-2.7c-5.4,4.5-8.08,9.21-8.08,14.14v1.34a41.5,41.5,0,0,0,4.37,15.49q3.7,7.4,7.4,15.16a35,35,0,0,1,3.71,15.13q32.31,34.35,64,68.68a335.89,335.89,0,0,1,51.83,73.38q13.46,7.4,18.51,17.49t10.11,19.87q5.06,9.78,10.1,18.85t16.5,11.78v12.12a194.5,194.5,0,0,1-37.38-4q-20.52-4-40.73-6.73a114.48,114.48,0,0,0-17.49-1.35,97.2,97.2,0,0,0-20.2,2q-17.52,4.05-31,20.19-16.84-1.35-27.27-9.75a76.13,76.13,0,0,1-17.51-20.2q-7.06-11.76-14.47-24.9a79.77,79.77,0,0,0-18.84-22.57A305.87,305.87,0,0,1,177.73,237q-28.29-33.67-54.54-69T68,99.31A381.16,381.16,0,0,0,0,38.37q12.79,0,22.89-9.75A190.69,190.69,0,0,1,44.76,10.44Q56.54,2,68.66,0H72Q82.8,0,97,10.76Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 428.05 941.17"><title>Iokharic</title><g id="Layer_2" data-name="Layer 2"><g id="Iokharic"><path d="M334.76,909.61V259.3l2.74-89.18c3.43,0,6.18-8.23,7.55-24.69,3.43,0,7.55-8.92,13.72-27.44,13-11,19.89-21.27,19.89-31.56,0-13-5.48-20.58-17.15-23.32l-30.87,2.74H320.36c-21.27,13-39.79,22.64-56.94,27.44h-37c-11.67,0-26.76,7.55-46,22q-12.34,0-30.86,16.46c-10.29,0-40.48,26.75-91.93,80.95,0,8.23-6.17,21.26-18.52,38.41l-3.43,15.78v41.84L67.23,343c2.74,0,9.6,6.86,19.89,19.9,24,18.52,36.36,30.86,36.36,38.41l-12.35,10.29H105c-24.7-15.78-45.28-32.93-62.43-52.13L15.78,316.92,0,266.85c3.43-17.84,7.55-29.5,13.72-35v-11c0-18.52,7.55-39.79,22-63.8,0-9.6,8.23-21.27,24.7-34.3,0-9.6,15.77-26.07,46.64-50.08,19.9-16.46,46-28.12,76.83-35,5.49-6.86,21.27-14.41,46.65-21.95C238,5.49,251.07,0,270.28,0h137.2c8.91,0,15.77,8.23,20.57,24V40.47l-5.48,8.23V166c0,17.15-7.55,31.55-21.95,43.22v41.15l-2.75,24.7q0,9.26,24.7,30.87v38.41c0,10.29-4.81,19.9-15.09,28.82h-6.86V558.39c0,55.57-4.81,97.41-15.1,124.16-4.8,2.75-7.54,19.21-9.6,48.71l2.74,17.15-2.74,76.14v30.19q0,32.93-32.93,86.43C337.5,937.74,334.76,926.76,334.76,909.61Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 527.7 940.25"><title>Rellanic</title><g id="Layer_2" data-name="Layer 2"><g id="Rellanic"><path d="M527.7,5.45q-3.83,19.65-15,30.56a129.61,129.61,0,0,1-26.46,19.64q-9.84,6.56-31.66,15.28-19.63,7.65-31.64,16.38Q380.33,103.69,342.16,108a468.46,468.46,0,0,1-54,3.28q-15.83,0-30.56-1.1a53.19,53.19,0,0,0-20.19-6.55H217.74q-7.12,1.11-21.29,1.1a51.67,51.67,0,0,1-20.18-4.36q8.72,19.65,25.63,29.46,14.19,8.74,28.38,29.47a634.05,634.05,0,0,1,98.78,90.58l91.12,103.69a65.1,65.1,0,0,0-.54,8.19,42.47,42.47,0,0,0,.54,7.09c.73,1.82,1.27,3.29,1.64,4.37q7.08,8.75,10.92,12,1.62,1.1,12.55,14.19a14,14,0,0,1,3.27,6.55,9.75,9.75,0,0,1,1.1,4.37,9.62,9.62,0,0,1-1.1,4.36q35.46,43.66,51.3,89.5,3.25,9.82,5.45,19.64a288.59,288.59,0,0,1,10.37,68.75v8.19a296,296,0,0,1-9.81,76.94q-7.12,27.3-24,77.5L418,831.65Q383,872,344.88,899.31a243.27,243.27,0,0,1-90.59,38.19,179.84,179.84,0,0,1-31.64,2.75q-38.78,0-81.87-15.84A293.78,293.78,0,0,1,78,886.22a312.61,312.61,0,0,1-51.85-48,300.52,300.52,0,0,0-18-46.94,60.18,60.18,0,0,1-4.92-13.64,82.36,82.36,0,0,1-2.19-19.11,104.89,104.89,0,0,1,.56-10.91,176.12,176.12,0,0,1-1.64-24,199.79,199.79,0,0,1,2.72-32.74Q5.45,663,5.45,645a103.71,103.71,0,0,0-.54-10.92,242.44,242.44,0,0,1,50.74-67.66,646.83,646.83,0,0,0,57.86-61.12q11.44-10.89,25.09-13.1A88.3,88.3,0,0,1,163.71,489q14.17-1.11,29.46-1.1a108.11,108.11,0,0,0,28.38-7.63q17.44,8.75,27.29,12a124.47,124.47,0,0,1,28.38,13.1q8.71,4.38,23.46,17.46,9.29,9.86,17.47,28.38,7.07,12,9.27,21.83a35.16,35.16,0,0,1,1.64,9.83V585a80.23,80.23,0,0,1-8.73,27.28q-8.2,14.19-18,22.93a166.18,166.18,0,0,1-19.65,19.64q-13.1,8.74-20.72,13.1l-7.65-4.37v-1.64q0-12,6.55-18-8.17-6.55-10.36-10.92l-2.18-8.73c0-2.18-.74-5.81-2.19-10.91v-3.29a38,38,0,0,0-3.82-7.63,196.53,196.53,0,0,0-33.84-40.39Q185.53,542.43,162.61,537a163.71,163.71,0,0,0-50.75,9.81q-25.08,8.76-32.2,36Q67.12,615.56,67.13,654.3a256,256,0,0,0,3.26,39.83,176.75,176.75,0,0,0,5.47,28.38Q88.37,770,122.78,812a452.22,452.22,0,0,0,103.13,58.94,153.57,153.57,0,0,0,107,5.45q25.63-12,37.66-27.28,13.62-14.21,23.46-34.93,10.36-18.57,20.2-39.29Q426.72,753.05,437.1,740q3.27-44.76,5.47-61.12a228.17,228.17,0,0,0,3.26-38.21,213.15,213.15,0,0,0-1.64-26.19,245.3,245.3,0,0,0-8.17-48q-2.2-8.17-4.93-16.36-9.27-30.55-34.92-61.12a70,70,0,0,0-2.18-18,29.12,29.12,0,0,0-4.37-10.37,175.28,175.28,0,0,0-17.46-29.48l-18.55-27.27q-12-16.38-16.38-28.38a282.35,282.35,0,0,1-27.81-28.37q-20.22-26.2-24-31.66Q269,295.76,260.29,286q-10.92-12-31.1-25.11-36.56-31.65-79.12-70.94-45.31-39.28-88.41-66.58-14.74-8.17-17.46-16.9a16.93,16.93,0,0,0-.54-3.83V99.87q0-8.73,6.54-19.11A102.47,102.47,0,0,1,63.3,61.12q9.27-9.82,12.56-18.56a223.6,223.6,0,0,1,38.73-3.27,271,271,0,0,1,40.93,3.27A367.15,367.15,0,0,0,215,47.48c6.91,0,13.64-.17,20.2-.56a45,45,0,0,0,21.27,5.47q17.44,0,25.65-1.1h22.93a77.75,77.75,0,0,1,24,7.65,114,114,0,0,1,27.82-3.29H364q27.25,2.2,39.29,2.19,16.34,0,36.55-5.45,19.1-6.55,27.83-22.93h2.72A20.48,20.48,0,0,0,484.58,24c2.17-4.71,6.17-7.09,12-7.09a26.6,26.6,0,0,1,4.92.54v-.54c0-1.08.72-3.46,2.19-7.11a36.74,36.74,0,0,1,6-6.54C512.57,1.1,515.12,0,517.32,0,521,0,524.41,1.82,527.7,5.45Z"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 541.53217 512"
version="1.1"
id="back-cover-icon"
sodipodi:docname="book-front-cover.svg"
width="541.53217"
height="512"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs22131" />
<sodipodi:namedview
id="namedview22129"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.39257813"
inkscape:cx="-263.64179"
inkscape:cy="444.49751"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg22127" />
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<g id="g20308" transform="matrix(3.7795276,0,0,3.7795276,-201.76367,-251.58203)">
<path id="rect20232" d="M95.1,66.6h-8.5c-4.7,0-8.5,3.8-8.5,8.5v21.4c3.5-0.4,7.4-0.5,12-0.5c0.7,0,0.6,0,1.2,0
c0-2.4,0-4.2,0.3-6.2c0.3-2.2,2.2-5.8,3.5-7c0.9-0.9,3-3.2,7-3.7c1-0.1,2-0.1,2.8,0c2.6,0.3,4.6,1.6,6.1,2.6
c3.9,2.7,7.4,6.4,14.8,13.8c6.3,6.3,9.8,9.8,12,12.4c1.1,1.3,2.1,2.4,2.9,4c0.9,1.7,1.4,4.2,1.4,5.6c0,1.4-0.5,4-1.4,5.6
c-0.9,1.6-1.8,2.7-2.9,4c-2.2,2.6-5.6,6-11.8,12.2c-3.8,3.8-7.4,7.3-10.2,9.9c-1.4,1.3-2.6,2.4-3.6,3.3c-0.5,0.4-1,0.8-1.5,1.2
c-0.3,0.2-0.5,0.4-1,0.7s-0.7,0.7-2.8,1.2c-4.3,1.1-6.3,0.4-9.4-1.3c-0.5-0.3-1.9-0.9-3.3-2.6c-1.4-1.7-2.1-3.7-2.4-5.1
c-0.5-2.4-0.5-4.3-0.6-7.2c-3.9,0-6,0.1-6.5,0.1c-0.5,0.1,0.2-0.2-1.2,0.5c-1.7,0.8-3.6,2.8-4.4,4.5c-0.3,0.8-0.5,1-0.6,6.6
c-0.1,2.2-0.2,4.3-0.4,6c0,0.3-0.1,0.6-0.1,0.8v1.9c0,4.7,3.8,8.5,8.5,8.5v16.9c-4.7,0-8.5,3.8-8.5,8.5c0,4.7,3.8,8.5,8.5,8.5h8.5
h76.2c14,0,25.4-11.4,25.4-25.4V92c0-14-11.4-25.4-25.4-25.4L95.1,66.6z M171.3,168.2c4.7,0,8.5,3.8,8.5,8.5c0,4.7-3.8,8.5-8.5,8.5
h-67.7v-16.9L171.3,168.2L171.3,168.2z"/>
<path id="path20297" d="M63.4,158c1.8,1.6,4.5,1.9,5.5,0.7c0.3-0.4,0.7-4,0.8-8.1c0.2-5.9,0.5-7.9,1.4-10c1.7-3.7,4.9-7,8.6-8.9
c3.1-1.5,3.6-1.6,11.7-1.6h8.5l0.3,7.6c0.3,7.5,0.3,7.7,1.7,8.5c0.8,0.5,2.1,0.7,2.8,0.5c0.8-0.2,7.4-6.4,14.9-13.9
c12.4-12.4,13.5-13.7,13.5-15.5c0-1.8-1.1-3.1-13.8-15.7c-14.7-14.7-15.4-15.2-18-12.7c-1,1-1.1,1.9-1.1,7.6c0,3.6-0.2,6.9-0.3,7.4
c-0.3,0.8-1.7,0.9-9.8,0.9c-15.6,0-21.1,1.7-27.9,8.5c-6.5,6.5-8.8,12-8.8,21.1c0,4.7,0.3,6.8,1.3,9.8
C56.2,148.6,60.7,155.7,63.4,158L63.4,158z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 541.53217 512"
version="1.1"
id="front-cover-icon"
sodipodi:docname="book-front-cover.svg"
width="541.53217"
height="512"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs22131" />
<sodipodi:namedview
id="namedview22129"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.39257813"
inkscape:cx="-263.64179"
inkscape:cy="444.49751"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg22127" />
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<g
id="g20308"
transform="matrix(3.7795276,0,0,3.7795276,-201.76367,-251.58203)">
<path
id="rect20232"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:17.9;stroke-linejoin:bevel;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke markers fill;stop-color:#000000"
d="m 78.783305,66.564412 c -14.022889,0 -25.4,11.377111 -25.4,25.4 v 84.666668 c 0,14.02289 11.377111,25.4 25.4,25.4 h 76.199995 8.46667 c 4.68312,0 8.46667,-3.78355 8.46667,-8.46667 0,-4.68311 -3.78355,-8.46666 -8.46667,-8.46666 v -16.93334 c 4.68312,0 8.46667,-3.78355 8.46667,-8.46666 v -1.9327 c -0.0322,-0.27545 -0.0652,-0.54693 -0.0946,-0.83923 -0.17511,-1.74441 -0.30542,-3.81626 -0.37672,-6.02909 -0.18285,-5.67612 -0.29322,-5.86808 -0.63459,-6.62698 -0.74838,-1.66366 -2.65792,-3.64941 -4.38681,-4.49844 -1.41973,-0.69716 -0.72585,-0.45434 -1.20923,-0.51934 -0.47548,-0.0639 -2.54581,-0.13856 -6.47454,-0.14056 -0.0907,2.9929 -0.0862,4.81682 -0.58601,7.244 -0.28023,1.36071 -0.97957,3.42078 -2.40812,5.10356 -1.42519,1.67884 -2.81498,2.35811 -3.28145,2.61896 -3.14428,1.76375 -5.09549,2.43427 -9.41597,1.33997 -2.05224,-0.5197 -2.32631,-0.92288 -2.76159,-1.19527 -0.43528,-0.27239 -0.71007,-0.47684 -0.97461,-0.67593 -0.52909,-0.39816 -0.97871,-0.77171 -1.48622,-1.20664 -1.015,-0.86987 -2.20927,-1.95397 -3.6096,-3.26182 -2.80065,-2.61568 -6.38094,-6.09226 -10.18335,-9.90844 -6.19117,-6.21357 -9.5466,-9.59164 -11.7874,-12.16412 -1.1204,-1.28623 -2.03413,-2.38181 -2.90576,-4.03127 -0.87162,-1.64948 -1.40664,-4.21493 -1.40664,-5.61103 0,-1.4012 0.54783,-3.99366 1.42989,-5.64668 0.88206,-1.65304 1.8039,-2.74855 2.94142,-4.04679 2.27504,-2.59646 5.70131,-6.03358 12.03699,-12.369267 7.37691,-7.376888 10.87768,-11.090687 14.75208,-13.810527 1.45289,-1.019939 3.46378,-2.249133 6.08386,-2.580204 0.87337,-0.110323 1.8133,-0.120299 2.82412,0.0098 4.0433,0.520471 6.12413,2.832857 7.01973,3.728454 1.29782,1.297845 3.1373,4.826955 3.46852,7.049182 0.29817,2.00025 0.26393,3.770666 0.25993,6.212541 0.57954,0.0034 0.50388,0.0217 1.17564,0.0217 4.54211,0 8.44363,0.111537 11.991,0.50953 v -21.41004 c 0,-4.683115 -3.78355,-8.466667 -8.46667,-8.466667 h -8.46667 z m 0,101.599998 h 67.733335 v 16.93334 H 78.783305 c -4.683115,0 -8.466667,-3.78357 -8.466667,-8.46667 0,-4.68313 3.783552,-8.46667 8.466667,-8.46667 z" />
<path
style="color:#000000;fill:#000000;stroke-width:17.9;stroke-linejoin:round;-inkscape-stroke:none;paint-order:stroke markers fill"
d="m 186.69094,157.95633 c 2.67243,-2.24871 7.17957,-9.39389 8.63888,-13.69528 1.03796,-3.05942 1.31928,-5.13546 1.33362,-9.84167 0.0278,-9.1246 -2.25302,-14.5915 -8.79325,-21.07662 -6.8535,-6.79576 -12.35348,-8.46107 -27.94423,-8.46107 -8.05417,0 -9.45684,-0.12924 -9.75203,-0.89852 -0.18964,-0.49417 -0.34479,-3.81715 -0.34479,-7.384389 0,-5.728497 -0.13266,-6.618534 -1.13607,-7.621956 -2.57777,-2.57775 -3.29907,-2.07141 -18.02212,12.651595 -12.64444,12.64444 -13.78771,13.94921 -13.78771,15.73575 0,1.78396 1.13629,3.08846 13.49078,15.48766 7.47518,7.50224 14.10644,13.69554 14.8715,13.88928 0.78576,0.19902 2.0096,-0.002 2.84016,-0.46789 1.42969,-0.80092 1.46523,-0.97351 1.74346,-8.46583 l 0.28402,-7.64825 h 8.52049 c 8.16738,0 8.65373,0.0655 11.73586,1.579 3.72428,1.82893 6.9202,5.12058 8.60236,8.86006 0.94352,2.09748 1.22898,4.1112 1.41901,10.01012 0.13083,4.06143 0.49647,7.70394 0.81253,8.09446 0.94895,1.17251 3.64241,0.80611 5.48753,-0.74645 z"
id="path20297" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -1,53 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 704.00001 512"
version="1.1"
id="svg22127"
sodipodi:docname="book-inside-cover.svg"
width="704"
height="512"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
inkscape:export-filename="InsideCover3.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs22131" />
<sodipodi:namedview
id="namedview22129"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.47274833"
inkscape:cx="83.55397"
inkscape:cy="178.74204"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg22127" />
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
id="path2161-6"
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
<path
style="color:#000000;fill:#000000;fill-opacity:1;stroke-width:1;-inkscape-stroke:none"
d="m 206.76992,184 c -36.98368,0 -73.07301,9.2343 -94.76923,16.9066 v 185.1887 c 27.62799,-7.7405 62.70503,-15.0804 94.76923,-15.0804 28.33376,0 58.16312,7.6425 81.23077,14.806 V 203.0154 C 273.60322,195.1776 243.44241,184 206.76992,184 Z"
id="path4372-8"
sodipodi:nodetypes="sccsccs" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:63.9999;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 255.99995,122.53007 c -31.8285,-15.342 -80.43462,-15.4137 -112,0"
id="path2371-6"
sodipodi:nodetypes="cc" />
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,54 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 704.00001 512"
version="1.1"
id="svg22127"
sodipodi:docname="book-part-cover.svg"
width="704"
height="512"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
inkscape:export-filename="InsideCover3.png"
inkscape:export-xdpi="300"
inkscape:export-ydpi="300"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs22131" />
<sodipodi:namedview
id="namedview22129"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="0.6685671"
inkscape:cx="299.8951"
inkscape:cy="80.021886"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg22127" />
<!--! Font Awesome Pro 6.3.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. -->
<path
id="path2161-6"
style="color:#000000;fill:#000000;stroke-width:1;-inkscape-stroke:none;paint-order:stroke fill markers"
d="M 208,0 C 147.0078,0 94.429433,14.25071 60.367188,26.66992 23.520854,39.96036 0,76.16076 0,112.95896 v 317.8321 c 0,59.8499 56.949847,92.6546 107.47266,76.6035 l -0.1543,0.049 c 26.46715,-8.335 74.84649,-18.3965 100.68164,-18.3965 17.25807,0 61.31688,10.6183 85.14453,18.8438 l 0.0508,0.018 0.0527,0.018 c 19.82627,6.5858 40.84117,4.9222 58.99804,-3.0762 18.04267,7.8799 38.84257,9.6126 58.33594,3.1328 l 0.13672,-0.045 0.13672,-0.047 c 23.88445,-8.0588 67.88646,-18.8437 85.14453,-18.8437 25.83515,0 74.22549,10.0266 100.68164,18.3964 l 0.1543,0.049 0.15625,0.049 C 647.13371,523.05316 704,490.64216 704,430.79226 v -317.8321 c 0,-36.8274 -23.49583,-72.8235 -60.00977,-86.25583 l -0.16015,-0.0606 -0.16211,-0.0566 C 609.79193,14.33005 557.11269,0.0012 496,0.0012 434.5671,0.0012 387.12553,14.01354 352,34.94261 316.87446,14.01344 269.4331,0.0012 208,0.0012 Z m 0,32.00977 c 58.3999,0 103.40004,18.89469 123,33.63279 3.3,2.4564 5,6.4246 5,10.3926 v 356.5508 c 0,10.7702 -11.70041,18.2326 -22.40039,14.6426 -26.59996,-8.9751 -71.69966,-22.2012 -105.59961,-22.2012 -38.49993,0 -88.40045,11.4317 -119.900391,21.3516 C 76.799621,449.96896 64,442.03166 64,430.78906 V 80.94726 C 64,69.51586 70.799631,58.93546 82.099609,54.87306 110.29956,44.57516 157.50009,32.00977 208,32.00977 Z m 288,0 c 50.49991,0 97.70044,12.56619 125.90039,22.76949 C 633.20037,58.93616 640,69.51586 640,80.94726 v 349.8418 c 0,11.2426 -12.79963,19.0854 -24.09961,15.5899 -31.49995,-9.9199 -81.40046,-21.3516 -119.90039,-21.3516 -33.89995,0 -78.99966,13.2261 -105.59961,22.2012 C 379.60041,450.81856 368,443.35616 368,432.58596 V 76.03516 c 0,-3.968 1.60001,-7.9362 5,-10.3926 19.59997,-14.7381 64.6001,-33.63279 123,-33.63279 z M 335.52734,45.75386 c -0.1289,0.093 -0.23137,0.2032 -0.35937,0.2969 -0.198,0.1477 -0.428,0.2796 -0.625,0.4278 z m 33.67969,0.5372 0.24805,0.1875 c -0.0427,-0.033 -0.0937,-0.061 -0.13672,-0.094 -0.0393,-0.03 -0.0713,-0.064 -0.11133,-0.094 z" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 417.64553,213.53304 c 88.71546,-18.9285 95.50522,-18.6158 172.79707,0.054"
id="path2371-8"
sodipodi:nodetypes="cc" />
<path
id="path2315"
style="stroke-width:67.6532;stroke-linejoin:bevel;paint-order:stroke markers fill;stop-color:#000000"
inkscape:transform-center-x="-3.4164388e-06"
inkscape:transform-center-y="-8.443352"
d="m 505.27489,52.89544 25.98603,52.6535 58.10652,8.4434 -42.04628,40.985 9.92578,57.8717 -51.97205,-27.3234 -51.97204,27.3234 9.92578,-57.8717 -42.04627,-40.985 58.10651,-8.4434 z" />
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,57 +0,0 @@
.fac {
display : inline-block;
}
.position-top-left {
content: url('../icons/position-top-left.svg');
}
.position-top-right {
content: url('../icons/position-top-right.svg');
}
.position-bottom-left {
content: url('../icons/position-bottom-left.svg');
}
.position-bottom-right {
content: url('../icons/position-bottom-right.svg');
}
.position-top {
content: url('../icons/position-top.svg');
}
.position-right {
content: url('../icons/position-right.svg');
}
.position-bottom {
content: url('../icons/position-bottom.svg');
}
.position-left {
content: url('../icons/position-left.svg');
}
.mask-edge {
content: url('../icons/mask-edge.svg');
}
.mask-corner {
content: url('../icons/mask-corner.svg');
}
.mask-center {
content: url('../icons/mask-center.svg');
}
.book-front-cover {
content: url('../icons/book-front-cover.svg');
}
.book-back-cover {
content: url('../icons/book-back-cover.svg');
}
.book-inside-cover {
content: url('../icons/book-inside-cover.svg');
}
.book-part-cover {
content: url('../icons/book-part-cover.svg');
}
.davek {
content: url('../icons/Davek.svg');
}
.rellanic {
content: url('../icons/Rellanic.svg');
}
.iokharic {
content: url('../icons/Iokharic.svg');
}

View File

@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="mask-center.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139"><pattern
inkscape:collect="always"
xlink:href="#Strips1_1"
id="pattern3077"
patternTransform="matrix(23.13193,-23.131931,19.25517,19.25517,18.091544,-20.306833)" /><pattern
inkscape:collect="always"
patternUnits="userSpaceOnUse"
width="2"
height="1"
patternTransform="translate(0,0) scale(10,10)"
id="Strips1_1"
inkscape:stockid="Stripes 1:1"><rect
style="fill:black;stroke:none"
x="0"
y="-0.5"
width="1"
height="2"
id="rect2097" /></pattern></defs><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.67711183"
inkscape:cx="31.75251"
inkscape:cy="260.66595"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="M 48,-5.2e-6 C 21.40803,-5.2e-6 1.98e-5,21.408025 1.98e-5,47.999995 V 464 C 1.98e-5,490.59197 21.40803,512 48,512 h 352 c 26.59198,0 48,-21.40803 48,-48 V 47.999995 C 448,21.408025 426.59198,-5.2e-6 400,-5.2e-6 Z M 64,63.999995 H 384 V 448 H 64 Z" /><rect
style="fill:url(#pattern3077);fill-opacity:1;stroke:#000000;stroke-width:48;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206"
width="176"
height="240"
x="136.00002"
y="136"
rx="48"
ry="48" /></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="mask-corner.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139"><pattern
inkscape:collect="always"
xlink:href="#Strips1_1"
id="pattern3077"
patternTransform="matrix(23.131931,-23.131931,19.25517,19.25517,26.214281,-26.952711)" /><pattern
inkscape:collect="always"
patternUnits="userSpaceOnUse"
width="2"
height="1"
patternTransform="translate(0,0) scale(10,10)"
id="Strips1_1"
inkscape:stockid="Stripes 1:1"><rect
style="fill:black;stroke:none"
x="0"
y="-0.5"
width="1"
height="2"
id="rect2097" /></pattern></defs><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.95758074"
inkscape:cx="275.17262"
inkscape:cy="306.50157"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="M 48,-5.2e-6 C 21.40803,-5.2e-6 1.98e-5,21.408025 1.98e-5,47.999995 V 464 C 1.98e-5,490.59197 21.40803,512 48,512 h 352 c 26.59198,0 48,-21.40803 48,-48 V 47.999995 C 448,21.408025 426.59198,-5.2e-6 400,-5.2e-6 Z M 64,63.999995 H 384 V 448 H 64 Z" /><rect
style="fill:url(#pattern3077);fill-opacity:1;stroke:#000000;stroke-width:48;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206"
width="208"
height="240"
x="32.000011"
y="32.000011"
rx="48"
ry="48" /></svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="mask-edge.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139"><pattern
inkscape:collect="always"
xlink:href="#Strips1_1"
id="pattern3077"
patternTransform="matrix(23.131931,-23.13193,19.25517,19.25517,26.214281,-26.952711)" /><pattern
inkscape:collect="always"
patternUnits="userSpaceOnUse"
width="2"
height="1"
patternTransform="translate(0,0) scale(10,10)"
id="Strips1_1"
inkscape:stockid="Stripes 1:1"><rect
style="fill:black;stroke:none"
x="0"
y="-0.5"
width="1"
height="2"
id="rect2097" /></pattern></defs><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.95758074"
inkscape:cx="231.31209"
inkscape:cy="171.78708"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="M 48,-5.2e-6 C 21.40803,-5.2e-6 1.98e-5,21.408025 1.98e-5,47.999995 V 464 C 1.98e-5,490.59197 21.40803,512 48,512 h 352 c 26.59198,0 48,-21.40803 48,-48 V 47.999995 C 448,21.408025 426.59198,-5.2e-6 400,-5.2e-6 Z M 64,63.999995 H 384 V 448 H 64 Z" /><rect
style="fill:url(#pattern3077);fill-opacity:1;stroke:#000000;stroke-width:48;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206"
width="208"
height="447.99997"
x="32.000011"
y="32.000011"
rx="48"
ry="48" /><rect
style="fill:#000000;fill-opacity:1;stroke-width:47.9999;stroke-linejoin:round;stroke-dasharray:none;paint-order:fill markers stroke;stop-color:#000000"
id="rect4640"
width="48"
height="512"
x="216"
y="0" /></svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-bottom-left.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.70792086"
inkscape:cx="174.45453"
inkscape:cy="325.60137"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="m 48,511.99998 c -26.59197,0 -48.00000035682677,-21.40803 -48.00000035682677,-48 v -416 C -3.5682677e-7,21.40801 21.40803,-1.9692461e-5 48,-1.9692461e-5 h 352 c 26.59198,0 48,21.408029692461 48,47.999999692461 v 416 c 0,26.59197 -21.40802,48 -48,48 z m 16,-64 h 320 v -384 H 64 Z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206"
width="208"
height="240"
x="-3.5682677e-07"
y="-512"
rx="48"
ry="48"
transform="scale(1,-1)" /></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-bottom-right.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.70792086"
inkscape:cx="174.45453"
inkscape:cy="325.60137"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="m 400,511.99998 c 26.59197,0 48,-21.40803 48,-48 v -416 C 448,21.40801 426.59197,-1.9692461e-5 400,-1.9692461e-5 H 48 C 21.40802,-1.9692461e-5 -3.5682677e-7,21.40801 -3.5682677e-7,47.99998 v 416 c 0,26.59197 21.40802035682677,48 48.00000035682677,48 z m -16,-64 H 64 v -384 h 320 z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206"
width="208"
height="240"
x="-448"
y="-512"
rx="48"
ry="48"
transform="scale(-1)" /></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-bottom.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="1.0011513"
inkscape:cx="273.18549"
inkscape:cy="216.25103"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201-2"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="m 48,512.00004 c -26.5919,0 -48,-21.4081 -48,-48 V 47.999996 C 0,21.408026 21.4081,-3.8146973e-6 48,-3.8146973e-6 h 352 c 26.592,0 48,21.4080298146973 48,47.9999998146973 V 464.00004 c 0,26.5919 -21.408,48 -48,48 z m 16,-64 H 384 V 63.999996 H 64 Z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.0001;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206-8"
width="447.99997"
height="240"
x="1.40625e-05"
y="-512.00006"
rx="48"
ry="48"
transform="scale(1,-1)" /></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-left.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.70792086"
inkscape:cx="164.56642"
inkscape:cy="243.6713"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201-0"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="M 48,0 C 21.4081,0 0,21.40803 0,48 v 416 c 0,26.59197 21.4081,48 48,48 h 352.0001 c 26.5919,0 48,-21.40803 48,-48 V 48 c 0,-26.59197 -21.4081,-48 -48,-48 z M 64,64 H 384.0001 V 448 H 64 Z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206-2"
width="208"
height="512.00006"
x="7.0762391e-05"
y="-8.8710935e-05"
rx="48"
ry="48.000004" /></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-right.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.70792086"
inkscape:cx="164.56642"
inkscape:cy="243.6713"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201-0"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="m 400.0001,0 c 26.5919,0 48,21.40803 48,48 v 416 c 0,26.59197 -21.4081,48 -48,48 H 48 C 21.4081,512 0,490.59197 0,464 V 48 C 0,21.40803 21.4081,0 48,0 Z m -16,64 H 64 v 384 h 320.0001 z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206-2"
width="208"
height="512.00006"
x="-448.00003"
y="-8.8710935e-05"
rx="48"
ry="48.000004"
transform="scale(-1,1)" /></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-top-left.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.70792086"
inkscape:cx="174.45453"
inkscape:cy="325.60137"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="M 48,0 C 21.40803,0 0,21.40803 0,48 v 416 c 0,26.59197 21.40803,48 48,48 h 352 c 26.59198,0 48,-21.40803 48,-48 V 48 C 448,21.40803 426.59198,0 400,0 Z M 64,64 H 384 V 448 H 64 Z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206"
width="208"
height="240"
x="-3.5682677e-07"
y="-1.9692461e-05"
rx="48"
ry="48" /></svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,46 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-top-right.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="0.70792086"
inkscape:cx="174.45453"
inkscape:cy="325.60137"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="m 400,0 c 26.59197,0 48,21.40803 48,48 v 416 c 0,26.59197 -21.40803,48 -48,48 H 48 C 21.40802,512 -3.5682677e-7,490.59197 -3.5682677e-7,464 V 48 C -3.5682677e-7,21.40803 21.40802,0 48,0 Z M 384,64 H 64 v 384 h 320 z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206"
width="208"
height="240"
x="-448"
y="-1.9692461e-05"
rx="48"
ry="48"
transform="scale(-1,1)" /></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -1,45 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 448 512"
version="1.1"
id="svg135"
sodipodi:docname="position-top.svg"
width="448"
height="512"
xml:space="preserve"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs139" /><sodipodi:namedview
id="namedview137"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
showguides="false"
inkscape:zoom="1.0011513"
inkscape:cx="273.18549"
inkscape:cy="216.25103"
inkscape:window-width="1920"
inkscape:window-height="991"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg135" /><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path
id="rect12201-2"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
d="M 48,0 C 21.4081,0 0,21.4081 0,48 v 416.00004 c 0,26.59197 21.4081,48 48,48 h 352 c 26.592,0 48,-21.40803 48,-48 V 48 C 448,21.4081 426.592,0 400,0 Z M 64,64 H 384 V 448.00004 H 64 Z" /><rect
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:30.0001;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
id="rect12206-8"
width="447.99997"
height="240"
x="1.40625e-05"
y="-3.8146973e-06"
rx="48"
ry="48" /></svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -11,10 +11,9 @@ const template = async function(name, title='', props = {}){
return `<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, height=device-height, interactive-widget=resizes-visual" />
<link href="//use.fontawesome.com/releases/v6.5.1/css/all.css" rel="stylesheet" type="text/css" />
<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`} type="text/css" rel='stylesheet' />
<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">

29
faq.md
View File

@@ -62,13 +62,16 @@ pre {
```
# FAQ
{{wide Updated Apr. 15, 2023}}
{{wide Updated Oct. 11, 2021}}
### The site is down for me! Anyone else?
You can check the site status here: [Everyone or Just Me](https://downforeveryoneorjustme.com/homebrewery.naturalcrit.com)
### How do I log out?
Go to https://homebrewery.naturalcrit.com/login, and hit the "*logout*" link.
### Why am I getting an error when trying to save, and my account is linked to Google?
@@ -102,7 +105,7 @@ The best way to avoid this is to leave space at the end of a column equal to one
### Why do I need to manually create a new page? Why doesn't text flow between pages?
A Homebrewery document is at its core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
A Homebrewery document is at it's core an HTML & CSS document, and currently limited by the specs of those technologies. It is currently not possible to flow content from inside one box ("page") to the inside of another box. It seems likely that someday CSS will add this capability, and if/when that happens, Homebrewery will adopt it as soon as possible.
### Where do I get images?
The Homebrewery does not provide images for use besides some page elements and example images for snippets. You will need to find your own images for use and be sure you are following the appropriate license requirements.
@@ -117,6 +120,26 @@ The fonts used were originally created for use with the English language, though
### Whenever I click on the "Get PDF" button, instead of getting a download, it opens Print Preview in another tab.
Yes, this is by design. In the print preview, select "Save as PDF" as the Destination, and then click "Save". There will be a normal download dialog where you can save your brew as a PDF.
### The preview window is suddenly gone, I can only see the editor side of the Homebrewery (or the other way around).
1. Press `CTRL`+`SHIFT`+`i` (or right-click and select "Inspect") while in the Homebrewery.
2. Expand...
```
- `body`
- `main`
- `div class="homebrew"`
- `div class="editPage page"`
- `div class="content"`
- `div class="splitPane"`
```
There you will find 3 divs: `div class="pane" [...]`, `div class="divider" [...]`, and `div class="pane" [...]`.
The `class="pane"` looks similar to this: `div class="pane" data-reactid="36" style="flex: 0 0 auto; width: 925px;"`.
Change whatever stands behind width: to something smaller than your display width.
### I have white borders on the bottom/sides of the print preview.
The Homebrewery paper size and your print paper size do not match.
@@ -126,4 +149,4 @@ The Homebrewery defaults to creating US Letter page sizes. If you are printing
### Typing `#### Adhesion` in the text editor doesn't show the header at all in the completed page?
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software.
Your ad-blocking software is mistakenly assuming your text to be an ad. Whitelist homebrewery.naturalcrit.com in your ad-blocking software.

View File

@@ -1,43 +0,0 @@
# Windows Installation Instructions
## Before Installing
These instructions assume that you are installing to a completely new, fresh Windows 10 installation. As such, some steps may not be necessary if you are installing to an existing Windows 10 instance.
## Installation instructions
1. Download the installation script from https://raw.githubusercontent.com/naturalcrit/homebrewery/master/install/windows/install.ps1.
2. Run Powershell as an Administrator.
a. Click the Start menu or press the Windows key.
b. Type `powershell` into the Search box.
c. Right click on the Powershell app and select "Run As Administrator".
d. Click YES in the prompt that appears.
3. Change the script execution policy.
a. Run the Powershell command `Set-ExecutionPolicy Bypass -Scope Process`.
b. Allow the change to be made - press Y at the prompt that appears.
4. Run the installation script.
a. Navigate to the location of the script, e.g. `cd C:\Users\ExampleUser\Downloads`.
b. Start the script - `.\install.ps1`
5. Once the script has completed, it will start the Homebrewery server. This will normally cause a Network Access prompt for NodeJS - if this appears, click "Allow".
**NOTE:** At this time, the script **ONLY** installs HomeBrewery. It does **NOT** install the NaturalCrit login system, as that is currently a completely separate project.
---
### Testing
These installation instructions have been tested on the following Ubuntu releases:
- *Windows 10 Home - OS Build 19045.2546*
## Final Notes
While this installation process works successfully at the time of writing (January 23, 2023), it relies on all of the Node.JS packages used in the HomeBrewery project retaining their cross-platform capabilities to continue to function. This is one of the inherent advantages of Node.JS, but it is by no means guaranteed and as such, functionality or even installation may fail without warning at some point in the future.
Regards,
G
January 23, 2023

View File

@@ -13,7 +13,7 @@ npm install
npm audit fix
npm run postinstall
cp install/freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
cp freebsd/rc.d/homebrewery /usr/local/etc/rc.d/
chmod +x /usr/local/etc/rc.d/homebrewery
sysrc homebrewery_enable=YES

View File

@@ -1,51 +0,0 @@
Write-Host Homebrewery Install -BackgroundColor Black -ForegroundColor Yellow
Write-Host =================== -BackgroundColor Black -ForegroundColor Yellow
Write-Host Install Chocolatey -BackgroundColor Black -ForegroundColor Yellow
Write-Host Instructions from https://chocolate.org/install -BackgroundColor Black -ForegroundColor Yellow
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
Write-Host Install Node JS v16.11.1 -BackgroundColor Black -ForegroundColor Yellow
choco install nodejs --version=16.11.1 -y
Write-Host Install MongoDB v 4.4.4 -BackgroundColor Black -ForegroundColor Yellow
choco install mongodb --version=4.4.4 -y
Write-Host Install GIT -BackgroundColor Black -ForegroundColor Yellow
choco install git -y
Write-Host Refresh Environment -BackgroundColor Black -ForegroundColor Yellow
Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1"
Update-SessionEnvironment
Write-Host Create Homebrewery directory - C:\Homebrewery -BackgroundColor Black -ForegroundColor Yellow
mkdir C:\Hombrewery
cd C:\Hombrewery
Write-Host Download Homebrewery project files -BackgroundColor Black -ForegroundColor Yellow
git clone https://github.com/naturalcrit/homebrewery.git
Write-Host Install Homebrewery files -BackgroundColor Black -ForegroundColor Yellow
cd homebrewery
npm install
npm audit fix
Write-Host Set install type to 'local' -BackgroundColor Black -ForegroundColor Yellow
[System.Environment]::SetEnvironmentVariable('NODE_ENV', 'local')
Write-Host INSTALL COMPLETE -BackgroundColor Black -ForegroundColor Yellow
Write-Host To start Homebrewery in the future, open a terminal in the Homebrewery directory and run npm start -BackgroundColor Black -ForegroundColor Yellow
Write-Host ================================================================================================== -BackgroundColor Black -ForegroundColor Yellow
Write-Host Start Homebrewery -BackgroundColor Black -ForegroundColor Yellow
npm start

22923
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "3.13.1",
"version": "3.3.2",
"engines": {
"npm": "^10.2.x",
"node": "^20.8.x"
"node": "16.11.x"
},
"repository": {
"type": "git",
@@ -13,30 +12,20 @@
"scripts": {
"dev": "node scripts/dev.js",
"quick": "node scripts/quick.js",
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
"builddev": "node scripts/buildHomebrew.js --dev",
"build": "node scripts/buildHomebrew.js",
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
"lint": "eslint --fix **/*.{js,jsx}",
"lint:dry": "eslint **/*.{js,jsx}",
"stylelint": "stylelint --fix **/*.{less}",
"stylelint:dry": "stylelint **/*.less",
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
"verify": "npm run lint && npm test",
"test": "jest --runInBand",
"test:api-unit": "jest server/*.spec.js --verbose",
"test:coverage": "jest --coverage --silent --runInBand",
"test": "jest",
"test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
"test:variables": "jest tests/markdown/variables.test.js --verbose",
"test:mustache-syntax": "jest \".*(mustache-syntax).*\" --verbose --noStackTrace",
"test:mustache-syntax:inline": "jest \".*(mustache-syntax).*\" -t '^Inline:.*' --verbose --noStackTrace",
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
"test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run build",
"postinstall": "npm run buildall",
"start": "node server.js"
},
"author": "stolksdorf",
@@ -45,31 +34,11 @@
"build/*"
],
"jest": {
"testTimeout": 30000,
"testTimeout": 15000,
"modulePaths": [
"node_modules",
"mode_modules",
"shared",
"server"
],
"coveragePathIgnorePatterns": [
"build/*"
],
"coverageThreshold": {
"global": {
"statements": 25,
"branches": 10,
"functions": 22,
"lines": 25
},
"server/homebrew.api.js": {
"statements": 65,
"branches": 50,
"functions": 60,
"lines": 70
}
},
"setupFilesAfterEnv": [
"jest-expect-message"
]
},
"babel": {
@@ -82,56 +51,45 @@
]
},
"dependencies": {
"@babel/core": "^7.24.7",
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@googleapis/drive": "^8.11.0",
"body-parser": "^1.20.2",
"classnames": "^2.5.1",
"@babel/core": "^7.19.6",
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.19.4",
"@babel/preset-react": "^7.18.6",
"body-parser": "^1.20.1",
"classnames": "^2.3.2",
"codemirror": "^5.65.6",
"cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.3",
"dompurify": "^3.1.5",
"expr-eval": "^2.0.2",
"express": "^4.19.2",
"dedent-tabs": "^0.10.2",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.7",
"fs-extra": "11.2.0",
"fs-extra": "10.1.0",
"googleapis": "109.0.1",
"js-yaml": "^4.1.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"marked": "11.2.0",
"marked-emoji": "^1.4.1",
"marked-extended-tables": "^1.0.8",
"marked-gfm-heading-id": "^3.2.0",
"marked-smartypants-lite": "^1.0.2",
"marked": "4.2.3",
"marked-extended-tables": "^1.0.5",
"markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.30.1",
"mongoose": "^8.4.5",
"moment": "^2.29.4",
"mongoose": "^6.7.3",
"nanoid": "3.3.4",
"nconf": "^0.12.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-frame-component": "^4.1.3",
"react-router-dom": "6.24.1",
"nconf": "^0.12.0",
"npm": "^8.10.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-frame-component": "4.1.3",
"react-router-dom": "6.4.3",
"sanitize-filename": "1.6.3",
"superagent": "^9.0.2",
"superagent": "^6.1.0",
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-react": "^7.34.3",
"jest": "^29.7.0",
"jest-expect-message": "^1.1.3",
"postcss-less": "^6.0.0",
"stylelint": "^15.11.0",
"stylelint-config-recess-order": "^4.6.0",
"stylelint-config-recommended": "^13.0.0",
"stylelint-stylistic": "^0.4.3",
"supertest": "^7.0.0"
"eslint": "^8.28.0",
"eslint-plugin-react": "^7.31.11",
"jest": "^29.2.2",
"supertest": "^6.3.1"
}
}

View File

@@ -21,7 +21,6 @@ const transforms = {
const build = async ({ bundle, render, ssr })=>{
const css = await lessTransform.generate({ paths: './shared' });
//css = `@layer bundle {\n${css}\n}`;
await fs.outputFile('./build/homebrew/bundle.css', css);
await fs.outputFile('./build/homebrew/bundle.js', bundle);
await fs.outputFile('./build/homebrew/ssr.js', ssr);
@@ -73,7 +72,6 @@ fs.emptyDirSync('./build');
themeData.path = dir;
themes.V3[dir] = (themeData);
fs.copy(`./themes/V3/${dir}/dropdownTexture.png`, `./build/themes/V3/${dir}/dropdownTexture.png`);
fs.copy(`./themes/V3/${dir}/dropdownPreview.png`, `./build/themes/V3/${dir}/dropdownPreview.png`);
const src = `./themes/V3/${dir}/style.less`;
((outputDirectory)=>{
less.render(fs.readFileSync(src).toString(), {
@@ -97,28 +95,6 @@ fs.emptyDirSync('./build');
// Move assets
await fs.copy('./themes/fonts', './build/fonts');
await fs.copy('./themes/assets', './build/assets');
await fs.copy('./client/icons', './build/icons');
//v==---------------------------MOVE CM EDITOR THEMES -----------------------------==v//
const editorThemesBuildDir = './build/homebrew/cm-themes';
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
const editorThemeFile = './themes/codeMirror/editorThemes.json';
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
stream.write('[\n"default"');
for (themeFile of editorThemeFiles) {
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
}
stream.write('\n]\n');
stream.end();
await fs.copy('./themes/codeMirror', './build/homebrew/codeMirror');
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
@@ -154,14 +130,12 @@ fs.emptyDirSync('./build');
// build(bundles);
//
//In development, set up LiveReload (refreshes browser), and Nodemon (restarts server)
if(isDev){
livereload('./build'); // Install the Chrome extension LiveReload to automatically refresh the browser
watchFile('./server.js', { // Restart server when change detected to this file or any nested directory from here
ignore : ['./build', './client', './themes'], // Ignore folders that are not running server code / avoids unneeded restarts
ext : 'js json' // Extensions to watch (only .js/.json by default)
//watch : ['./server', './themes'], // Watch additional folders if needed
});
}
})().catch(console.error);
})().catch(console.error);
//In development set up a watch server and livereload
if(isDev){
livereload('./build');
watchFile('./server.js', {
watch : ['./client', './server'] // Watch additional folders if you want
});
}

View File

@@ -25,8 +25,8 @@
"codemirror/addon/edit/closetag.js",
"codemirror/addon/edit/trailingspace.js",
"codemirror/addon/selection/active-line.js",
"codemirror/addon/hint/show-hint.js",
"moment",
"superagent"
"superagent",
"marked"
]
}

View File

@@ -7,14 +7,6 @@ DB.connect(config).then(()=>{
// before launching server
const PORT = process.env.PORT || config.get('web_port') || 8000;
server.app.listen(PORT, ()=>{
const reset = '\x1b[0m'; // Reset to default style
const bright = '\x1b[1m'; // Bright (bold) style
const cyan = '\x1b[36m'; // Cyan color
const underline = '\x1b[4m'; // Underlined style
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
console.log(`\tserver on port: ${PORT}`);
console.log(`\t${bright + cyan}Open in browser: ${reset}${underline + bright + cyan}http://localhost:${PORT}${reset}\n\n`);
console.log(`server on port: ${PORT}`);
});
});

View File

@@ -16,7 +16,7 @@ const mw = {
.status(401)
.send('Authorization Required');
}
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
const [username, password] = new Buffer(req.get('authorization').split(' ').pop(), 'base64')
.toString('ascii')
.split(':');
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
@@ -26,124 +26,85 @@ const mw = {
}
};
const junkBrewPipeline = [
{ $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
} },
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
{ $match: { textBinSize: { $lt: 140 } } },
{ $limit: 100 }
];
/* Search for brews that are older than 3 days and that are shorter than a tweet */
const junkBrewQuery = HomebrewModel.find({
'$where' : 'this.text.length < 140',
createdAt : {
$lt : Moment().subtract(30, 'days').toDate()
}
}).limit(100).maxTime(60000);
/* Search for brews that aren't compressed (missing the compressed text field) */
const uncompressedBrewQuery = HomebrewModel.find({
'text' : { '$exists': true }
}).lean().limit(10000).select('_id');
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length }))
.catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
junkBrewQuery.exec((err, objs)=>{
if(err) return res.status(500).send(err);
return res.json({ count: objs.length });
});
});
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
/* Removes all empty brews that are older than 3 days and that are shorter than a tweet */
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((docs)=>{
const ids = docs.map((doc)=>doc._id);
return HomebrewModel.deleteMany({ _id: { $in: ids } });
}).then((result)=>{
res.json({ count: result.deletedCount });
}).catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
junkBrewQuery.remove().exec((err, objs)=>{
if(err) return res.status(500).send(err);
return res.json({ count: objs.length });
});
});
/* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{
HomebrewModel.findOne({
$or : [
{ editId: { $regex: req.params.id, $options: 'i' } },
{ shareId: { $regex: req.params.id, $options: 'i' } },
]
}).exec()
.then((brew)=>{
if(!brew) // No document found
return res.status(404).json({ error: 'Document not found' });
else
return res.json(brew);
})
.catch((err)=>{
console.error(err);
return res.status(500).json({ error: 'Internal Server Error' });
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next)=>{
HomebrewModel.findOne({ $or : [
{ editId: { '$regex': req.params.id, '$options': 'i' } },
{ shareId: { '$regex': req.params.id, '$options': 'i' } },
] }).exec((err, brew)=>{
return res.json(brew);
});
});
/* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
const query = uncompressedBrewQuery.clone();
query.exec()
.then((objs)=>{
const ids = objs.map((obj)=>obj._id);
res.json({ count: ids.length, ids });
})
.catch((err)=>{
console.error(err);
res.status(500).send(err.message || 'Internal Server Error');
});
uncompressedBrewQuery.exec((err, objs)=>{
if(err) return res.status(500).send(err);
objs = objs.map((obj)=>{return obj._id;});
return res.json({ count: objs.length, ids: objs });
});
});
/* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{
HomebrewModel.findOne({ _id: req.params.id })
HomebrewModel.get({ _id: req.params.id })
.then((brew)=>{
if(!brew)
return res.status(404).send('Brew not found');
brew.textBin = zlib.deflateRawSync(brew.text); // Compress brew text to binary before saving
brew.text = undefined; // Delete the non-binary text field since it's not needed anymore
if(brew.text) {
brew.textBin = brew.textBin || zlib.deflateRawSync(brew.text); //Don't overwrite textBin if exists
brew.text = undefined;
}
return brew.save();
brew.save((err, obj)=>{
if(err) throw err;
return res.status(200).send(obj);
});
})
.then((obj)=>res.status(200).send(obj))
.catch((err)=>{
console.error(err);
res.status(500).send('Error while saving');
console.log(err);
return res.status(500).send('Error while saving');
});
});
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try {
const totalBrewsCount = await HomebrewModel.countDocuments({});
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
router.get('/admin/stats', mw.adminOnly, (req, res)=>{
HomebrewModel.count({}, (err, count)=>{
return res.json({
totalBrews : totalBrewsCount,
totalPublishedBrews : publishedBrewsCount
totalBrews : count
});
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Internal Server Error' });
}
});
});
router.get('/admin', mw.adminOnly, (req, res)=>{
templateFn('admin', {
url : req.originalUrl
})
.then((page)=>res.send(page))
.catch((err)=>res.sendStatus(500));
.then((page)=>res.send(page))
.catch((err)=>res.sendStatus(500));
});
module.exports = router;

View File

@@ -1,4 +1,4 @@
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
// Set working directory to project root
process.chdir(`${__dirname}/..`);
@@ -15,22 +15,35 @@ const serveCompressedStaticAssets = require('./static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler');
const { DEFAULT_BREW } = require('./brewDefaults.js');
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
const splitTextStyleAndMetadata = (brew)=>{
brew.text = brew.text.replaceAll('\r\n', '\n');
if(brew.text.startsWith('```metadata')) {
const index = brew.text.indexOf('```\n\n');
const metadataSection = brew.text.slice(12, index - 1);
const metadata = yaml.load(metadataSection);
Object.assign(brew, _.pick(metadata, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']));
brew.text = brew.text.slice(index + 5);
}
if(brew.text.startsWith('```css')) {
const index = brew.text.indexOf('```\n\n');
brew.style = brew.text.slice(7, index - 1);
brew.text = brew.text.slice(index + 5);
}
_.defaults(brew, { 'renderer': 'legacy', 'theme': '5ePHB' });
};
const sanitizeBrew = (brew, accessType)=>{
brew._id = undefined;
brew.__v = undefined;
if(accessType !== 'edit' && accessType !== 'shareAuthor') {
if(accessType !== 'edit'){
brew.editId = undefined;
}
return brew;
};
app.use('/', serveCompressedStaticAssets(`build`));
app.use(require('./middleware/content-negotiation.js'));
//app.use(express.static(`${__dirname}/build`));
app.use(require('body-parser').json({ limit: '25mb' }));
app.use(require('cookie-parser')());
app.use(require('./forcessl.mw.js'));
@@ -65,10 +78,10 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
const defaultMetaTags = {
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
title : 'The Homebrewery',
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
image : `${config.get('publicUrl')}/thumbnail.png`,
description : 'A NaturalCrit Tool for Homebrews',
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
type : 'website'
};
@@ -135,7 +148,8 @@ app.get('/changelog', async (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags,
title : 'Changelog',
description : 'Development changelog.'
description : 'Development changelog.',
thumbnail : null
};
splitTextStyleAndMetadata(req.brew);
@@ -178,19 +192,12 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
sanitizeBrew(brew, 'share');
const prefix = 'HB - ';
const encodeRFC3986ValueChars = (str)=>{
return (
encodeURIComponent(str)
.replace(/[!'()*]/g, (char)=>{`%${char.charCodeAt(0).toString(16).toUpperCase()}`;})
);
};
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/plain',
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
});
res.status(200).send(brew.text);
});
@@ -201,7 +208,8 @@ app.get('/user/:username', async (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags,
title : `${req.params.username}'s Collection`,
description : 'View my collection of homebrew on the Homebrewery.'
description : 'View my collection of homebrew on the Homebrewery.',
image : null
// type : could be 'profile'?
};
@@ -211,7 +219,6 @@ app.get('/user/:username', async (req, res, next)=>{
'pageCount',
'description',
'authors',
'lang',
'published',
'views',
'shareId',
@@ -244,7 +251,6 @@ app.get('/user/:username', async (req, res, next)=>{
brew.pageCount = googleBrews[match].pageCount;
brew.renderer = googleBrews[match].renderer;
brew.version = googleBrews[match].version;
brew.webViewLink = googleBrews[match].webViewLink;
googleBrews.splice(match, 1);
}
}
@@ -255,9 +261,6 @@ app.get('/user/:username', async (req, res, next)=>{
}
req.brews = _.map(brews, (brew)=>{
// Clean up brew data
brew.title = brew.title?.trim();
brew.description = brew.description?.trim();
return sanitizeBrew(brew, ownAccount ? 'edit' : 'share');
});
@@ -271,7 +274,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.',
image : req.brew.thumbnail || defaultMetaTags.image,
image : req.brew.thumbnail || null,
type : 'article'
};
@@ -285,20 +288,12 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
const brew = {
shareId : req.brew.shareId,
title : `CLONE - ${req.brew.title}`,
text : req.brew.text,
style : req.brew.style,
renderer : req.brew.renderer,
theme : req.brew.theme,
tags : req.brew.tags
};
req.brew = _.defaults(brew, DEFAULT_BREW);
req.brew.title = `CLONE - ${req.brew.title}`;
req.ogMeta = { ...defaultMetaTags,
title : 'New',
description : 'Start crafting your homebrew on the Homebrewery!'
description : 'Start crafting your homebrew on the Homebrewery!',
image : null
};
return next();
@@ -307,41 +302,45 @@ app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
//Share Page
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
const { brew } = req;
req.ogMeta = { ...defaultMetaTags,
title : req.brew.title || 'Untitled Brew',
description : req.brew.description || 'No description.',
image : req.brew.thumbnail || defaultMetaTags.image,
image : req.brew.thumbnail || null,
type : 'article'
};
// increase visitor view count, do not include visits by author(s)
if(!brew.authors.includes(req.account?.username)){
if(req.params.id.length > 12 && !brew._id) {
const googleId = brew.googleId;
const shareId = brew.shareId;
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
};
brew.authors.includes(req.account?.username) ? sanitizeBrew(req.brew, 'shareAuthor') : sanitizeBrew(req.brew, 'share');
if(req.params.id.length > 12 && !brew._id) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
await HomebrewModel.increaseView({ shareId: brew.shareId });
}
sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
return next();
}));
//Print Page
app.get('/print/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
sanitizeBrew(req.brew, 'share');
splitTextStyleAndMetadata(req.brew);
next();
});
//Account Page
app.get('/account', asyncHandler(async (req, res, next)=>{
const data = {};
data.title = 'Account Information Page';
let auth;
let googleCount = [];
let files;
if(req.account) {
if(req.account.googleId) {
try {
auth = await GoogleActions.authCheck(req.account, res, false);
auth = await GoogleActions.authCheck(req.account, res);
} catch (e) {
auth = undefined;
console.log('Google auth check failed!');
@@ -349,9 +348,9 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
}
if(auth.credentials.access_token) {
try {
googleCount = await GoogleActions.listGoogleBrews(auth);
files = await GoogleActions.listGoogleBrews(auth);
} catch (e) {
googleCount = undefined;
files = undefined;
console.log('List Google files failed!');
console.log(e);
}
@@ -359,19 +358,18 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
}
const query = { authors: req.account.username, googleId: { $exists: false } };
const mongoCount = await HomebrewModel.countDocuments(query)
const brews = await HomebrewModel.find(query, 'id')
.catch((err)=>{
mongoCount = 0;
console.log(err);
});
data.accountDetails = {
username : req.account.username,
issued : req.account.issued,
googleId : Boolean(req.account.googleId),
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
mongoCount : mongoCount,
googleCount : googleCount?.length
data.uiItems = {
username : req.account.username,
issued : req.account.issued,
mongoCount : brews.length,
googleId : Boolean(req.account.googleId),
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
fileCount : files?.length || '-'
};
}
@@ -379,12 +377,14 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
req.ogMeta = { ...defaultMetaTags,
title : `Account Page`,
description : null
description : null,
image : null
};
return next();
}));
const nodeEnv = config.get('node_env');
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
// Local only
@@ -401,7 +401,7 @@ if(isLocalEnvironment){
//Render the page
const templateFn = require('./../client/template.js');
const renderPage = async (req, res)=>{
app.use(asyncHandler(async (req, res, next)=>{
// Create configuration object
const configuration = {
local : isLocalEnvironment,
@@ -410,7 +410,7 @@ const renderPage = async (req, res)=>{
};
const props = {
version : require('./../package.json').version,
url : req.customUrl || req.originalUrl,
url : req.originalUrl,
brew : req.brew,
brews : req.brews,
googleBrews : req.googleBrews,
@@ -424,20 +424,15 @@ const renderPage = async (req, res)=>{
const page = await templateFn('homebrew', title, props)
.catch((err)=>{
console.log(err);
return res.sendStatus(500);
});
return page;
};
//Send rendered page
app.use(asyncHandler(async (req, res, next)=>{
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
}));
//v=====----- Error-Handling Middleware -----=====v//
//Format Errors as plain objects so all fields will appear in the string sent
const formatErrors = (key, value)=>{
//Format Errors so all fields will be sent
const replaceErrors = (key, value)=>{
if(value instanceof Error) {
const error = {};
Object.getOwnPropertyNames(value).forEach(function (key) {
@@ -449,39 +444,13 @@ const formatErrors = (key, value)=>{
};
const getPureError = (error)=>{
return JSON.parse(JSON.stringify(error, formatErrors));
return JSON.parse(JSON.stringify(error, replaceErrors));
};
app.use(async (err, req, res, next)=>{
err.originalUrl = req.originalUrl;
app.use((err, req, res, next)=>{
const status = err.status || 500;
console.error(err);
if(err.originalUrl?.startsWith('/api/')) {
// console.log('API error');
res.status(err.status || err.response?.status || 500).send(err);
return;
}
// console.log('non-API error');
const status = err.status || err.code || 500;
req.ogMeta = { ...defaultMetaTags,
title : 'Error Page',
description : 'Something went wrong!'
};
req.brew = {
...err,
title : 'Error - Something went wrong!',
text : err.errors?.map((error)=>{return error.message;}).join('\n\n') || err.message || 'Unknown error!',
status : status,
HBErrorCode : err.HBErrorCode ?? '00',
pureError : getPureError(err)
};
req.customUrl= '/error';
const page = await renderPage(req, res);
if(!page) return;
res.send(page);
res.status(status).send(getPureError(err));
});
app.use((req, res)=>{

View File

@@ -1,38 +0,0 @@
const _ = require('lodash');
// Default properties for newly-created brews
const DEFAULT_BREW = {
title : '',
text : '',
style : undefined,
description : '',
editId : undefined,
shareId : undefined,
createdAt : undefined,
updatedAt : undefined,
renderer : 'V3',
theme : '5ePHB',
authors : [],
tags : [],
systems : [],
lang : 'en',
thumbnail : '',
views : 0,
published : false,
pageCount : 1,
gDrive : false,
trashed : false
};
// Default values for older brews with missing properties
// e.g., missing "renderer" is assumed to be "legacy"
const DEFAULT_BREW_LOAD = _.defaults(
{
renderer : 'legacy',
},
DEFAULT_BREW);
module.exports = {
DEFAULT_BREW,
DEFAULT_BREW_LOAD
};

View File

@@ -27,8 +27,8 @@ const disconnect = async ()=>{
};
const connect = async (config)=>{
return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
.catch((error)=>handleConnectionError(error));
return await Mongoose.connect(getMongoDBURL(config),
{ retryWrites: false }, handleConnectionError);
};
module.exports = {

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