Merge branch 'master' into Language-Attribute
@@ -55,9 +55,15 @@ jobs:
|
|||||||
at: .
|
at: .
|
||||||
|
|
||||||
# run tests!
|
# run tests!
|
||||||
|
- run:
|
||||||
|
name: Test - API Unit Tests
|
||||||
|
command: npm run test:api-unit
|
||||||
- run:
|
- run:
|
||||||
name: Test - Basic
|
name: Test - Basic
|
||||||
command: npm run test:basic
|
command: npm run test:basic
|
||||||
|
- run:
|
||||||
|
name: Test - Coverage
|
||||||
|
command: npm run test:coverage
|
||||||
- run:
|
- run:
|
||||||
name: Test - Mustache Spans
|
name: Test - Mustache Spans
|
||||||
command: npm run test:mustache-span
|
command: npm run test:mustache-span
|
||||||
|
|||||||
2
.gitignore
vendored
@@ -12,3 +12,5 @@ todo.md
|
|||||||
startDB.bat
|
startDB.bat
|
||||||
startMViewer.bat
|
startMViewer.bat
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
coverage
|
||||||
|
|||||||
32
README.md
@@ -21,24 +21,29 @@ below.
|
|||||||
First, install three programs that The Homebrewery requires to run and retrieve
|
First, install three programs that The Homebrewery requires to run and retrieve
|
||||||
updates:
|
updates:
|
||||||
|
|
||||||
1. install [node](https://nodejs.org/en/)
|
1. install [node](https://nodejs.org/en/), version v16 or higher.
|
||||||
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
1. install [mongodb](https://www.mongodb.com/try/download/community) (Community version)
|
||||||
|
|
||||||
For the easiest installation, follow these steps:
|
For the easiest installation, follow these steps:
|
||||||
1. In the installer, uncheck the option to run as a service.
|
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. 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. Go to the C:\ drive and create a folder called "data".
|
||||||
1. Inside the "data" folder, create a new folder called "db".
|
1. Inside the "data" folder, create a new folder called "db".
|
||||||
1. Open a command prompt or other terminal and navigate to your MongoDB install folder (C:\Program Files\Mongo\Server\4.4\bin).
|
1. Open a command prompt or other terminal and navigate to your MongoDB install folder (C:\Program Files\Mongo\Server\6.0\bin).
|
||||||
1. In the command prompt, run "mongod", which will start up your local database server.
|
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. 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. 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 "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 "New" and paste in the path to the MongoDB "bin" folder.
|
||||||
1. Click "OK" three times to close all the windows.
|
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).
|
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]):
|
Checkout the repo ([documentation][github-clone-repo-docs-url]):
|
||||||
@@ -51,11 +56,19 @@ git clone https://github.com/naturalcrit/homebrewery.git
|
|||||||
Second, you will need to add the environment variable `NODE_ENV=local` to allow
|
Second, you will need to add the environment variable `NODE_ENV=local` to allow
|
||||||
the project to run locally.
|
the project to run locally.
|
||||||
|
|
||||||
You can set this temporarily in your shell of choice:
|
You can set this **temporarily** (until you close the terminal) in your shell of choice with admin privileges:
|
||||||
* Windows Powershell: `$env:NODE_ENV="local"`
|
* Windows Powershell: `$env:NODE_ENV="local"`
|
||||||
* Windows CMD: `set NODE_ENV=local`
|
* Windows CMD: `set NODE_ENV=local`
|
||||||
* Linux / macOS: `export 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
|
Third, you will need to install the Node dependencies, compile the app, and run
|
||||||
it using the two commands:
|
it using the two commands:
|
||||||
|
|
||||||
@@ -65,6 +78,13 @@ it using the two commands:
|
|||||||
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
You should now be able to go to [http://localhost:8000](http://localhost:8000)
|
||||||
in your browser and use The Homebrewery offline.
|
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
|
### Running the application via Docker
|
||||||
|
|
||||||
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
Please see the docs here: [README.DOCKER.md](./README.DOCKER.md)
|
||||||
|
|||||||
193
changelog.md
@@ -1,13 +1,36 @@
|
|||||||
```css
|
```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 {
|
h5 {
|
||||||
font-size: .35cm !important;
|
font-size: .35cm !important;
|
||||||
margin-top: 0.3cm;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page ul ul {
|
.page ul ul {
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page .taskList {
|
||||||
|
display:block;
|
||||||
|
break-inside:auto;
|
||||||
|
}
|
||||||
|
|
||||||
.taskList li input {
|
.taskList li input {
|
||||||
list-style-type : none;
|
list-style-type : none;
|
||||||
margin-left : -0.52cm;
|
margin-left : -0.52cm;
|
||||||
@@ -36,15 +59,176 @@ pre {
|
|||||||
margin-top : 0.1cm;
|
margin-top : 0.1cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page ul + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p + h5 {
|
||||||
|
margin-top: 0.25cm;
|
||||||
|
}
|
||||||
|
|
||||||
.page .openSans {
|
.page .openSans {
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
padding-bottom: 1.5cm;
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
|
|
||||||
|
### 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 correctly
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
##### G-Ambatte
|
||||||
|
|
||||||
|
* [x] Auto-compile Themes CSS on development server
|
||||||
|
|
||||||
|
##### 5e-Cleric
|
||||||
|
|
||||||
|
* [x] Fix cloned brews inheriting the parent view count
|
||||||
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
}}
|
||||||
|
|
||||||
|
\page
|
||||||
|
|
||||||
|
### 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
|
### Thursday 28/10/2022 - v3.3.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -90,7 +274,6 @@ Fixes issues [#2135](https://github.com/naturalcrit/homebrewery/issues/2135)
|
|||||||
|
|
||||||
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
Fixes issues [#2427](https://github.com/naturalcrit/homebrewery/issues/2427)
|
||||||
|
|
||||||
|
|
||||||
##### Gazook:
|
##### Gazook:
|
||||||
|
|
||||||
* [x] Several updates to bug reporting and error popups
|
* [x] Several updates to bug reporting and error popups
|
||||||
@@ -140,6 +323,10 @@ 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)
|
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
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -166,8 +353,6 @@ 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)
|
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
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,8 @@ const BrewRenderer = createClass({
|
|||||||
|
|
||||||
renderStyle : function() {
|
renderStyle : function() {
|
||||||
if(!this.props.style) return;
|
if(!this.props.style) return;
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.props.style} </style>` }} />;
|
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.props.style}\n} </style>` }} />;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.props.style}\n</style>` }} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPage : function(pageText, index){
|
renderPage : function(pageText, index){
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const Editor = createClass({
|
|||||||
onTextChange : ()=>{},
|
onTextChange : ()=>{},
|
||||||
onStyleChange : ()=>{},
|
onStyleChange : ()=>{},
|
||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
|
reportError : ()=>{},
|
||||||
|
|
||||||
renderer : 'legacy'
|
renderer : 'legacy'
|
||||||
};
|
};
|
||||||
@@ -139,10 +140,10 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// Highlight injectors {style}
|
// Highlight injectors {style}
|
||||||
if(line.includes('{') && line.includes('}')){
|
if(line.includes('{') && line.includes('}')){
|
||||||
const regex = /(?<!{){(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\1}/g;
|
const regex = /(?:^|[^{\n])({(?=((?::(?:"[\w,\-()#%. ]*"|[\w\-()#%.]*)|[^"':{}\s]*)*))\2})/gm;
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null) {
|
while ((match = regex.exec(line)) != null) {
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: 'injection' });
|
codeMirror.markText({ line: lineNumber, ch: line.indexOf(match[1]) }, { line: lineNumber, ch: line.indexOf(match[1]) + match[1].length }, { className: 'injection' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Highlight inline spans {{content}}
|
// Highlight inline spans {{content}}
|
||||||
@@ -291,7 +292,8 @@ const Editor = createClass({
|
|||||||
rerenderParent={this.rerenderParent} />
|
rerenderParent={this.rerenderParent} />
|
||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
onChange={this.props.onMetaChange} />
|
onChange={this.props.onMetaChange}
|
||||||
|
reportError={this.props.reportError}/>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,18 +4,24 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Combobox = require('client/components/combobox.jsx');
|
const Combobox = require('client/components/combobox.jsx');
|
||||||
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
const validations = require('./validations.js')
|
const validations = require('./validations.js');
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
const homebreweryThumbnail = require('../../thumbnail.png');
|
const homebreweryThumbnail = require('../../thumbnail.png');
|
||||||
|
|
||||||
|
const callIfExists = (val, fn, ...args)=>{
|
||||||
|
if(val[fn]) {
|
||||||
|
val[fn](...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MetadataEditor = createClass({
|
const MetadataEditor = createClass({
|
||||||
displayName : 'MetadataEditor',
|
displayName : 'MetadataEditor',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
@@ -33,7 +39,8 @@ const MetadataEditor = createClass({
|
|||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : 'en'
|
lang : 'en'
|
||||||
},
|
},
|
||||||
onChange : ()=>{}
|
onChange : ()=>{},
|
||||||
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -55,15 +62,13 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleFieldChange : function(name, e){
|
handleFieldChange : function(name, e){
|
||||||
e.persist();
|
|
||||||
|
|
||||||
// load validation rules, and check input value against them
|
// load validation rules, and check input value against them
|
||||||
const inputRules = validations[name] ?? [];
|
const inputRules = validations[name] ?? [];
|
||||||
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
const validationErr = inputRules.map((rule)=>rule(e.target.value)).filter(Boolean);
|
||||||
|
|
||||||
// if no validation rules, save to props
|
// if no validation rules, save to props
|
||||||
if(validationErr.length === 0){
|
if(validationErr.length === 0){
|
||||||
e.target.setCustomValidity('');
|
callIfExists(e.target, 'setCustomValidity', '');
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.metadata,
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
@@ -73,10 +78,14 @@ const MetadataEditor = createClass({
|
|||||||
const errMessage = validationErr.map((err)=>{
|
const errMessage = validationErr.map((err)=>{
|
||||||
return `- ${err}`;
|
return `- ${err}`;
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
console.log(`Input error ${errMessage}`);
|
|
||||||
e.target.setCustomValidity(errMessage);
|
// console.log(`Input error ${errMessage}`);
|
||||||
e.target.reportValidity();
|
// e.target.setCustomValidity(errMessage);
|
||||||
};
|
// e.target.reportValidity();
|
||||||
|
// };
|
||||||
|
callIfExists(e.target, 'setCustomValidity', errMessage);
|
||||||
|
callIfExists(e.target, 'reportValidity');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSystem : function(system, e){
|
handleSystem : function(system, e){
|
||||||
@@ -125,8 +134,12 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
request.delete(`/api/${this.props.metadata.googleId ?? ''}${this.props.metadata.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end((err, res)=>{
|
||||||
window.location.href = '/';
|
if(err) {
|
||||||
|
this.props.reportError(err);
|
||||||
|
} else {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -188,6 +201,10 @@ const MetadataEditor = createClass({
|
|||||||
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||||
{`${theme.renderer} : ${theme.name}`}
|
{`${theme.renderer} : ${theme.name}`}
|
||||||
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
<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>;
|
</div>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -197,14 +214,14 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
if(this.props.metadata.renderer == 'legacy') {
|
if(this.props.metadata.renderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled' trigger='disabled'>
|
<Nav.dropdown className='disabled value' trigger='disabled'>
|
||||||
<div>
|
<div>
|
||||||
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
||||||
</div>
|
</div>
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown trigger='click'>
|
<Nav.dropdown className='value' trigger='click'>
|
||||||
<div>
|
<div>
|
||||||
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
||||||
</div>
|
</div>
|
||||||
@@ -290,6 +307,8 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='metadataEditor'>
|
return <div className='metadataEditor'>
|
||||||
|
<h1 className='sectionHead'>Brew</h1>
|
||||||
|
|
||||||
<div className='field title'>
|
<div className='field title'>
|
||||||
<label>title</label>
|
<label>title</label>
|
||||||
<input type='text' className='value'
|
<input type='text' className='value'
|
||||||
@@ -323,8 +342,6 @@ const MetadataEditor = createClass({
|
|||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
@@ -338,6 +355,23 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
{this.renderRenderOptions()}
|
{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 authors 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'>
|
<div className='field publish'>
|
||||||
<label>publish</label>
|
<label>publish</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
|
|||||||
@@ -10,6 +10,15 @@
|
|||||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
|
|
||||||
|
.sectionHead {
|
||||||
|
font-weight: 1000;
|
||||||
|
margin: 20px 0;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
@@ -26,13 +35,13 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 5 0 200px;
|
flex: 5 0 200px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.field{
|
.field{
|
||||||
display : flex;
|
display : flex;
|
||||||
|
flex-wrap : wrap;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -89,6 +98,10 @@
|
|||||||
&.language .language-dropdown {
|
&.language .language-dropdown {
|
||||||
max-width:150px;
|
max-width:150px;
|
||||||
}
|
}
|
||||||
|
small {
|
||||||
|
font-size : 0.6em;
|
||||||
|
font-style : italic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -139,10 +152,6 @@
|
|||||||
button.unpublish{
|
button.unpublish{
|
||||||
.button(@silver);
|
.button(@silver);
|
||||||
}
|
}
|
||||||
small{
|
|
||||||
font-size : 0.6em;
|
|
||||||
font-style : italic;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete.field .value{
|
.delete.field .value{
|
||||||
@@ -159,7 +168,6 @@
|
|||||||
font-size : 13.33px;
|
font-size : 13.33px;
|
||||||
.navDropdownContainer {
|
.navDropdownContainer {
|
||||||
background-color : white;
|
background-color : white;
|
||||||
width : 100%;
|
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 500;
|
z-index : 500;
|
||||||
&.disabled {
|
&.disabled {
|
||||||
@@ -182,24 +190,51 @@
|
|||||||
}
|
}
|
||||||
.navDropdown {
|
.navDropdown {
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
.item {
|
.item {
|
||||||
padding : 3px 3px;
|
padding : 3px 3px;
|
||||||
border-top : 1px solid rgb(118, 118, 118);
|
border-top : 1px solid rgb(118, 118, 118);
|
||||||
position : relative;
|
position : relative;
|
||||||
overflow : hidden;
|
overflow : visible;
|
||||||
background-color : white;
|
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 {
|
&:hover {
|
||||||
background-color : @blue;
|
background-color : @blue;
|
||||||
color : white;
|
color : white;
|
||||||
}
|
}
|
||||||
img {
|
&:hover > .preview {
|
||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
opacity: 1;
|
||||||
|
}
|
||||||
|
>img {
|
||||||
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
left : ~"max(100px, 100% - 300px)";
|
right : 0;
|
||||||
top : 0px;
|
top : 0px;
|
||||||
|
width : 50%;
|
||||||
|
height : 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,6 +242,7 @@
|
|||||||
}
|
}
|
||||||
.field .list {
|
.field .list {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
|||||||
@@ -163,15 +163,23 @@ const SnippetGroup = createClass({
|
|||||||
onSnippetClick : function(){},
|
onSnippetClick : function(){},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSnippetClick : function(snippet){
|
handleSnippetClick : function(e, snippet){
|
||||||
|
e.stopPropagation();
|
||||||
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
this.props.onSnippetClick(execute(snippet.gen, this.props.brew));
|
||||||
},
|
},
|
||||||
renderSnippets : function(){
|
renderSnippets : function(snippets){
|
||||||
return _.map(this.props.snippets, (snippet)=>{
|
return _.map(snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={()=>this.handleSnippetClick(snippet)}>
|
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
||||||
<i className={snippet.icon} />
|
<i className={snippet.icon} />
|
||||||
{snippet.name}
|
<span className='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></>}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -182,7 +190,7 @@ const SnippetGroup = createClass({
|
|||||||
<span className='groupName'>{this.props.groupName}</span>
|
<span className='groupName'>{this.props.groupName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='dropdown'>
|
<div className='dropdown'>
|
||||||
{this.renderSnippets()}
|
{this.renderSnippets(this.props.snippets)}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
@import (less) './client/icons/customIcons.less';
|
||||||
.snippetBar{
|
.snippetBar{
|
||||||
@menuHeight : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
.snippetGroup{
|
.snippetGroup{
|
||||||
border-right : 1px solid black;
|
border-right : 1px solid black;
|
||||||
&:hover{
|
&:hover{
|
||||||
.dropdown{
|
&>.dropdown{
|
||||||
visibility : visible;
|
visibility : visible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,15 +97,45 @@
|
|||||||
background-color : #ddd;
|
background-color : #ddd;
|
||||||
.snippet{
|
.snippet{
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
padding : 5px;
|
display : flex;
|
||||||
cursor : pointer;
|
align-items : center;
|
||||||
font-size : 10px;
|
min-width : max-content;
|
||||||
|
padding : 5px;
|
||||||
|
cursor : pointer;
|
||||||
|
font-size : 10px;
|
||||||
i{
|
i{
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
|
height : 1.2em;
|
||||||
|
&~i{
|
||||||
|
margin-right: 0;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
margin-right : auto;
|
||||||
|
}
|
||||||
|
.beta {
|
||||||
|
color : white;
|
||||||
|
padding : 4px 6px;
|
||||||
|
line-height : 1em;
|
||||||
|
margin-left : 5px;
|
||||||
|
align-self : center;
|
||||||
|
background : grey;
|
||||||
|
border-radius : 12px;
|
||||||
|
font-family : monospace;
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover{
|
||||||
background-color : #999;
|
background-color : #999;
|
||||||
|
&>.dropdown{
|
||||||
|
visibility : visible;
|
||||||
|
&.side {
|
||||||
|
left: 100%;
|
||||||
|
top: 0%;
|
||||||
|
margin-left:0;
|
||||||
|
box-shadow: -1px 1px 2px 0px #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ const StringArrayEditor = createClass({
|
|||||||
label : '',
|
label : '',
|
||||||
values : [],
|
values : [],
|
||||||
valuePatterns : null,
|
valuePatterns : null,
|
||||||
|
validators : [],
|
||||||
placeholder : '',
|
placeholder : '',
|
||||||
|
notes : [],
|
||||||
unique : false,
|
unique : false,
|
||||||
cannotEdit : [],
|
cannotEdit : [],
|
||||||
onChange : ()=>{}
|
onChange : ()=>{}
|
||||||
@@ -83,7 +85,8 @@ const StringArrayEditor = createClass({
|
|||||||
}
|
}
|
||||||
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||||
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||||
return matchesPatterns && uniqueIfSet;
|
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
|
||||||
|
return matchesPatterns && uniqueIfSet && passesValidators;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleValueInputKeyDown : function(event, index) {
|
handleValueInputKeyDown : function(event, index) {
|
||||||
@@ -123,17 +126,21 @@ const StringArrayEditor = createClass({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return <div className='field values'>
|
return <div className='field'>
|
||||||
<label>{this.props.label}</label>
|
<label>{this.props.label}</label>
|
||||||
<div className='list'>
|
<div style={{ flex: '1 0' }}>
|
||||||
{valueElements}
|
<div className='list'>
|
||||||
<div className='input-group'>
|
{valueElements}
|
||||||
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
<div className='input-group'>
|
||||||
value={this.state.temporaryValue}
|
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||||
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
value={this.state.temporaryValue}
|
||||||
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||||
{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}
|
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||||
|
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{this.props.notes ? this.props.notes.map((n)=><p><small>{n}</small></p>) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 305 KiB |
8
client/homebrew/googleDrive.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 755 B |
|
Before Width: | Height: | Size: 17 KiB |
85
client/homebrew/navbar/error-navitem.jsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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 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>;
|
||||||
|
} else 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(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!
|
||||||
|
<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;
|
||||||
77
client/homebrew/navbar/error-navitem.less
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -42,7 +42,6 @@ const AccountPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderUiItems : function() {
|
renderUiItems : function() {
|
||||||
// console.log(this.props.uiItems);
|
|
||||||
return <>
|
return <>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h1>Account Information <i className='fas fa-user'></i></h1>
|
<h1>Account Information <i className='fas fa-user'></i></h1>
|
||||||
@@ -51,12 +50,16 @@ const AccountPage = createClass({
|
|||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
<h3>Homebrewery Information <NaturalCritIcon /></h3>
|
||||||
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount || '-'}</p>
|
<p><strong>Brews on Homebrewery: </strong> {this.props.uiItems.mongoCount}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className='dataGroup'>
|
<div className='dataGroup'>
|
||||||
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
<h3>Google Information <i className='fab fa-google-drive'></i></h3>
|
||||||
<p><strong>Linked to Google: </strong> {this.props.uiItems.googleId ? 'YES' : 'NO'}</p>
|
<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> : '' }
|
{this.props.uiItems.googleId &&
|
||||||
|
<p>
|
||||||
|
<strong>Brews on Google Drive: </strong> {this.props.uiItems.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>
|
||||||
</>;
|
</>;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ const createClass = require('create-react-class');
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
const request = require('superagent');
|
const request = require('../../../../utils/request-middleware.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.png');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const BrewItem = createClass({
|
const BrewItem = createClass({
|
||||||
@@ -18,7 +18,8 @@ const BrewItem = createClass({
|
|||||||
description : '',
|
description : '',
|
||||||
authors : [],
|
authors : [],
|
||||||
stubbed : true
|
stubbed : true
|
||||||
}
|
},
|
||||||
|
reportError : ()=>{}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -33,8 +34,12 @@ const BrewItem = createClass({
|
|||||||
|
|
||||||
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
request.delete(`/api/${this.props.brew.googleId ?? ''}${this.props.brew.editId}`)
|
||||||
.send()
|
.send()
|
||||||
.end(function(err, res){
|
.end((err, res)=>{
|
||||||
location.reload();
|
if(err) {
|
||||||
|
this.props.reportError(err);
|
||||||
|
} else {
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.googleDriveIcon {
|
.googleDriveIcon {
|
||||||
height : 20px;
|
height : 18px;
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const ListPage = createClass({
|
|||||||
brews : []
|
brews : []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
navItems : <></>
|
navItems : <></>,
|
||||||
|
reportError : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -81,7 +82,7 @@ const ListPage = createClass({
|
|||||||
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
if(!brews || !brews.length) return <div className='noBrews'>No Brews.</div>;
|
||||||
|
|
||||||
return _.map(brews, (brew, idx)=>{
|
return _.map(brews, (brew, idx)=>{
|
||||||
return <BrewItem brew={brew} key={idx}/>;
|
return <BrewItem brew={brew} key={idx} reportError={this.props.reportError}/>;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -218,12 +219,13 @@ const ListPage = createClass({
|
|||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='listPage sitePage'>
|
return <div className='listPage sitePage'>
|
||||||
|
{/*<style>@layer V3_5ePHB, bundle;</style>*/}
|
||||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet'/>
|
||||||
{this.props.navItems}
|
{this.props.navItems}
|
||||||
{this.renderSortOptions()}
|
{this.renderSortOptions()}
|
||||||
|
|
||||||
<div className='content V3'>
|
<div className='content V3'>
|
||||||
<div className='phb page'>
|
<div className='page'>
|
||||||
{this.renderBrewCollection(this.state.brewCollection)}
|
{this.renderBrewCollection(this.state.brewCollection)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
-moz-column-width : auto;
|
-moz-column-width : auto;
|
||||||
-webkit-column-gap : auto;
|
-webkit-column-gap : auto;
|
||||||
-moz-column-gap : auto;
|
-moz-column-gap : auto;
|
||||||
|
height : auto;
|
||||||
|
min-height : 279.4mm;
|
||||||
|
margin : 20px auto;
|
||||||
}
|
}
|
||||||
.listPage{
|
.listPage{
|
||||||
.content{
|
.content{
|
||||||
.phb{
|
.page{
|
||||||
.noColumns();
|
.noColumns() !important; //Needed to override PHB Theme since this is on a lower @layer
|
||||||
height : auto;
|
|
||||||
min-height : 279.4mm;
|
|
||||||
margin : 20px auto;
|
|
||||||
&::after{
|
&::after{
|
||||||
display : none;
|
display : none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ require('./editPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -12,6 +12,7 @@ const Navbar = require('../../navbar/navbar.jsx');
|
|||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
|
|
||||||
@@ -21,8 +22,9 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const googleDriveActive = require('../../googleDrive.png');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const googleDriveInactive = require('../../googleDriveMono.png');
|
|
||||||
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 3000;
|
const SAVE_TIMEOUT = 3000;
|
||||||
|
|
||||||
@@ -30,25 +32,7 @@ const EditPage = createClass({
|
|||||||
displayName : 'EditPage',
|
displayName : 'EditPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW_LOAD
|
||||||
text : '',
|
|
||||||
style : '',
|
|
||||||
shareId : null,
|
|
||||||
editId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
gDrive : false,
|
|
||||||
trashed : false,
|
|
||||||
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
tags : '',
|
|
||||||
published : false,
|
|
||||||
authors : [],
|
|
||||||
systems : [],
|
|
||||||
renderer : 'legacy',
|
|
||||||
lang : 'en'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -61,7 +45,7 @@ const EditPage = createClass({
|
|||||||
alertLoginToTransfer : false,
|
alertLoginToTransfer : false,
|
||||||
saveGoogle : this.props.brew.googleId ? true : false,
|
saveGoogle : this.props.brew.googleId ? true : false,
|
||||||
confirmGoogleTransfer : false,
|
confirmGoogleTransfer : false,
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(this.props.brew.text),
|
htmlErrors : Markdown.validate(this.props.brew.text),
|
||||||
url : '',
|
url : '',
|
||||||
autoSave : true,
|
autoSave : true,
|
||||||
@@ -76,7 +60,6 @@ const EditPage = createClass({
|
|||||||
url : window.location.href
|
url : window.location.href
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
|
||||||
|
|
||||||
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
|
||||||
@@ -173,7 +156,10 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
|
||||||
}));
|
}));
|
||||||
this.clearErrors();
|
this.setState({
|
||||||
|
error : null,
|
||||||
|
isSaving : false
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
closeAlerts : function(event){
|
closeAlerts : function(event){
|
||||||
@@ -189,24 +175,16 @@ const EditPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
saveGoogle : !prevState.saveGoogle,
|
saveGoogle : !prevState.saveGoogle,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
errors : null
|
error : null
|
||||||
}), ()=>this.save());
|
}), ()=>this.save());
|
||||||
},
|
},
|
||||||
|
|
||||||
clearErrors : function(){
|
|
||||||
this.setState({
|
|
||||||
errors : null,
|
|
||||||
isSaving : false
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
isSaving : true,
|
isSaving : true,
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -221,8 +199,9 @@ const EditPage = createClass({
|
|||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error Updating Local Brew');
|
console.log('Error Updating Local Brew');
|
||||||
this.setState({ errors: err });
|
this.setState({ error: err });
|
||||||
});
|
});
|
||||||
|
if(!res) return;
|
||||||
|
|
||||||
this.savedBrew = res.body;
|
this.savedBrew = res.body;
|
||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
@@ -231,7 +210,8 @@ const EditPage = createClass({
|
|||||||
brew : { ...prevState.brew,
|
brew : { ...prevState.brew,
|
||||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||||
editId : this.savedBrew.editId,
|
editId : this.savedBrew.editId,
|
||||||
shareId : this.savedBrew.shareId
|
shareId : this.savedBrew.shareId,
|
||||||
|
version : this.savedBrew.version
|
||||||
},
|
},
|
||||||
isPending : false,
|
isPending : false,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
@@ -241,10 +221,7 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
renderGoogleDriveIcon : function(){
|
renderGoogleDriveIcon : function(){
|
||||||
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
|
||||||
{this.state.saveGoogle
|
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
|
||||||
? <img src={googleDriveActive} alt='googleDriveActive'/>
|
|
||||||
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{this.state.confirmGoogleTransfer &&
|
{this.state.confirmGoogleTransfer &&
|
||||||
<div className='errorContainer' onClick={this.closeAlerts}>
|
<div className='errorContainer' onClick={this.closeAlerts}>
|
||||||
@@ -281,67 +258,6 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
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()){
|
if(this.state.autoSaveWarning && this.hasChanges()){
|
||||||
this.setAutosaveWarning();
|
this.setAutosaveWarning();
|
||||||
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
|
||||||
@@ -385,6 +301,12 @@ const EditPage = createClass({
|
|||||||
this.warningTimer;
|
this.warningTimer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
errorReported : function(error) {
|
||||||
|
this.setState({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
renderAutoSaveButton : function(){
|
renderAutoSaveButton : function(){
|
||||||
return <Nav.item onClick={this.handleAutoSave}>
|
return <Nav.item onClick={this.handleAutoSave}>
|
||||||
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
|
||||||
@@ -429,10 +351,13 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderGoogleDriveIcon()}
|
{this.renderGoogleDriveIcon()}
|
||||||
<Nav.dropdown className='save-menu'>
|
{this.state.error ?
|
||||||
{this.renderSaveButton()}
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
{this.renderAutoSaveButton()}
|
<Nav.dropdown className='save-menu'>
|
||||||
</Nav.dropdown>
|
{this.renderSaveButton()}
|
||||||
|
{this.renderAutoSaveButton()}
|
||||||
|
</Nav.dropdown>
|
||||||
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem/>
|
<HelpNavItem/>
|
||||||
<Nav.dropdown>
|
<Nav.dropdown>
|
||||||
@@ -470,6 +395,7 @@ const EditPage = createClass({
|
|||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
|
|||||||
@@ -13,87 +13,17 @@
|
|||||||
cursor : initial;
|
cursor : initial;
|
||||||
color : #666;
|
color : #666;
|
||||||
}
|
}
|
||||||
&.error{
|
|
||||||
position : relative;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.googleDriveStorage {
|
.googleDriveStorage {
|
||||||
position : relative;
|
position : relative;
|
||||||
}
|
}
|
||||||
.googleDriveStorage img{
|
.googleDriveStorage img{
|
||||||
height : 20px;
|
height : 18px;
|
||||||
padding : 0px;
|
padding : 0px;
|
||||||
margin : -5px;
|
margin : -5px;
|
||||||
}
|
|
||||||
.errorContainer{
|
&.inactive {
|
||||||
animation-name: glideDown;
|
filter: grayscale(1);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const React = require('react');
|
|||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -12,35 +12,38 @@ const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
|||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const HomePage = createClass({
|
const HomePage = createClass({
|
||||||
displayName : 'HomePage',
|
displayName : 'HomePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW,
|
||||||
text : '',
|
ver : '0.0.0'
|
||||||
},
|
|
||||||
ver : '0.0.0'
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text
|
welcomeText : this.props.brew.text,
|
||||||
|
error : undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(this.state.brew)
|
||||||
.end((err, res)=>{
|
.end((err, res)=>{
|
||||||
if(err) return;
|
if(err) {
|
||||||
|
this.setState({ error: err });
|
||||||
|
return;
|
||||||
|
}
|
||||||
const brew = res.body;
|
const brew = res.body;
|
||||||
window.location = `/edit/${brew.editId}`;
|
window.location = `/edit/${brew.editId}`;
|
||||||
});
|
});
|
||||||
@@ -56,6 +59,10 @@ const HomePage = createClass({
|
|||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
return <Navbar ver={this.props.ver}>
|
return <Navbar ver={this.props.ver}>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
|
|||||||
@@ -40,4 +40,11 @@
|
|||||||
right : 350px;
|
right : 350px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navItem.save{
|
||||||
|
background-color: @orange;
|
||||||
|
&:hover{
|
||||||
|
background-color: @green;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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!
|
If you want to save ink or have a monochrome printer, add the **PRINT → {{fas,fa-tint}} Ink Friendly** snippet to your brew!
|
||||||
}}
|
}}
|
||||||
|
|
||||||
 {position:absolute,bottom:20px,left:130px,width:220px}
|
 {position:absolute,bottom:20px,left:130px,width:220px}
|
||||||
|
|
||||||
{{artist,bottom:160px,left:100px
|
{{artist,bottom:160px,left:100px
|
||||||
##### Homebrew Mug
|
##### Homebrew Mug
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ require('./newPage.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const request = require('superagent');
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
const Markdown = require('naturalcrit/markdown.js');
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
|
||||||
@@ -17,6 +18,8 @@ const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
|||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
const METAKEY = 'homebrewery-new-meta';
|
const METAKEY = 'homebrewery-new-meta';
|
||||||
@@ -26,38 +29,18 @@ const NewPage = createClass({
|
|||||||
displayName : 'NewPage',
|
displayName : 'NewPage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW
|
||||||
text : '',
|
|
||||||
style : undefined,
|
|
||||||
title : '',
|
|
||||||
description : '',
|
|
||||||
renderer : 'V3',
|
|
||||||
theme : '5ePHB',
|
|
||||||
lang : 'en'
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
let brew = this.props.brew;
|
const 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',
|
|
||||||
lang : brew.lang ?? 'en'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
brew : brew,
|
brew : brew,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
errors : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text)
|
htmlErrors : Markdown.validate(brew.text)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -86,7 +69,8 @@ const NewPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
if(brew.style)
|
||||||
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -143,14 +127,6 @@ const NewPage = createClass({
|
|||||||
;
|
;
|
||||||
},
|
},
|
||||||
|
|
||||||
clearErrors : function(){
|
|
||||||
this.setState({
|
|
||||||
errors : null,
|
|
||||||
isSaving : false
|
|
||||||
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
save : async function(){
|
save : async function(){
|
||||||
this.setState({
|
this.setState({
|
||||||
isSaving : true
|
isSaving : true
|
||||||
@@ -173,7 +149,7 @@ const NewPage = createClass({
|
|||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(err);
|
console.log(err);
|
||||||
this.setState({ isSaving: false, errors: err });
|
this.setState({ isSaving: false, error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
|
|
||||||
@@ -185,67 +161,6 @@ const NewPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderSaveButton : function(){
|
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){
|
if(this.state.isSaving){
|
||||||
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
|
||||||
save...
|
save...
|
||||||
@@ -275,7 +190,10 @@ const NewPage = createClass({
|
|||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{this.renderSaveButton()}
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
this.renderSaveButton()
|
||||||
|
}
|
||||||
{this.renderLocalPrintButton()}
|
{this.renderLocalPrintButton()}
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
|
|||||||
@@ -4,79 +4,5 @@
|
|||||||
&:hover{
|
&:hover{
|
||||||
background-color: @green;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ const PrintPage = createClass({
|
|||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : {
|
||||||
text : this.props.brew.text || '',
|
text : this.props.brew.text || '',
|
||||||
style : this.props.brew.style || undefined,
|
style : this.props.brew.style || undefined,
|
||||||
renderer : this.props.brew.renderer || 'legacy'
|
renderer : this.props.brew.renderer || 'legacy',
|
||||||
|
theme : this.props.brew.theme || '5ePHB'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -48,7 +49,7 @@ const PrintPage = createClass({
|
|||||||
text : brewStorage,
|
text : brewStorage,
|
||||||
style : styleStorage,
|
style : styleStorage,
|
||||||
renderer : metaStorage?.renderer || 'legacy',
|
renderer : metaStorage?.renderer || 'legacy',
|
||||||
theme : metaStorage?.theme || '5ePHB'
|
theme : metaStorage?.theme || '5ePHB'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -59,7 +60,8 @@ const PrintPage = createClass({
|
|||||||
|
|
||||||
renderStyle : function() {
|
renderStyle : function() {
|
||||||
if(!this.state.brew.style) return;
|
if(!this.state.brew.style) return;
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${this.state.brew.style} </style>` }} />;
|
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${this.state.brew.style}\n} </style>` }} />;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>\n${this.state.brew.style}\n</style>` }} />;
|
||||||
},
|
},
|
||||||
|
|
||||||
renderPages : function(){
|
renderPages : function(){
|
||||||
|
|||||||
@@ -12,21 +12,13 @@ const Account = require('../../navbar/account.navitem.jsx');
|
|||||||
|
|
||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = createClass({
|
||||||
displayName : 'SharePage',
|
displayName : 'SharePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : {
|
brew : DEFAULT_BREW_LOAD
|
||||||
title : '',
|
|
||||||
text : '',
|
|
||||||
style : '',
|
|
||||||
shareId : null,
|
|
||||||
createdAt : null,
|
|
||||||
updatedAt : null,
|
|
||||||
views : 0,
|
|
||||||
renderer : ''
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
|
|
||||||
const UserPage = createClass({
|
const UserPage = createClass({
|
||||||
displayName : 'UserPage',
|
displayName : 'UserPage',
|
||||||
@@ -19,7 +20,8 @@ const UserPage = createClass({
|
|||||||
return {
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : [],
|
brews : [],
|
||||||
query : ''
|
query : '',
|
||||||
|
error : null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -50,10 +52,19 @@ const UserPage = createClass({
|
|||||||
brewCollection : brewCollection
|
brewCollection : brewCollection
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
errorReported : function(error) {
|
||||||
|
this.setState({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
navItems : function() {
|
navItems : function() {
|
||||||
return <Navbar>
|
return <Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
@@ -63,7 +74,7 @@ const UserPage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
|
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
12
client/homebrew/utils/request-middleware.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
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;
|
||||||
48
client/icons/book-front-cover.svg
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 541.53217 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg22127"
|
||||||
|
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>
|
||||||
|
After Width: | Height: | Size: 4.6 KiB |
39
client/icons/customIcons.less
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
.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');
|
||||||
|
}
|
||||||
|
.fa-file-c {
|
||||||
|
content: url('../icons/fa-file-c.svg');
|
||||||
|
}
|
||||||
|
.book-front-cover {
|
||||||
|
content: url('../icons/book-front-cover.svg');
|
||||||
|
}
|
||||||
30
client/icons/fa-file-c.svg
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 24.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 610.4 816.5" style="enable-background:new 0 0 610.4 816.5;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
.st1{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:20;stroke-miterlimit:10;}
|
||||||
|
</style>
|
||||||
|
<title>fa-file-c</title>
|
||||||
|
<g id="Layer_2_1_">
|
||||||
|
<g id="Layer_1-2">
|
||||||
|
<g id="page">
|
||||||
|
<path id="page-2" d="M610.3,468.3c0,77.3,0.2,154.5,0,231.8s-39.8,116.5-116.8,116.4c-127.6,0-255.1,0-382.7,0
|
||||||
|
c-68.1,0-110.5-41.7-110.6-109.8c-0.2-197.7-0.2-395.5,0-593.2c0-68.4,43.2-110.9,112.1-111c90-0.1,180,0.2,270-0.2
|
||||||
|
c12.8,0,21.5,0.6,32.9,4c17.1,5,152.7,150.7,190.7,188.8c-0.7,18-6,5.7,1.4,35.1c0,6.8,3.1,11.2,3.1,18.1
|
||||||
|
C610.2,320.8,610.3,395.7,610.3,468.3z"/>
|
||||||
|
<path id="white_corner" class="st0" d="M364.1,0v200c0,9.3,1.7,25.6,13.1,36.8c12,11.7,28.8,12.1,37.5,12.2
|
||||||
|
c119.8,1.3,195.6,0.4,195.6,0.4l0,0l-0.3-54.3l-197,1l3-192L364.1,0z"/>
|
||||||
|
</g>
|
||||||
|
<path class="st1" d="M317.7,719.8c-38.3,0-71-8.1-98.3-24.3c-27.2-16.2-48.1-39.2-62.7-69C142.3,596.8,135,561.2,135,520
|
||||||
|
c0-30.9,4.1-58.6,12.4-83.1c8.3-24.5,20.2-45.3,35.9-62.4c15.6-17.1,34.9-30.4,57.7-39.8s48.4-14.1,76.7-14.1
|
||||||
|
c22.1-0.1,44,3.1,65.1,9.7c20.6,6.4,38.4,15.9,53.5,28.4c4.8,3.7,8,7.8,9.7,12.4c1.6,4.2,1.8,8.9,0.6,13.2
|
||||||
|
c-1.2,4.1-3.5,7.7-6.6,10.5c-3.1,2.8-7.2,4.2-11.3,4.1c-4.4,0-9.4-1.8-14.9-5.5c-13-10.5-27.7-18.6-43.6-23.7
|
||||||
|
c-16.6-5.3-33.9-7.9-51.3-7.7c-29.1,0-53.7,6.2-74,18.5s-35.5,30.3-45.8,53.8c-10.3,23.6-15.4,52.1-15.4,85.5s5.1,62.1,15.4,85.9
|
||||||
|
c10.3,23.7,25.6,41.8,45.8,54.1c20.2,12.3,44.9,18.5,74,18.5c17.4,0.1,34.8-2.6,51.3-8c16.2-5.3,31.3-13.5,44.7-24
|
||||||
|
c5.5-3.7,10.5-5.4,14.9-5.3c4,0.1,7.9,1.5,11,4.1c3,2.7,5.2,6.1,6.4,9.9c1.3,4.1,1.3,8.6,0,12.7c-1.3,4.4-4.1,8.3-8.6,11.6
|
||||||
|
c-15.5,13.3-33.6,23.3-54.4,30.1C362.7,716.6,340.3,720,317.7,719.8z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
63
client/icons/mask-corner.svg
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
69
client/icons/mask-edge.svg
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
46
client/icons/position-bottom-left.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
46
client/icons/position-bottom-right.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.1 KiB |
46
client/icons/position-bottom.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
45
client/icons/position-left.svg
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
46
client/icons/position-right.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
45
client/icons/position-top-left.svg
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
46
client/icons/position-top-right.svg
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
45
client/icons/position-top.svg
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
58027
package-lock.json
generated
51
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.3.1",
|
"version": "3.7.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "16.11.x"
|
"node": "16.11.x"
|
||||||
},
|
},
|
||||||
@@ -14,11 +14,14 @@
|
|||||||
"quick": "node scripts/quick.js",
|
"quick": "node scripts/quick.js",
|
||||||
"build": "node scripts/buildHomebrew.js",
|
"build": "node scripts/buildHomebrew.js",
|
||||||
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
"buildall": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||||
|
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||||
"lint": "eslint --fix **/*.{js,jsx}",
|
"lint": "eslint --fix **/*.{js,jsx}",
|
||||||
"lint:dry": "eslint **/*.{js,jsx}",
|
"lint:dry": "eslint **/*.{js,jsx}",
|
||||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||||
|
"test:coverage": "jest --coverage --silent",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
|
||||||
@@ -36,10 +39,24 @@
|
|||||||
"jest": {
|
"jest": {
|
||||||
"testTimeout": 15000,
|
"testTimeout": 15000,
|
||||||
"modulePaths": [
|
"modulePaths": [
|
||||||
"mode_modules",
|
"node_modules",
|
||||||
"shared",
|
"shared",
|
||||||
"server"
|
"server"
|
||||||
]
|
],
|
||||||
|
"coverageThreshold" : {
|
||||||
|
"global" : {
|
||||||
|
"statements" : 25,
|
||||||
|
"branches" : 10,
|
||||||
|
"functions" : 22,
|
||||||
|
"lines" : 25
|
||||||
|
},
|
||||||
|
"server/homebrew.api.js" : {
|
||||||
|
"statements" : 65,
|
||||||
|
"branches" : 50,
|
||||||
|
"functions" : 60,
|
||||||
|
"lines" : 70
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"babel": {
|
"babel": {
|
||||||
"presets": [
|
"presets": [
|
||||||
@@ -51,11 +68,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.19.6",
|
"@babel/core": "^7.21.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
"@babel/plugin-transform-runtime": "^7.21.0",
|
||||||
"@babel/preset-env": "^7.19.4",
|
"@babel/preset-env": "^7.19.4",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"body-parser": "^1.20.1",
|
"@googleapis/drive": "^4.0.2",
|
||||||
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
@@ -64,32 +82,31 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.1.7",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "10.1.0",
|
"fs-extra": "11.1.0",
|
||||||
"googleapis": "109.0.1",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "4.2.2",
|
"marked": "4.2.12",
|
||||||
"marked-extended-tables": "^1.0.5",
|
"marked-extended-tables": "^1.0.5",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mongoose": "^6.7.0",
|
"mongoose": "^6.9.2",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.0",
|
"nconf": "^0.12.0",
|
||||||
"npm": "^8.10.0",
|
"npm": "^8.10.0",
|
||||||
"react": "^16.14.0",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^17.0.2",
|
||||||
"react-frame-component": "4.1.3",
|
"react-frame-component": "4.1.3",
|
||||||
"react-router-dom": "6.4.3",
|
"react-router-dom": "6.8.2",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^6.1.0",
|
"superagent": "^6.1.0",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.35.0",
|
||||||
"eslint-plugin-react": "^7.31.10",
|
"eslint-plugin-react": "^7.32.2",
|
||||||
"jest": "^29.2.2",
|
"jest": "^29.4.3",
|
||||||
"supertest": "^6.3.1"
|
"supertest": "^6.3.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const transforms = {
|
|||||||
|
|
||||||
const build = async ({ bundle, render, ssr })=>{
|
const build = async ({ bundle, render, ssr })=>{
|
||||||
const css = await lessTransform.generate({ paths: './shared' });
|
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.css', css);
|
||||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||||
@@ -72,6 +73,7 @@ fs.emptyDirSync('./build');
|
|||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.V3[dir] = (themeData);
|
themes.V3[dir] = (themeData);
|
||||||
fs.copy(`./themes/V3/${dir}/dropdownTexture.png`, `./build/themes/V3/${dir}/dropdownTexture.png`);
|
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`;
|
const src = `./themes/V3/${dir}/style.less`;
|
||||||
((outputDirectory)=>{
|
((outputDirectory)=>{
|
||||||
less.render(fs.readFileSync(src).toString(), {
|
less.render(fs.readFileSync(src).toString(), {
|
||||||
@@ -95,6 +97,7 @@ fs.emptyDirSync('./build');
|
|||||||
// Move assets
|
// Move assets
|
||||||
await fs.copy('./themes/fonts', './build/fonts');
|
await fs.copy('./themes/fonts', './build/fonts');
|
||||||
await fs.copy('./themes/assets', './build/assets');
|
await fs.copy('./themes/assets', './build/assets');
|
||||||
|
await fs.copy('./client/icons', './build/icons');
|
||||||
|
|
||||||
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
//v==----------------------------- BUNDLE PACKAGES --------------------------------==v//
|
||||||
|
|
||||||
@@ -136,6 +139,6 @@ fs.emptyDirSync('./build');
|
|||||||
if(isDev){
|
if(isDev){
|
||||||
livereload('./build');
|
livereload('./build');
|
||||||
watchFile('./server.js', {
|
watchFile('./server.js', {
|
||||||
watch : ['./client', './server'] // Watch additional folders if you want
|
watch : ['./client', './server', './themes'] // Watch additional folders if you want
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
|||||||
const sanitizeFilename = require('sanitize-filename');
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||||
|
|
||||||
const splitTextStyleAndMetadata = (brew)=>{
|
const splitTextStyleAndMetadata = (brew)=>{
|
||||||
brew.text = brew.text.replaceAll('\r\n', '\n');
|
brew.text = brew.text.replaceAll('\r\n', '\n');
|
||||||
if(brew.text.startsWith('```metadata')) {
|
if(brew.text.startsWith('```metadata')) {
|
||||||
@@ -29,7 +31,6 @@ const splitTextStyleAndMetadata = (brew)=>{
|
|||||||
brew.style = brew.text.slice(7, index - 1);
|
brew.style = brew.text.slice(7, index - 1);
|
||||||
brew.text = brew.text.slice(index + 5);
|
brew.text = brew.text.slice(index + 5);
|
||||||
}
|
}
|
||||||
_.defaults(brew, { 'renderer': 'legacy', 'theme': '5ePHB' });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
@@ -78,10 +79,10 @@ const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
|||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
const defaultMetaTags = {
|
const defaultMetaTags = {
|
||||||
siteName : 'The Homebrewery - Make your Homebrew content look legit!',
|
site_name : 'The Homebrewery - Make your Homebrew content look legit!',
|
||||||
title : 'The Homebrewery',
|
title : 'The Homebrewery',
|
||||||
description : 'A NaturalCrit Tool for Homebrews',
|
description : 'A NaturalCrit Tool for creating authentic Homebrews using Markdown.',
|
||||||
thumbnail : `${config.get('publicUrl')}/thumbnail.png`,
|
image : `${config.get('publicUrl')}/thumbnail.png`,
|
||||||
type : 'website'
|
type : 'website'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,8 +149,7 @@ app.get('/changelog', async (req, res, next)=>{
|
|||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : 'Changelog',
|
title : 'Changelog',
|
||||||
description : 'Development changelog.',
|
description : 'Development changelog.'
|
||||||
thumbnail : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
@@ -192,12 +192,19 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
sanitizeBrew(brew, 'share');
|
sanitizeBrew(brew, 'share');
|
||||||
const prefix = 'HB - ';
|
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(' ', '');
|
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
|
||||||
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
|
||||||
res.set({
|
res.set({
|
||||||
'Cache-Control' : 'no-cache',
|
'Cache-Control' : 'no-cache',
|
||||||
'Content-Type' : 'text/plain',
|
'Content-Type' : 'text/plain',
|
||||||
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
|
'Content-Disposition' : `attachment; filename*=UTF-8''${encodeRFC3986ValueChars(fileName)}.txt`
|
||||||
});
|
});
|
||||||
res.status(200).send(brew.text);
|
res.status(200).send(brew.text);
|
||||||
});
|
});
|
||||||
@@ -208,8 +215,7 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : `${req.params.username}'s Collection`,
|
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'?
|
// type : could be 'profile'?
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,7 +281,7 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
|||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || null,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,12 +295,19 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
|||||||
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||||
sanitizeBrew(req.brew, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
req.brew.title = `CLONE - ${req.brew.title}`;
|
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
|
||||||
|
};
|
||||||
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : 'New',
|
title : 'New',
|
||||||
description : 'Start crafting your homebrew on the Homebrewery!',
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||||
image : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
@@ -307,7 +320,7 @@ app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, r
|
|||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
image : req.brew.thumbnail || null,
|
image : req.brew.thumbnail || defaultMetaTags.image,
|
||||||
type : 'article'
|
type : 'article'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -337,11 +350,11 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
let auth;
|
let auth;
|
||||||
let files;
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
try {
|
try {
|
||||||
auth = await GoogleActions.authCheck(req.account, res);
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
auth = undefined;
|
auth = undefined;
|
||||||
console.log('Google auth check failed!');
|
console.log('Google auth check failed!');
|
||||||
@@ -349,9 +362,9 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
if(auth.credentials.access_token) {
|
if(auth.credentials.access_token) {
|
||||||
try {
|
try {
|
||||||
files = await GoogleActions.listGoogleBrews(auth);
|
googleCount = await GoogleActions.listGoogleBrews(auth);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
files = undefined;
|
googleCount = undefined;
|
||||||
console.log('List Google files failed!');
|
console.log('List Google files failed!');
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
@@ -359,18 +372,19 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
const brews = await HomebrewModel.find(query, 'id')
|
const mongoCount = await HomebrewModel.countDocuments(query)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
|
mongoCount = 0;
|
||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
data.uiItems = {
|
data.uiItems = {
|
||||||
username : req.account.username,
|
username : req.account.username,
|
||||||
issued : req.account.issued,
|
issued : req.account.issued,
|
||||||
mongoCount : brews.length,
|
googleId : Boolean(req.account.googleId),
|
||||||
googleId : Boolean(req.account.googleId),
|
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||||
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
mongoCount : mongoCount,
|
||||||
fileCount : files?.length || '-'
|
googleCount : googleCount?.length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,8 +392,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : `Account Page`,
|
title : `Account Page`,
|
||||||
description : null,
|
description : null
|
||||||
image : null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
@@ -403,6 +416,7 @@ if(isLocalEnvironment){
|
|||||||
//Render the page
|
//Render the page
|
||||||
const templateFn = require('./../client/template.js');
|
const templateFn = require('./../client/template.js');
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
|
|
||||||
// Create configuration object
|
// Create configuration object
|
||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
|
|||||||
37
server/brewDefaults.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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 : [],
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -1,35 +1,39 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const { google } = require('googleapis');
|
const googleDrive = require('@googleapis/drive');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
const token = require('./token.js');
|
const token = require('./token.js');
|
||||||
const config = require('./config.js');
|
const config = require('./config.js');
|
||||||
|
|
||||||
const keys = typeof(config.get('service_account')) == 'string' ?
|
|
||||||
JSON.parse(config.get('service_account')) :
|
|
||||||
config.get('service_account');
|
|
||||||
let serviceAuth;
|
let serviceAuth;
|
||||||
try {
|
if(!config.get('service_account')){
|
||||||
serviceAuth = google.auth.fromJSON(keys);
|
console.log('No Google Service Account in config files - Google Drive integration will not be available.');
|
||||||
serviceAuth.scopes = [
|
} else {
|
||||||
'https://www.googleapis.com/auth/drive'
|
const keys = typeof(config.get('service_account')) == 'string' ?
|
||||||
];
|
JSON.parse(config.get('service_account')) :
|
||||||
} catch (err) {
|
config.get('service_account');
|
||||||
console.warn(err);
|
|
||||||
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
|
try {
|
||||||
|
serviceAuth = googleDrive.auth.fromJSON(keys);
|
||||||
|
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(err);
|
||||||
|
console.log('Please make sure the Google Service Account is set up properly in your config files.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
google.options({ auth: serviceAuth || config.get('google_api_key') });
|
|
||||||
|
const defaultAuth = serviceAuth || config.get('google_api_key');
|
||||||
|
|
||||||
const GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
authCheck : (account, res)=>{
|
authCheck : (account, res, updateTokens=true)=>{
|
||||||
if(!account || !account.googleId){ // If not signed into Google
|
if(!account || !account.googleId){ // If not signed into Google
|
||||||
const err = new Error('Not Signed In');
|
const err = new Error('Not Signed In');
|
||||||
err.status = 401;
|
err.status = 401;
|
||||||
throw (err);
|
throw (err);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oAuth2Client = new google.auth.OAuth2(
|
const oAuth2Client = new googleDrive.auth.OAuth2(
|
||||||
config.get('google_client_id'),
|
config.get('google_client_id'),
|
||||||
config.get('google_client_secret'),
|
config.get('google_client_secret'),
|
||||||
'/auth/google/redirect'
|
'/auth/google/redirect'
|
||||||
@@ -40,7 +44,7 @@ const GoogleActions = {
|
|||||||
refresh_token : account.googleRefreshToken
|
refresh_token : account.googleRefreshToken
|
||||||
});
|
});
|
||||||
|
|
||||||
oAuth2Client.on('tokens', (tokens)=>{
|
updateTokens && oAuth2Client.on('tokens', (tokens)=>{
|
||||||
if(tokens.refresh_token) {
|
if(tokens.refresh_token) {
|
||||||
account.googleRefreshToken = tokens.refresh_token;
|
account.googleRefreshToken = tokens.refresh_token;
|
||||||
}
|
}
|
||||||
@@ -56,7 +60,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getGoogleFolder : async (auth)=>{
|
getGoogleFolder : async (auth)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
fileMetadata = {
|
fileMetadata = {
|
||||||
'name' : 'Homebrewery',
|
'name' : 'Homebrewery',
|
||||||
@@ -93,7 +97,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
listGoogleBrews : async (auth)=>{
|
listGoogleBrews : async (auth)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const obj = await drive.files.list({
|
const obj = await drive.files.list({
|
||||||
pageSize : 1000,
|
pageSize : 1000,
|
||||||
@@ -134,7 +138,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateGoogleBrew : async (brew)=>{
|
updateGoogleBrew : async (brew)=>{
|
||||||
const drive = google.drive({ version: 'v3' });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
fileId : brew.googleId,
|
fileId : brew.googleId,
|
||||||
@@ -166,7 +170,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
newGoogleBrew : async (auth, brew)=>{
|
newGoogleBrew : async (auth, brew)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const media = {
|
const media = {
|
||||||
mimeType : 'text/plain',
|
mimeType : 'text/plain',
|
||||||
@@ -218,7 +222,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getGoogleBrew : async (id, accessId, accessType)=>{
|
getGoogleBrew : async (id, accessId, accessType)=>{
|
||||||
const drive = google.drive({ version: 'v3' });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -253,7 +257,6 @@ const GoogleActions = {
|
|||||||
text : file.data,
|
text : file.data,
|
||||||
|
|
||||||
description : obj.data.description,
|
description : obj.data.description,
|
||||||
tags : obj.data.properties.tags ? obj.data.properties.tags : '',
|
|
||||||
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
systems : obj.data.properties.systems ? obj.data.properties.systems.split(',') : [],
|
||||||
authors : [],
|
authors : [],
|
||||||
lang : obj.data.properties.lang,
|
lang : obj.data.properties.lang,
|
||||||
@@ -276,7 +279,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteGoogleBrew : async (auth, id, accessId)=>{
|
deleteGoogleBrew : async (auth, id, accessId)=>{
|
||||||
const drive = google.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const obj = await drive.files.get({
|
const obj = await drive.files.get({
|
||||||
fileId : id,
|
fileId : id,
|
||||||
@@ -302,7 +305,7 @@ const GoogleActions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
increaseView : async (id, accessId, accessType, brew)=>{
|
increaseView : async (id, accessId, accessType, brew)=>{
|
||||||
const drive = google.drive({ version: 'v3' });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
fileId : brew.googleId,
|
fileId : brew.googleId,
|
||||||
|
|||||||
@@ -9,330 +9,339 @@ const yaml = require('js-yaml');
|
|||||||
const asyncHandler = require('express-async-handler');
|
const asyncHandler = require('express-async-handler');
|
||||||
const { nanoid } = require('nanoid');
|
const { nanoid } = require('nanoid');
|
||||||
|
|
||||||
|
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||||
|
|
||||||
// const getTopBrews = (cb) => {
|
// const getTopBrews = (cb) => {
|
||||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||||
// cb(brews);
|
// cb(brews);
|
||||||
// });
|
// });
|
||||||
// };
|
// };
|
||||||
|
|
||||||
const getId = (req)=>{
|
|
||||||
// Set the id and initial potential google id, where the google id is present on the existing brew.
|
|
||||||
let id = req.params.id, googleId = req.body?.googleId;
|
|
||||||
|
|
||||||
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
|
||||||
if(id.length > 12) {
|
|
||||||
googleId = id.slice(0, -12);
|
|
||||||
id = id.slice(-12);
|
|
||||||
}
|
|
||||||
return { id, googleId };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBrew = (accessType)=>{
|
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
|
||||||
return async (req, res, next)=>{
|
|
||||||
// Get relevant IDs for the brew
|
|
||||||
const { id, googleId } = getId(req);
|
|
||||||
|
|
||||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
|
||||||
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
|
||||||
.catch((err)=>{
|
|
||||||
if(googleId) {
|
|
||||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
|
||||||
} else {
|
|
||||||
console.warn(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stub = stub?.toObject();
|
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
|
||||||
if(googleId || stub?.googleId) {
|
|
||||||
let googleError;
|
|
||||||
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.warn(err);
|
|
||||||
googleError = err;
|
|
||||||
});
|
|
||||||
// If we can't find the google brew and there is a google id for the brew, throw an error.
|
|
||||||
if(!googleBrew) throw googleError;
|
|
||||||
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
|
||||||
stub = stub ? _.assign({ ...excludeStubProps(stub), stubbed: true }, excludeGoogleProps(googleBrew)) : googleBrew;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If after all of that we still don't have a brew, throw an exception
|
|
||||||
if(!stub) {
|
|
||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
|
||||||
}
|
|
||||||
|
|
||||||
if(typeof stub.tags === 'string') {
|
|
||||||
stub.tags = [];
|
|
||||||
}
|
|
||||||
req.brew = stub;
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mergeBrewText = (brew)=>{
|
|
||||||
let text = brew.text;
|
|
||||||
if(brew.style !== undefined) {
|
|
||||||
text = `\`\`\`css\n` +
|
|
||||||
`${brew.style || ''}\n` +
|
|
||||||
`\`\`\`\n\n` +
|
|
||||||
`${text}`;
|
|
||||||
}
|
|
||||||
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme', 'lang']);
|
|
||||||
text = `\`\`\`metadata\n` +
|
|
||||||
`${yaml.dump(metadata)}\n` +
|
|
||||||
`\`\`\`\n\n` +
|
|
||||||
`${text}`;
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAX_TITLE_LENGTH = 100;
|
const MAX_TITLE_LENGTH = 100;
|
||||||
|
|
||||||
const getGoodBrewTitle = (text)=>{
|
const api = {
|
||||||
const tokens = Markdown.marked.lexer(text);
|
homebrewApi : router,
|
||||||
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
getId : (req)=>{
|
||||||
.slice(0, MAX_TITLE_LENGTH);
|
// Set the id and initial potential google id, where the google id is present on the existing brew.
|
||||||
};
|
let id = req.params.id, googleId = req.body?.googleId;
|
||||||
|
|
||||||
const excludePropsFromUpdate = (brew)=>{
|
// If the id is longer than 12, then it's a google id + the edit id. This splits the longer id up.
|
||||||
// Remove undesired properties
|
if(id.length > 12) {
|
||||||
const modified = _.clone(brew);
|
googleId = id.slice(0, -12);
|
||||||
const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId'];
|
id = id.slice(-12);
|
||||||
for (const prop of propsToExclude) {
|
|
||||||
delete modified[prop];
|
|
||||||
}
|
|
||||||
return modified;
|
|
||||||
};
|
|
||||||
|
|
||||||
const excludeGoogleProps = (brew)=>{
|
|
||||||
const modified = _.clone(brew);
|
|
||||||
const propsToExclude = ['tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail', 'lang'];
|
|
||||||
for (const prop of propsToExclude) {
|
|
||||||
delete modified[prop];
|
|
||||||
}
|
|
||||||
return modified;
|
|
||||||
};
|
|
||||||
|
|
||||||
const excludeStubProps = (brew)=>{
|
|
||||||
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount', 'version'];
|
|
||||||
for (const prop of propsToExclude) {
|
|
||||||
brew[prop] = undefined;
|
|
||||||
}
|
|
||||||
return brew;
|
|
||||||
};
|
|
||||||
|
|
||||||
const beforeNewSave = (account, brew)=>{
|
|
||||||
if(!brew.title) {
|
|
||||||
brew.title = getGoodBrewTitle(brew.text);
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.authors = (account) ? [account.username] : [];
|
|
||||||
brew.text = mergeBrewText(brew);
|
|
||||||
};
|
|
||||||
|
|
||||||
const newGoogleBrew = async (account, brew, res)=>{
|
|
||||||
const oAuth2Client = GoogleActions.authCheck(account, res);
|
|
||||||
|
|
||||||
const newBrew = excludeGoogleProps(brew);
|
|
||||||
|
|
||||||
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
|
||||||
};
|
|
||||||
|
|
||||||
const newBrew = async (req, res)=>{
|
|
||||||
const brew = req.body;
|
|
||||||
const { saveToGoogle } = req.query;
|
|
||||||
|
|
||||||
delete brew.editId;
|
|
||||||
delete brew.shareId;
|
|
||||||
delete brew.googleId;
|
|
||||||
|
|
||||||
beforeNewSave(req.account, brew);
|
|
||||||
|
|
||||||
const newHomebrew = new HomebrewModel(brew);
|
|
||||||
newHomebrew.editId = nanoid(12);
|
|
||||||
newHomebrew.shareId = nanoid(12);
|
|
||||||
|
|
||||||
let googleId, saved;
|
|
||||||
if(saveToGoogle) {
|
|
||||||
googleId = await newGoogleBrew(req.account, newHomebrew, res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
|
||||||
});
|
|
||||||
if(!googleId) return;
|
|
||||||
excludeStubProps(newHomebrew);
|
|
||||||
newHomebrew.googleId = googleId;
|
|
||||||
} else {
|
|
||||||
// Compress brew text to binary before saving
|
|
||||||
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
|
||||||
newHomebrew.text = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
saved = await newHomebrew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err, err.toString(), err.stack);
|
|
||||||
throw `Error while creating new brew, ${err.toString()}`;
|
|
||||||
});
|
|
||||||
if(!saved) return;
|
|
||||||
saved = saved.toObject();
|
|
||||||
|
|
||||||
res.status(200).send(saved);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateBrew = async (req, res)=>{
|
|
||||||
// Initialize brew from request and body, destructure query params, set a constant for the google id, and set the initial value for the after-save method
|
|
||||||
let brew = _.assign(req.brew, excludePropsFromUpdate(req.body));
|
|
||||||
const { saveToGoogle, removeFromGoogle } = req.query;
|
|
||||||
const googleId = brew.googleId;
|
|
||||||
let afterSave = async ()=>true;
|
|
||||||
|
|
||||||
brew.text = mergeBrewText(brew);
|
|
||||||
|
|
||||||
if(brew.googleId && removeFromGoogle) {
|
|
||||||
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
|
||||||
afterSave = async ()=>{
|
|
||||||
return await deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
brew.googleId = undefined;
|
|
||||||
} else if(!brew.googleId && saveToGoogle) {
|
|
||||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
|
||||||
brew.googleId = await newGoogleBrew(req.account, excludeGoogleProps(brew), res)
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || err.response.status).send(err.message || err);
|
|
||||||
});
|
|
||||||
if(!brew.googleId) return;
|
|
||||||
} else if(brew.googleId) {
|
|
||||||
// If the google id exists and no other actions are being performed, update the google brew
|
|
||||||
const updated = await GoogleActions.updateGoogleBrew(excludeGoogleProps(brew))
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err?.response?.status || 500).send(err);
|
|
||||||
});
|
|
||||||
if(!updated) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(brew.googleId) {
|
|
||||||
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
|
||||||
excludeStubProps(brew);
|
|
||||||
} else {
|
|
||||||
// Compress brew text to binary before saving
|
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
|
||||||
// Delete the non-binary text field since it's not needed anymore
|
|
||||||
brew.text = undefined;
|
|
||||||
}
|
|
||||||
brew.updatedAt = new Date();
|
|
||||||
|
|
||||||
if(req.account) {
|
|
||||||
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the brew from the database again (if it existed there to begin with), and assign the existing brew to it
|
|
||||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
|
||||||
|
|
||||||
if(!brew.markModified) {
|
|
||||||
// If it wasn't in the database, create a new db brew
|
|
||||||
brew = new HomebrewModel(brew);
|
|
||||||
}
|
|
||||||
|
|
||||||
brew.markModified('authors');
|
|
||||||
brew.markModified('systems');
|
|
||||||
|
|
||||||
// Save the database brew
|
|
||||||
const saved = await brew.save()
|
|
||||||
.catch((err)=>{
|
|
||||||
console.error(err);
|
|
||||||
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
|
||||||
});
|
|
||||||
if(!saved) return;
|
|
||||||
// Call and wait for afterSave to complete
|
|
||||||
const after = await afterSave();
|
|
||||||
if(!after) return;
|
|
||||||
|
|
||||||
res.status(200).send(saved);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteGoogleBrew = async (account, id, editId, res)=>{
|
|
||||||
const auth = await GoogleActions.authCheck(account, res);
|
|
||||||
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteBrew = async (req, res, next)=>{
|
|
||||||
// Delete an orphaned stub if its Google brew doesn't exist
|
|
||||||
try {
|
|
||||||
await getBrew('edit')(req, res, ()=>{});
|
|
||||||
} catch (err) {
|
|
||||||
const { id, googleId } = getId(req);
|
|
||||||
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
|
||||||
await HomebrewModel.deleteOne({ editId: id });
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
let brew = req.brew;
|
|
||||||
const { googleId, editId } = brew;
|
|
||||||
const account = req.account;
|
|
||||||
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
|
|
||||||
// If the user is the owner and the file is saved to google, mark the google brew for deletion
|
|
||||||
const shouldDeleteGoogleBrew = googleId && isOwner;
|
|
||||||
|
|
||||||
if(brew._id) {
|
|
||||||
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
|
||||||
if(account) {
|
|
||||||
// Remove current user as author
|
|
||||||
brew.authors = _.pull(brew.authors, account.username);
|
|
||||||
brew.markModified('authors');
|
|
||||||
}
|
}
|
||||||
|
return { id, googleId };
|
||||||
|
},
|
||||||
|
getBrew : (accessType, stubOnly = false)=>{
|
||||||
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
|
return async (req, res, next)=>{
|
||||||
|
// Get relevant IDs for the brew
|
||||||
|
const { id, googleId } = api.getId(req);
|
||||||
|
|
||||||
if(brew.authors.length === 0) {
|
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||||
// Delete brew if there are no authors left
|
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
||||||
await brew.remove()
|
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
if(googleId) {
|
||||||
throw { status: 500, message: 'Error while removing' };
|
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||||
|
} else {
|
||||||
|
console.warn(err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
stub = stub?.toObject();
|
||||||
if(shouldDeleteGoogleBrew) {
|
|
||||||
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
// If there is a google id, try to find the google brew
|
||||||
brew.googleId = undefined;
|
if(!stubOnly && (googleId || stub?.googleId)) {
|
||||||
brew.textBin = zlib.deflateRawSync(brew.text);
|
let googleError;
|
||||||
brew.text = undefined;
|
const googleBrew = await GoogleActions.getGoogleBrew(googleId || stub?.googleId, id, accessType)
|
||||||
|
.catch((err)=>{
|
||||||
|
googleError = err;
|
||||||
|
});
|
||||||
|
// Throw any error caught while attempting to retrieve Google brew.
|
||||||
|
if(googleError) throw googleError;
|
||||||
|
// Combine the Homebrewery stub with the google brew, or if the stub doesn't exist just use the google brew
|
||||||
|
stub = stub ? _.assign({ ...api.excludeStubProps(stub), stubbed: true }, api.excludeGoogleProps(googleBrew)) : googleBrew;
|
||||||
|
}
|
||||||
|
const authorsExist = stub?.authors?.length > 0;
|
||||||
|
const isAuthor = stub?.authors?.includes(req.account?.username);
|
||||||
|
const isInvited = stub?.invitedAuthors?.includes(req.account?.username);
|
||||||
|
if(accessType === 'edit' && (authorsExist && !(isAuthor || isInvited))) {
|
||||||
|
throw `The current logged in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, save the brew with updated author list
|
// If after all of that we still don't have a brew, throw an exception
|
||||||
await brew.save()
|
if(!stub && !stubOnly) {
|
||||||
.catch((err)=>{
|
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||||
throw { status: 500, message: err };
|
}
|
||||||
});
|
|
||||||
|
// Clean up brew: fill in missing fields with defaults / fix old invalid values
|
||||||
|
if(stub) {
|
||||||
|
stub.tags = stub.tags || undefined; // Clear empty strings
|
||||||
|
stub.renderer = stub.renderer || undefined; // Clear empty strings
|
||||||
|
stub = _.defaults(stub, DEFAULT_BREW_LOAD); // Fill in blank fields
|
||||||
|
}
|
||||||
|
|
||||||
|
req.brew = stub ?? {};
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mergeBrewText : (brew)=>{
|
||||||
|
let text = brew.text;
|
||||||
|
if(brew.style !== undefined) {
|
||||||
|
text = `\`\`\`css\n` +
|
||||||
|
`${brew.style || ''}\n` +
|
||||||
|
`\`\`\`\n\n` +
|
||||||
|
`${text}`;
|
||||||
}
|
}
|
||||||
}
|
const metadata = _.pick(brew, ['title', 'description', 'tags', 'systems', 'renderer', 'theme']);
|
||||||
if(shouldDeleteGoogleBrew) {
|
text = `\`\`\`metadata\n` +
|
||||||
const deleted = await deleteGoogleBrew(account, googleId, editId, res)
|
`${yaml.dump(metadata)}\n` +
|
||||||
|
`\`\`\`\n\n` +
|
||||||
|
`${text}`;
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
getGoodBrewTitle : (text)=>{
|
||||||
|
const tokens = Markdown.marked.lexer(text);
|
||||||
|
return (tokens.find((token)=>token.type === 'heading' || token.type === 'paragraph')?.text || 'No Title')
|
||||||
|
.slice(0, MAX_TITLE_LENGTH);
|
||||||
|
},
|
||||||
|
excludePropsFromUpdate : (brew)=>{
|
||||||
|
// Remove undesired properties
|
||||||
|
const modified = _.clone(brew);
|
||||||
|
const propsToExclude = ['_id', 'views', 'lastViewed'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
delete modified[prop];
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
},
|
||||||
|
excludeGoogleProps : (brew)=>{
|
||||||
|
const modified = _.clone(brew);
|
||||||
|
const propsToExclude = ['version', 'tags', 'systems', 'published', 'authors', 'owner', 'views', 'thumbnail'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
delete modified[prop];
|
||||||
|
}
|
||||||
|
return modified;
|
||||||
|
},
|
||||||
|
excludeStubProps : (brew)=>{
|
||||||
|
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
||||||
|
for (const prop of propsToExclude) {
|
||||||
|
brew[prop] = undefined;
|
||||||
|
}
|
||||||
|
return brew;
|
||||||
|
},
|
||||||
|
beforeNewSave : (account, brew)=>{
|
||||||
|
if(!brew.title) {
|
||||||
|
brew.title = api.getGoodBrewTitle(brew.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
brew.authors = (account) ? [account.username] : [];
|
||||||
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
|
_.defaults(brew, DEFAULT_BREW);
|
||||||
|
},
|
||||||
|
newGoogleBrew : async (account, brew, res)=>{
|
||||||
|
const oAuth2Client = GoogleActions.authCheck(account, res);
|
||||||
|
|
||||||
|
const newBrew = api.excludeGoogleProps(brew);
|
||||||
|
|
||||||
|
return await GoogleActions.newGoogleBrew(oAuth2Client, newBrew);
|
||||||
|
},
|
||||||
|
newBrew : async (req, res)=>{
|
||||||
|
const brew = req.body;
|
||||||
|
const { saveToGoogle } = req.query;
|
||||||
|
|
||||||
|
delete brew.editId;
|
||||||
|
delete brew.shareId;
|
||||||
|
delete brew.googleId;
|
||||||
|
|
||||||
|
api.beforeNewSave(req.account, brew);
|
||||||
|
|
||||||
|
const newHomebrew = new HomebrewModel(brew);
|
||||||
|
newHomebrew.editId = nanoid(12);
|
||||||
|
newHomebrew.shareId = nanoid(12);
|
||||||
|
|
||||||
|
let googleId, saved;
|
||||||
|
if(saveToGoogle) {
|
||||||
|
googleId = await api.newGoogleBrew(req.account, newHomebrew, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
||||||
|
});
|
||||||
|
if(!googleId) return;
|
||||||
|
api.excludeStubProps(newHomebrew);
|
||||||
|
newHomebrew.googleId = googleId;
|
||||||
|
} else {
|
||||||
|
// Compress brew text to binary before saving
|
||||||
|
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
|
||||||
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
|
newHomebrew.text = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
saved = await newHomebrew.save()
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.error(err);
|
console.error(err, err.toString(), err.stack);
|
||||||
res.status(500).send(err);
|
throw `Error while creating new brew, ${err.toString()}`;
|
||||||
});
|
});
|
||||||
if(!deleted) return;
|
if(!saved) return;
|
||||||
|
saved = saved.toObject();
|
||||||
|
|
||||||
|
res.status(200).send(saved);
|
||||||
|
},
|
||||||
|
updateBrew : async (req, res)=>{
|
||||||
|
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||||
|
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
|
const brewFromServer = req.brew;
|
||||||
|
if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
|
||||||
|
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
return res.status(409).send(JSON.stringify({ message: `The brew has been changed on a different device. Please save your changes elsewhere, refresh, and try again.` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let brew = _.assign(brewFromServer, brewFromClient);
|
||||||
|
const googleId = brew.googleId;
|
||||||
|
const { saveToGoogle, removeFromGoogle } = req.query;
|
||||||
|
let afterSave = async ()=>true;
|
||||||
|
|
||||||
|
brew.text = api.mergeBrewText(brew);
|
||||||
|
|
||||||
|
if(brew.googleId && removeFromGoogle) {
|
||||||
|
// If the google id exists and we're removing it from google, set afterSave to delete the google brew and mark the brew's google id as undefined
|
||||||
|
afterSave = async ()=>{
|
||||||
|
return await api.deleteGoogleBrew(req.account, googleId, brew.editId, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.status || err?.response?.status || 500).send(err.message || err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
brew.googleId = undefined;
|
||||||
|
} else if(!brew.googleId && saveToGoogle) {
|
||||||
|
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||||
|
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
|
if(!brew.googleId) return;
|
||||||
|
} else if(brew.googleId) {
|
||||||
|
// If the google id exists and no other actions are being performed, update the google brew
|
||||||
|
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew))
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.response?.status || 500).send(err);
|
||||||
|
});
|
||||||
|
if(!updated) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(brew.googleId) {
|
||||||
|
// If the google id exists after all those actions, exclude the props that are stored in google and aren't needed for rendering the brew items
|
||||||
|
api.excludeStubProps(brew);
|
||||||
|
} else {
|
||||||
|
// Compress brew text to binary before saving
|
||||||
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
|
// Delete the non-binary text field since it's not needed anymore
|
||||||
|
brew.text = undefined;
|
||||||
|
}
|
||||||
|
brew.updatedAt = new Date();
|
||||||
|
brew.version = (brew.version || 1) + 1;
|
||||||
|
|
||||||
|
if(req.account) {
|
||||||
|
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
|
||||||
|
brew.invitedAuthors = _.uniq(_.filter(brew.invitedAuthors, (a)=>req.account.username !== a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// define a function to catch our save errors
|
||||||
|
const saveError = (err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || 500).send(err.message || 'Unable to save brew to Homebrewery database');
|
||||||
|
};
|
||||||
|
let saved;
|
||||||
|
if(!brew._id) {
|
||||||
|
// if the brew does not have a stub id, create and save it, then write the new value back to the brew.
|
||||||
|
saved = await new HomebrewModel(brew).save().catch(saveError);
|
||||||
|
} else {
|
||||||
|
// if the brew does have a stub id, update it using the stub id as the key.
|
||||||
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
saved = await brew.save()
|
||||||
|
.catch(saveError);
|
||||||
|
}
|
||||||
|
if(!saved) return;
|
||||||
|
// Call and wait for afterSave to complete
|
||||||
|
const after = await afterSave();
|
||||||
|
if(!after) return;
|
||||||
|
|
||||||
|
res.status(200).send(saved);
|
||||||
|
},
|
||||||
|
deleteGoogleBrew : async (account, id, editId, res)=>{
|
||||||
|
const auth = await GoogleActions.authCheck(account, res);
|
||||||
|
await GoogleActions.deleteGoogleBrew(auth, id, editId);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
deleteBrew : async (req, res, next)=>{
|
||||||
|
// Delete an orphaned stub if its Google brew doesn't exist
|
||||||
|
try {
|
||||||
|
await api.getBrew('edit')(req, res, ()=>{});
|
||||||
|
} catch (err) {
|
||||||
|
const { id, googleId } = api.getId(req);
|
||||||
|
console.warn(`No google brew found for id ${googleId}, the stub with id ${id} will be deleted.`);
|
||||||
|
await HomebrewModel.deleteOne({ editId: id });
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
let brew = req.brew;
|
||||||
|
const { googleId, editId } = brew;
|
||||||
|
const account = req.account;
|
||||||
|
const isOwner = account && (brew.authors.length === 0 || brew.authors[0] === account.username);
|
||||||
|
// If the user is the owner and the file is saved to google, mark the google brew for deletion
|
||||||
|
const shouldDeleteGoogleBrew = googleId && isOwner;
|
||||||
|
|
||||||
|
if(brew._id) {
|
||||||
|
brew = _.assign(await HomebrewModel.findOne({ _id: brew._id }), brew);
|
||||||
|
if(account) {
|
||||||
|
// Remove current user as author
|
||||||
|
brew.authors = _.pull(brew.authors, account.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(brew.authors.length === 0) {
|
||||||
|
// Delete brew if there are no authors left
|
||||||
|
await brew.remove()
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
throw { status: 500, message: 'Error while removing' };
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if(shouldDeleteGoogleBrew) {
|
||||||
|
// When there are still authors remaining, we delete the google brew but store the full brew in the Homebrewery database
|
||||||
|
brew.googleId = undefined;
|
||||||
|
brew.textBin = zlib.deflateRawSync(brew.text);
|
||||||
|
brew.text = undefined;
|
||||||
|
}
|
||||||
|
brew.markModified('authors'); //Mongo will not properly update arrays without markModified()
|
||||||
|
await brew.save()
|
||||||
|
.catch((err)=>{
|
||||||
|
throw { status: 500, message: err };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(shouldDeleteGoogleBrew) {
|
||||||
|
const deleted = await api.deleteGoogleBrew(account, googleId, editId, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send(err);
|
||||||
|
});
|
||||||
|
if(!deleted) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(204).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(204).send();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
router.post('/api', asyncHandler(newBrew));
|
router.use('/api', require('./middleware/check-client-version.js'));
|
||||||
router.put('/api/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.post('/api', asyncHandler(api.newBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(getBrew('edit')), asyncHandler(updateBrew));
|
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(deleteBrew));
|
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(deleteBrew));
|
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||||
|
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||||
|
|
||||||
module.exports = {
|
module.exports = api;
|
||||||
homebrewApi : router,
|
|
||||||
getBrew
|
|
||||||
};
|
|
||||||
|
|||||||
758
server/homebrew.api.spec.js
Normal file
@@ -0,0 +1,758 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
|
describe('Tests for api', ()=>{
|
||||||
|
let api;
|
||||||
|
let google;
|
||||||
|
let model;
|
||||||
|
let hbBrew;
|
||||||
|
let googleBrew;
|
||||||
|
let res;
|
||||||
|
|
||||||
|
let modelBrew;
|
||||||
|
let saveFunc;
|
||||||
|
let removeFunc;
|
||||||
|
let markModifiedFunc;
|
||||||
|
let saved;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
saved = undefined;
|
||||||
|
saveFunc = jest.fn(async function() {
|
||||||
|
saved = { ...this, _id: '1' };
|
||||||
|
return saved;
|
||||||
|
});
|
||||||
|
removeFunc = jest.fn(async function() {});
|
||||||
|
markModifiedFunc = jest.fn(()=>true);
|
||||||
|
|
||||||
|
modelBrew = (brew)=>({
|
||||||
|
...brew,
|
||||||
|
save : saveFunc,
|
||||||
|
remove : removeFunc,
|
||||||
|
markModified : markModifiedFunc,
|
||||||
|
toObject : function() {
|
||||||
|
delete this.save;
|
||||||
|
delete this.toObject;
|
||||||
|
delete this.remove;
|
||||||
|
delete this.markModified;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
google = require('./googleActions.js');
|
||||||
|
model = require('./homebrew.model.js').model;
|
||||||
|
|
||||||
|
jest.mock('./googleActions.js');
|
||||||
|
google.authCheck = jest.fn(()=>'client');
|
||||||
|
google.newGoogleBrew = jest.fn(()=>'id');
|
||||||
|
google.deleteGoogleBrew = jest.fn(()=>true);
|
||||||
|
|
||||||
|
jest.mock('./homebrew.model.js');
|
||||||
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
|
res = {
|
||||||
|
status : jest.fn(()=>res),
|
||||||
|
send : jest.fn(()=>{})
|
||||||
|
};
|
||||||
|
|
||||||
|
api = require('./homebrew.api');
|
||||||
|
|
||||||
|
hbBrew = {
|
||||||
|
text : `brew text`,
|
||||||
|
style : 'hello yes i am css',
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
published : true,
|
||||||
|
authors : ['1', '2'],
|
||||||
|
owner : '1',
|
||||||
|
thumbnail : '',
|
||||||
|
_id : 'mongoid',
|
||||||
|
editId : 'abcdefg',
|
||||||
|
shareId : 'hijklmnop',
|
||||||
|
views : 1,
|
||||||
|
lastViewed : new Date(),
|
||||||
|
version : 1,
|
||||||
|
pageCount : 1,
|
||||||
|
textBin : '',
|
||||||
|
views : 0
|
||||||
|
};
|
||||||
|
googleBrew = {
|
||||||
|
...hbBrew,
|
||||||
|
googleId : '12345'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getId', ()=>{
|
||||||
|
it('should return only id if google id is not present', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefgh'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefgh');
|
||||||
|
expect(googleId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return id and google id from request body', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : 'abcdefgh'
|
||||||
|
},
|
||||||
|
body : {
|
||||||
|
googleId : '12345'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefgh');
|
||||||
|
expect(googleId).toEqual('12345');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return id and google id from params', ()=>{
|
||||||
|
const { id, googleId } = api.getId({
|
||||||
|
params : {
|
||||||
|
id : '123456789012abcdefghijkl'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(id).toEqual('abcdefghijkl');
|
||||||
|
expect(googleId).toEqual('123456789012');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getBrew', ()=>{
|
||||||
|
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
||||||
|
const notFoundError = 'Brew not found in Homebrewery database or Google Drive';
|
||||||
|
|
||||||
|
it('returns middleware', ()=>{
|
||||||
|
const getFn = api.getBrew('share');
|
||||||
|
expect(getFn).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch from mongoose', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', authors: [] };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual(testBrew);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mongoose error', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>new Promise((_, rej)=>rej('Unable to find brew')));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, next);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual(notFoundError);
|
||||||
|
expect(req.brew).toEqual({});
|
||||||
|
expect(next).not.toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('changes tags from string to array', async ()=>{
|
||||||
|
const testBrew = { title: 'test brew', authors: [], tags: '' };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew.tags).toEqual([]);
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if invalid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await fn(req, null, null);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).toEqual(`The current logged in user does not have editor access to this brew.
|
||||||
|
|
||||||
|
If you believe you should have access to this brew, ask the file owner to invite you as an author by opening the brew, viewing the Properties tab, and adding your username to the "invited authors" list. You can then try to access this document again.`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw if no authors', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: [] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(req.brew.title).toEqual('test brew');
|
||||||
|
expect(req.brew.authors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not throw if valid author', async ()=>{
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise({ title: 'test brew', authors: ['a'] }));
|
||||||
|
|
||||||
|
const fn = api.getBrew('edit', true);
|
||||||
|
const req = { brew: {}, account: { username: 'a' } };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(req.brew.title).toEqual('test brew');
|
||||||
|
expect(req.brew.authors).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches google brew if needed', async()=>{
|
||||||
|
const stubBrew = { title: 'test brew', authors: ['a'] };
|
||||||
|
const googleBrew = { title: 'test google brew', text: 'brew text' };
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
|
model.get = jest.fn(()=>toBrewPromise(stubBrew));
|
||||||
|
google.getGoogleBrew = jest.fn(()=>new Promise((res)=>res(googleBrew)));
|
||||||
|
|
||||||
|
const fn = api.getBrew('share', false);
|
||||||
|
const req = { brew: {} };
|
||||||
|
const next = jest.fn();
|
||||||
|
await fn(req, null, next);
|
||||||
|
|
||||||
|
expect(req.brew).toEqual({
|
||||||
|
title : 'test google brew',
|
||||||
|
authors : ['a'],
|
||||||
|
text : 'brew text',
|
||||||
|
stubbed : true,
|
||||||
|
description : '',
|
||||||
|
editId : undefined,
|
||||||
|
pageCount : 1,
|
||||||
|
published : false,
|
||||||
|
renderer : 'legacy',
|
||||||
|
shareId : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
textBin : undefined,
|
||||||
|
version : undefined,
|
||||||
|
createdAt : undefined,
|
||||||
|
gDrive : false,
|
||||||
|
style : undefined,
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined,
|
||||||
|
views : 0
|
||||||
|
});
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(api.getId).toHaveBeenCalledWith(req);
|
||||||
|
expect(model.get).toHaveBeenCalledWith({ shareId: '1' });
|
||||||
|
expect(google.getGoogleBrew).toHaveBeenCalledWith('2', '1', 'share');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mergeBrewText', ()=>{
|
||||||
|
it('should set metadata and no style if it is not present', ()=>{
|
||||||
|
const result = api.mergeBrewText({
|
||||||
|
text : `brew`,
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
googleId : '12345'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(`\`\`\`metadata
|
||||||
|
title: some title
|
||||||
|
description: this is a description
|
||||||
|
tags:
|
||||||
|
- something
|
||||||
|
- fun
|
||||||
|
systems:
|
||||||
|
- D&D 5e
|
||||||
|
renderer: v3
|
||||||
|
theme: phb
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
brew`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set metadata and style', ()=>{
|
||||||
|
const result = api.mergeBrewText({
|
||||||
|
text : `brew`,
|
||||||
|
style : 'hello yes i am css',
|
||||||
|
title : 'some title',
|
||||||
|
description : 'this is a description',
|
||||||
|
tags : ['something', 'fun'],
|
||||||
|
systems : ['D&D 5e'],
|
||||||
|
renderer : 'v3',
|
||||||
|
theme : 'phb',
|
||||||
|
googleId : '12345'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual(`\`\`\`metadata
|
||||||
|
title: some title
|
||||||
|
description: this is a description
|
||||||
|
tags:
|
||||||
|
- something
|
||||||
|
- fun
|
||||||
|
systems:
|
||||||
|
- D&D 5e
|
||||||
|
renderer: v3
|
||||||
|
theme: phb
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
\`\`\`css
|
||||||
|
hello yes i am css
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
brew`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('exclusion methods', ()=>{
|
||||||
|
it('excludePropsFromUpdate removes the correct keys', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludePropsFromUpdate(sent);
|
||||||
|
|
||||||
|
expect(sent).toEqual(googleBrew);
|
||||||
|
expect(result._id).toBeUndefined();
|
||||||
|
expect(result.views).toBeUndefined();
|
||||||
|
expect(result.lastViewed).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludeGoogleProps removes the correct keys', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludeGoogleProps(sent);
|
||||||
|
|
||||||
|
expect(sent).toEqual(googleBrew);
|
||||||
|
expect(result.tags).toBeUndefined();
|
||||||
|
expect(result.systems).toBeUndefined();
|
||||||
|
expect(result.published).toBeUndefined();
|
||||||
|
expect(result.authors).toBeUndefined();
|
||||||
|
expect(result.owner).toBeUndefined();
|
||||||
|
expect(result.views).toBeUndefined();
|
||||||
|
expect(result.thumbnail).toBeUndefined();
|
||||||
|
expect(result.version).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludeStubProps removes the correct keys from the original object', ()=>{
|
||||||
|
const sent = Object.assign({}, googleBrew);
|
||||||
|
const result = api.excludeStubProps(sent);
|
||||||
|
|
||||||
|
expect(sent).not.toEqual(googleBrew);
|
||||||
|
expect(result.text).toBeUndefined();
|
||||||
|
expect(result.textBin).toBeUndefined();
|
||||||
|
expect(result.renderer).toBeUndefined();
|
||||||
|
expect(result.pageCount).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('beforeNewSave', ()=>{
|
||||||
|
it('sets the title if none', ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
title : undefined
|
||||||
|
};
|
||||||
|
api.beforeNewSave({}, brew);
|
||||||
|
|
||||||
|
expect(brew.title).toEqual('brew text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not override the title if present', ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
title : 'test'
|
||||||
|
};
|
||||||
|
api.beforeNewSave({}, brew);
|
||||||
|
|
||||||
|
expect(brew.title).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set authors if account missing username', ()=>{
|
||||||
|
api.beforeNewSave({}, hbBrew);
|
||||||
|
|
||||||
|
expect(hbBrew.authors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets authors if account has username', ()=>{
|
||||||
|
api.beforeNewSave({ username: 'hi' }, hbBrew);
|
||||||
|
|
||||||
|
expect(hbBrew.authors).toEqual(['hi']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges brew text', ()=>{
|
||||||
|
api.mergeBrewText = jest.fn(()=>'merged');
|
||||||
|
api.beforeNewSave({}, hbBrew);
|
||||||
|
|
||||||
|
expect(api.mergeBrewText).toHaveBeenCalled();
|
||||||
|
expect(hbBrew.text).toEqual('merged');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newGoogleBrew', ()=>{
|
||||||
|
it('should call the correct methods', ()=>{
|
||||||
|
api.excludeGoogleProps = jest.fn(()=>'newBrew');
|
||||||
|
|
||||||
|
const acct = { username: 'test' };
|
||||||
|
const brew = { title: 'test title' };
|
||||||
|
api.newGoogleBrew(acct, brew, res);
|
||||||
|
|
||||||
|
expect(google.authCheck).toHaveBeenCalledWith(acct, res);
|
||||||
|
expect(api.excludeGoogleProps).toHaveBeenCalledWith(brew);
|
||||||
|
expect(google.newGoogleBrew).toHaveBeenCalledWith('client', 'newBrew');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('newBrew', ()=>{
|
||||||
|
it('should set up a default brew via Homebrew model', async ()=>{
|
||||||
|
await api.newBrew({ body: { text: 'asdf' }, query: {}, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
_id : '1',
|
||||||
|
authors : ['test user'],
|
||||||
|
createdAt : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : expect.any(String),
|
||||||
|
gDrive : false,
|
||||||
|
pageCount : 1,
|
||||||
|
published : false,
|
||||||
|
renderer : 'V3',
|
||||||
|
shareId : expect.any(String),
|
||||||
|
style : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
text : undefined,
|
||||||
|
textBin : expect.objectContaining({}),
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
title : 'asdf',
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined,
|
||||||
|
views : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove edit/share/google ids', async ()=>{
|
||||||
|
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalled();
|
||||||
|
const sent = res.send.mock.calls[0][0];
|
||||||
|
expect(sent.editId).not.toEqual('1234');
|
||||||
|
expect(sent.shareId).not.toEqual('1234');
|
||||||
|
expect(sent.googleId).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mongo error', async ()=>{
|
||||||
|
saveFunc = jest.fn(async function() {
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
model.mockImplementation((brew)=>modelBrew(brew));
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.newBrew({ body: { editId: '1234', shareId: '1234', googleId: '1234', text: 'asdf', title: '' }, query: {} }, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(res.send).not.toHaveBeenCalled();
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save to google if requested', async()=>{
|
||||||
|
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(google.newGoogleBrew).toHaveBeenCalled();
|
||||||
|
expect(res.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(res.send).toHaveBeenCalledWith({
|
||||||
|
_id : '1',
|
||||||
|
authors : ['test user'],
|
||||||
|
createdAt : undefined,
|
||||||
|
description : '',
|
||||||
|
editId : expect.any(String),
|
||||||
|
gDrive : false,
|
||||||
|
pageCount : undefined,
|
||||||
|
published : false,
|
||||||
|
renderer : undefined,
|
||||||
|
shareId : expect.any(String),
|
||||||
|
googleId : expect.any(String),
|
||||||
|
style : undefined,
|
||||||
|
systems : [],
|
||||||
|
tags : [],
|
||||||
|
text : undefined,
|
||||||
|
textBin : undefined,
|
||||||
|
theme : '5ePHB',
|
||||||
|
thumbnail : '',
|
||||||
|
title : 'asdf',
|
||||||
|
trashed : false,
|
||||||
|
updatedAt : undefined,
|
||||||
|
views : 0
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle google error', async()=>{
|
||||||
|
google.newGoogleBrew = jest.fn(()=>{
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('err');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteGoogleBrew', ()=>{
|
||||||
|
it('should check auth and delete brew', async ()=>{
|
||||||
|
const result = await api.deleteGoogleBrew({ username: 'test user' }, 'id', 'editId', res);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(google.authCheck).toHaveBeenCalledWith({ username: 'test user' }, expect.objectContaining({}));
|
||||||
|
expect(google.deleteGoogleBrew).toHaveBeenCalledWith('client', 'id', 'editId');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteBrew', ()=>{
|
||||||
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
|
api.getBrew = jest.fn(()=>async ()=>{ throw 'err'; });
|
||||||
|
api.getId = jest.fn(()=>({ id: '1', googleId: '2' }));
|
||||||
|
model.deleteOne = jest.fn(async ()=>{});
|
||||||
|
const next = jest.fn(()=>{});
|
||||||
|
|
||||||
|
await api.deleteBrew(null, null, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
expect(model.deleteOne).toHaveBeenCalledWith({ editId: '1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete if no authors', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : []
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
const req = {};
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw on delete error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : []
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
removeFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||||
|
const req = {};
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete when one author', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove one author when multiple present', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(markModifiedFunc).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle save error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...hbBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
saveFunc = jest.fn(async ()=>{ throw 'err'; });
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
let err;
|
||||||
|
try {
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
} catch (e) {
|
||||||
|
err = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(err).not.toBeUndefined();
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete google brew', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle google brew delete error', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>{
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete google brew and retain stub when multiple authors and owner request deletion', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(markModifiedFunc).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test2']);
|
||||||
|
expect(saved.googleId).toEqual(undefined);
|
||||||
|
expect(saved.text).toEqual(undefined);
|
||||||
|
expect(saved.textBin).not.toEqual(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should retain google brew and update stub when multiple authors and extra author requests deletion', async ()=>{
|
||||||
|
const brew = {
|
||||||
|
...googleBrew,
|
||||||
|
authors : ['test', 'test2']
|
||||||
|
};
|
||||||
|
api.getBrew = jest.fn(()=>async (req)=>{
|
||||||
|
req.brew = brew;
|
||||||
|
});
|
||||||
|
model.findOne = jest.fn(async ()=>modelBrew(brew));
|
||||||
|
api.deleteGoogleBrew = jest.fn(async ()=>true);
|
||||||
|
const req = { account: { username: 'test2' } };
|
||||||
|
|
||||||
|
await api.deleteBrew(req, res);
|
||||||
|
|
||||||
|
expect(api.getBrew).toHaveBeenCalled();
|
||||||
|
expect(model.findOne).toHaveBeenCalled();
|
||||||
|
expect(removeFunc).not.toHaveBeenCalled();
|
||||||
|
expect(api.deleteGoogleBrew).not.toHaveBeenCalled();
|
||||||
|
expect(saveFunc).toHaveBeenCalled();
|
||||||
|
expect(saved.authors).toEqual(['test']);
|
||||||
|
expect(saved.googleId).toEqual(brew.googleId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -12,14 +12,15 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
textBin : { type: Buffer },
|
textBin : { type: Buffer },
|
||||||
pageCount : { type: Number, default: 1 },
|
pageCount : { type: Number, default: 1 },
|
||||||
|
|
||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : [String],
|
tags : [String],
|
||||||
systems : [String],
|
systems : [String],
|
||||||
lang : { type: String, default: 'en' },
|
lang : { type: String, default: 'en' },
|
||||||
renderer : { type: String, default: '' },
|
renderer : { type: String, default: '' },
|
||||||
authors : [String],
|
authors : [String],
|
||||||
published : { type: Boolean, default: false },
|
invitedAuthors : [String],
|
||||||
thumbnail : { type: String, default: '' },
|
published : { type: Boolean, default: false },
|
||||||
|
thumbnail : { type: String, default: '' },
|
||||||
|
|
||||||
createdAt : { type: Date, default: Date.now },
|
createdAt : { type: Date, default: Date.now },
|
||||||
updatedAt : { type: Date, default: Date.now },
|
updatedAt : { type: Date, default: Date.now },
|
||||||
@@ -47,8 +48,6 @@ HomebrewSchema.statics.get = function(query, fields=null){
|
|||||||
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
unzipped = zlib.inflateRawSync(brews[0].textBin);
|
||||||
brews[0].text = unzipped.toString();
|
brews[0].text = unzipped.toString();
|
||||||
}
|
}
|
||||||
if(!brews[0].renderer)
|
|
||||||
brews[0].renderer = 'legacy';
|
|
||||||
return resolve(brews[0]);
|
return resolve(brews[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
12
server/middleware/check-client-version.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module.exports = (req, res, next)=>{
|
||||||
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = require('../../package.json').version;
|
||||||
|
|
||||||
|
if(userVersion != version) {
|
||||||
|
return res.status(412).send({
|
||||||
|
message : `Client version ${userVersion} is out of date. Please save your changes elsewhere and refresh to pick up client version ${version}.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
i{
|
i{
|
||||||
display : block;
|
display : block !important;
|
||||||
margin : 10px 0px;
|
margin : 10px 0px;
|
||||||
font-size : 6px;
|
font-size : 6px;
|
||||||
color : #666;
|
color : #666;
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
|
contain : size;
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
|
|||||||
BIN
themes/V3/5eDMG/dropdownPreview.png
Normal file
|
After Width: | Height: | Size: 574 KiB |
BIN
themes/V3/5ePHB/dropdownPreview.png
Normal file
|
After Width: | Height: | Size: 782 KiB |
@@ -169,9 +169,10 @@ module.exports = [
|
|||||||
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Cover Page',
|
name : 'Cover Page',
|
||||||
icon : 'fas fa-file-word',
|
icon : 'fac book-front-cover',
|
||||||
gen : CoverPageGen,
|
gen : CoverPageGen,
|
||||||
|
experimental : true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name : 'Magic Item',
|
name : 'Magic Item',
|
||||||
|
|||||||
@@ -1,55 +1,46 @@
|
|||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
const titles = [
|
const titles = [
|
||||||
'The Burning Gallows',
|
'The Burning Gallows', 'The Ring of Nenlast',
|
||||||
'The Ring of Nenlast',
|
'Below the Blind Tavern', 'Below the Hungering River',
|
||||||
'Below the Blind Tavern',
|
'Before Bahamut\'s Land', 'The Cruel Grave from Within',
|
||||||
'Below the Hungering River',
|
'The Strength of Trade Road', 'Through The Raven Queen\'s Worlds',
|
||||||
'Before Bahamut\'s Land',
|
'Within the Settlement', 'The Crown from Within',
|
||||||
'The Cruel Grave from Within',
|
'The Merchant Within the Battlefield', 'Ioun\'s Fading Traveler',
|
||||||
'The Strength of Trade Road',
|
'The Legion Ingredient', 'The Explorer Lure',
|
||||||
'Through The Raven Queen\'s Worlds',
|
'Before the Charming Badlands', 'Vecna\'s Hidden Sage',
|
||||||
'Within the Settlement',
|
'The Living Dead Above the Fearful Cage', 'Bahamut\'s Demonspawn',
|
||||||
'The Crown from Within',
|
'Across Gruumsh\'s Elemental Chaos', 'The Blade of Orcus',
|
||||||
'The Merchant Within the Battlefield',
|
'Beyond Revenge', 'Brain of Insanity',
|
||||||
'Ioun\'s Fading Traveler',
|
'Breed Battle!, A New Beginning', 'Evil Lake, A New Beginning',
|
||||||
'The Legion Ingredient',
|
'Invasion of the Gigantic Cat, Part II', 'Kraken War 2020',
|
||||||
'The Explorer Lure',
|
'The Body Whisperers', 'The Doctor from Heaven',
|
||||||
'Before the Charming Badlands',
|
'The Diabolical Tales of the Ape-Women', 'The Doctor Immortal',
|
||||||
'The Living Dead Above the Fearful Cage',
|
'Core of Heaven: Guardian of Amazement', 'The Graveyard',
|
||||||
'Vecna\'s Hidden Sage',
|
'Guardian: Skies of the Dark Wizard', 'Lute of Eternity',
|
||||||
'Bahamut\'s Demonspawn',
|
'Mercury\'s Planet: Brave Evolution', 'Azure Core',
|
||||||
'Across Gruumsh\'s Elemental Chaos',
|
'Sky of Zelda: The Thunder of Force', 'Core Battle',
|
||||||
'The Blade of Orcus',
|
'Ruby of Atlantis: The Quake of Peace', 'Deadly Amazement III',
|
||||||
'Beyond Revenge',
|
'Dry Chaos IX', 'Gate Thunder',
|
||||||
'Brain of Insanity',
|
'Vyse\'s Skies', 'White Greatness III',
|
||||||
'Breed Battle!, A New Beginning',
|
'Yellow Divinity', 'Zidane\'s Ghost'
|
||||||
'Evil Lake, A New Beginning',
|
|
||||||
'Invasion of the Gigantic Cat, Part II',
|
|
||||||
'Kraken War 2020',
|
|
||||||
'The Body Whisperers',
|
|
||||||
'The Diabolical Tales of the Ape-Women',
|
|
||||||
'The Doctor Immortal',
|
|
||||||
'The Doctor from Heaven',
|
|
||||||
'The Graveyard',
|
|
||||||
'Azure Core',
|
|
||||||
'Core Battle',
|
|
||||||
'Core of Heaven: The Guardian of Amazement',
|
|
||||||
'Deadly Amazement III',
|
|
||||||
'Dry Chaos IX',
|
|
||||||
'Gate Thunder',
|
|
||||||
'Guardian: Skies of the Dark Wizard',
|
|
||||||
'Lute of Eternity',
|
|
||||||
'Mercury\'s Planet: Brave Evolution',
|
|
||||||
'Ruby of Atlantis: The Quake of Peace',
|
|
||||||
'Sky of Zelda: The Thunder of Force',
|
|
||||||
'Vyse\'s Skies',
|
|
||||||
'White Greatness III',
|
|
||||||
'Yellow Divinity',
|
|
||||||
'Zidane\'s Ghost'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const subtitles = [
|
const subtitles = [
|
||||||
|
'Tomb of Shadows', 'Dragon\'s Lair',
|
||||||
|
'Lost Caverns', 'The Necromancer',
|
||||||
|
'Mystic Forest', 'Cursed Ruins',
|
||||||
|
'The Dark Abyss', 'Enchanted Maze',
|
||||||
|
'Haunted Castle', 'Sands of Fate',
|
||||||
|
'Dragon\'s Hoard', 'Undead Menace',
|
||||||
|
'Lost City Ruins', 'Goblin Ambush',
|
||||||
|
'Enchanted Forest', 'Darkness Rising',
|
||||||
|
'Quest for Glory', 'Ancient Prophecy',
|
||||||
|
'Shadowy Depths', 'Mystic Isles'
|
||||||
|
];
|
||||||
|
|
||||||
|
const footnote = [
|
||||||
'In an ominous universe, a botanist opposes terrorism.',
|
'In an ominous universe, a botanist opposes terrorism.',
|
||||||
'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.',
|
'In a demon-haunted city, in an age of lies and hate, a physicist tries to find an ancient treasure and battles a mob of aliens.',
|
||||||
'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.',
|
'In a land of corruption, two cyberneticists and a dungeon delver search for freedom.',
|
||||||
@@ -74,51 +65,26 @@ const subtitles = [
|
|||||||
'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.',
|
'On a planet of mysticism, three travelers and a fire fighter quest for the ultimate weapon and oppose evil.',
|
||||||
'In a wicked universe, five seers fight lawlessness.',
|
'In a wicked universe, five seers fight lawlessness.',
|
||||||
'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.',
|
'In a kingdom of death, in an era of illusion and blood, four colonists search for fame.',
|
||||||
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.',
|
'In an amazing kingdom, in an age of sorcery and lost souls, eight space pirates quest for freedom.'
|
||||||
'In a cursed empire, five inventors oppose terrorism.',
|
|
||||||
'On a crime-ridden planet of conspiracy, a watchman and an artificial intelligence try to find love and oppose lawlessness.',
|
|
||||||
'In a forgotten land, a reporter and a spy try to stop the apocalypse.',
|
|
||||||
'In a forbidden land of prophecy, a scientist and an archivist oppose a cabal of barbarians intent on stealing the souls of the innocent.',
|
|
||||||
'On an infernal world of illusion, a grave robber and a watchman try to find revenge and combat a syndicate of mages intent on stealing the source of all magic.',
|
|
||||||
'In a galaxy of dark magic, four fighters seek freedom.',
|
|
||||||
'In an empire of deception, six tomb-robbers quest for the ultimate weapon and combat an army of raiders.',
|
|
||||||
'In a kingdom of corruption and lost souls, in an age of panic, eight planetologists oppose evil.',
|
|
||||||
'In a galaxy of misery and hopelessness, in a time of agony and pain, five planetologists search for vengance.',
|
|
||||||
'In a universe of technology and insanity, in a time of sorcery, a computer techician quests for hope.',
|
|
||||||
'On a planet of dark magic and barbarism, in an age of horror and blasphemy, seven librarians search for fame.',
|
|
||||||
'In an empire of dark magic, in a time of blood and illusions, four monks try to find the ultimate weapon and combat terrorism.',
|
|
||||||
'In a forgotten empire of dark magic, six kings try to prevent the destruction of mankind.',
|
|
||||||
'In a galaxy of dark magic and horror, in an age of hopelessness, four marines and an outlaw combat evil.',
|
|
||||||
'In a mysterious city of illusion, in an age of computerization, a witch-hunter tries to find the ultimate weapon and opposes an evil corporation.',
|
|
||||||
'In a damned kingdom of technology, a virtual reality programmer and a fighter seek fame.',
|
|
||||||
'In a hellish kingdom, in an age of blasphemy and blasphemy, an astrologer searches for fame.',
|
|
||||||
'In a damned world of devils, an alien and a ranger quest for love and oppose a syndicate of demons.',
|
|
||||||
'In a cursed galaxy, in a time of pain, seven librarians hope to avert the apocalypse.',
|
|
||||||
'In a crime-infested galaxy, in an era of hopelessness and panic, three champions and a grave robber try to solve the ultimate crime.'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
module.exports = ()=>{
|
module.exports = ()=>{
|
||||||
return `<style>
|
return dedent`
|
||||||
.page#p1{ text-align:center; counter-increment: none; }
|
{{coverPage }}
|
||||||
.page#p1:after{ display:none; }
|
|
||||||
.page:nth-child(2n) .pageNumber { left: inherit !important; right: 2px !important; }
|
|
||||||
.page:nth-child(2n+1) .pageNumber { right: inherit !important; left: 2px !important; }
|
|
||||||
.page:nth-child(2n)::after { transform: scaleX(1); }
|
|
||||||
.page:nth-child(2n+1)::after { transform: scaleX(-1); }
|
|
||||||
.page:nth-child(2n) .footnote { left: inherit; text-align: right; }
|
|
||||||
.page:nth-child(2n+1) .footnote { left: 80px; text-align: left; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
{{margin-top:225px}}
|
{{logo }}
|
||||||
|
|
||||||
# ${_.sample(titles)}
|
# ${_.sample(titles)}
|
||||||
|
## ${_.sample(subtitles)}
|
||||||
|
__________
|
||||||
|
|
||||||
{{margin-top:25px}}
|
{{banner HOMEBREW}}
|
||||||
|
|
||||||
{{wide
|
{{footnote
|
||||||
##### ${_.sample(subtitles)}
|
${_.sample(footnote)}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
\\page`;
|

|
||||||
};
|
|
||||||
|
\page`;
|
||||||
|
};
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ body {
|
|||||||
border-image-outset : 9px 0px;
|
border-image-outset : 9px 0px;
|
||||||
border-image-width : 11px;
|
border-image-width : 11px;
|
||||||
padding : 0.13cm 0.16cm;
|
padding : 0.13cm 0.16cm;
|
||||||
filter : drop-shadow(1px 4px 6px #888);
|
box-shadow : 1px 4px 14px #888;
|
||||||
.page :where(&) {
|
.page :where(&) {
|
||||||
margin-top : 9px; //Prevent top border getting cut off on colbreak
|
margin-top : 9px; //Prevent top border getting cut off on colbreak
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,7 @@ body {
|
|||||||
border-image : @descriptiveBoxImage 12 stretch;
|
border-image : @descriptiveBoxImage 12 stretch;
|
||||||
border-image-outset : 4px;
|
border-image-outset : 4px;
|
||||||
padding : 0.1em;
|
padding : 0.1em;
|
||||||
filter : drop-shadow(0 0 3px #faf7ea);
|
box-shadow : 0 0 6px #faf7ea;
|
||||||
.page :where(&) {
|
.page :where(&) {
|
||||||
margin-top : 4px; //Prevent top border getting cut off on colbreak
|
margin-top : 4px; //Prevent top border getting cut off on colbreak
|
||||||
}
|
}
|
||||||
@@ -397,7 +397,7 @@ body {
|
|||||||
border-image-outset : 0px 2px;
|
border-image-outset : 0px 2px;
|
||||||
background-blend-mode : overlay;
|
background-blend-mode : overlay;
|
||||||
background-attachment : fixed;
|
background-attachment : fixed;
|
||||||
filter : drop-shadow(1px 4px 6px #888);
|
box-shadow : 1px 4px 14px #888;
|
||||||
padding : 4px 2px;
|
padding : 4px 2px;
|
||||||
margin-left : -0.16cm;
|
margin-left : -0.16cm;
|
||||||
margin-right : -0.16cm;
|
margin-right : -0.16cm;
|
||||||
@@ -624,134 +624,242 @@ body {
|
|||||||
// * CLASS TABLE
|
// * CLASS TABLE
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page .classTable{
|
.page .classTable{
|
||||||
th[colspan]:not([rowspan]) {
|
th[colspan]:not([rowspan]) {
|
||||||
white-space : nowrap;
|
white-space : nowrap;
|
||||||
|
}
|
||||||
|
&.frame {
|
||||||
|
margin-top : 0.7cm;
|
||||||
|
margin-bottom : 0.9cm;
|
||||||
|
margin-left : -0.1cm;
|
||||||
|
margin-right : -0.1cm;
|
||||||
|
width : calc(100% + 0.2cm);
|
||||||
|
border-collapse : separate;
|
||||||
|
background-color : white;
|
||||||
|
border : initial;
|
||||||
|
border-style : solid;
|
||||||
|
border-image-outset : 0.4cm 0.3cm;
|
||||||
|
border-image-repeat : stretch;
|
||||||
|
border-image-slice : 200;
|
||||||
|
border-image-source : @frameBorderImage;
|
||||||
|
border-image-width : 47px;
|
||||||
|
&.wide:first-child {
|
||||||
|
margin-top: 0.12cm;
|
||||||
}
|
}
|
||||||
&.frame {
|
& + * {
|
||||||
margin-top : 0.7cm;
|
margin-top: 0;
|
||||||
margin-bottom : 0.9cm;
|
|
||||||
margin-left : -0.1cm;
|
|
||||||
margin-right : -0.1cm;
|
|
||||||
width : calc(100% + 0.2cm);
|
|
||||||
border-collapse : separate;
|
|
||||||
background-color : white;
|
|
||||||
border : initial;
|
|
||||||
border-style : solid;
|
|
||||||
border-image-outset : 0.4cm 0.3cm;
|
|
||||||
border-image-repeat : stretch;
|
|
||||||
border-image-slice : 200;
|
|
||||||
border-image-source : @frameBorderImage;
|
|
||||||
border-image-width : 47px;
|
|
||||||
&.wide:first-child {
|
|
||||||
margin-top: 0.12cm;
|
|
||||||
}
|
|
||||||
& + * {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.decoration {
|
|
||||||
position:relative;
|
|
||||||
}
|
|
||||||
&.decoration::before {
|
|
||||||
content :'';
|
|
||||||
position : absolute;
|
|
||||||
background-image : @classTableDecoration,
|
|
||||||
@classTableDecoration;
|
|
||||||
background-size : contain, contain;
|
|
||||||
background-repeat : no-repeat, no-repeat;
|
|
||||||
background-position : top, bottom;
|
|
||||||
width : 7.75cm;
|
|
||||||
height : calc(100% + 3.3cm);
|
|
||||||
top : 50%;
|
|
||||||
left : 50%;
|
|
||||||
transform : translateY(-50%) translateX(-50%);
|
|
||||||
filter : drop-shadow(0px 0px 1px #C8C5C080);
|
|
||||||
z-index : -1;
|
|
||||||
}
|
|
||||||
&.decoration.wide::before {
|
|
||||||
width : calc(100% + 3.3cm);
|
|
||||||
height : 7.75cm;
|
|
||||||
background-position : left, right;
|
|
||||||
}
|
|
||||||
h5 + table{
|
|
||||||
margin-top : 0.2cm;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.decoration {
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
&.decoration::before {
|
||||||
|
content :'';
|
||||||
|
position : absolute;
|
||||||
|
background-image : @classTableDecoration,
|
||||||
|
@classTableDecoration;
|
||||||
|
background-size : contain, contain;
|
||||||
|
background-repeat : no-repeat, no-repeat;
|
||||||
|
background-position : top, bottom;
|
||||||
|
width : 7.75cm;
|
||||||
|
height : calc(100% + 3.3cm);
|
||||||
|
top : 50%;
|
||||||
|
left : 50%;
|
||||||
|
transform : translateY(-50%) translateX(-50%);
|
||||||
|
filter : drop-shadow(0px 0px 1px #C8C5C080);
|
||||||
|
z-index : -1;
|
||||||
|
}
|
||||||
|
&.decoration.wide::before {
|
||||||
|
width : calc(100% + 3.3cm);
|
||||||
|
height : 7.75cm;
|
||||||
|
background-position : left, right;
|
||||||
|
}
|
||||||
|
h5 + table{
|
||||||
|
margin-top : 0.2cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * COVER PAGE
|
||||||
|
// *****************************/
|
||||||
|
.page:has(.coverPage) {
|
||||||
|
columns : 1;
|
||||||
|
text-align : center;
|
||||||
|
&:after {
|
||||||
|
all: unset;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
position : absolute;
|
||||||
|
top : 0.5cm;
|
||||||
|
left : 0;
|
||||||
|
right : 0;
|
||||||
|
filter :drop-shadow(0 0 0.075cm black);
|
||||||
|
img {
|
||||||
|
height : 2cm;
|
||||||
|
width : 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.columnWrapper > p img {
|
||||||
|
position : absolute;
|
||||||
|
bottom : 0;
|
||||||
|
left : 0;
|
||||||
|
height : 100%;
|
||||||
|
min-width : 100%;
|
||||||
|
z-index : -1;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
--shadow-x0 : #000 0px 0px 0.1cm;
|
||||||
|
--shadow-x1 : var(--shadow-x0), var(--shadow-x0), var(--shadow-x0);
|
||||||
|
--shadow-x2 : var(--shadow-x1), var(--shadow-x1), var(--shadow-x1);
|
||||||
|
--shadow-x3 : var(--shadow-x2), var(--shadow-x2), var(--shadow-x2);
|
||||||
|
text-shadow : var(--shadow-x3), var(--shadow-x3), var(--shadow-x3);
|
||||||
|
text-transform : uppercase;
|
||||||
|
font-weight : normal;
|
||||||
|
display : block;
|
||||||
|
margin-top : 1.2cm;
|
||||||
|
margin-bottom : 0;
|
||||||
|
color : white;
|
||||||
|
font-family : NodestoCapsCondensed;
|
||||||
|
font-size : 2.245cm;
|
||||||
|
line-height : 0.85em;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
--shadow-x0 : #000 0px 0px 2.5px;
|
||||||
|
--shadow-x1 : var(--shadow-x0), var(--shadow-x0), var(--shadow-x0);
|
||||||
|
--shadow-x2 : var(--shadow-x1), var(--shadow-x1), var(--shadow-x1);
|
||||||
|
--shadow-x3 : var(--shadow-x2), var(--shadow-x2), var(--shadow-x2);
|
||||||
|
text-shadow : var(--shadow-x3), var(--shadow-x3), var(--shadow-x3);
|
||||||
|
font-family : NodestoCapsCondensed;
|
||||||
|
font-weight : normal;
|
||||||
|
font-size : 0.85cm;
|
||||||
|
letter-spacing : 0.1cm;
|
||||||
|
color : white;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
display : block;
|
||||||
|
position : relative;
|
||||||
|
background-image : @horizontalRule;
|
||||||
|
background-size : 100% 100%;
|
||||||
|
visibility : visible;
|
||||||
|
height : 0.5cm;
|
||||||
|
width : 12cm;
|
||||||
|
border : none;
|
||||||
|
margin : auto;
|
||||||
|
filter : drop-shadow(0 0 3px black);
|
||||||
|
}
|
||||||
|
.banner {
|
||||||
|
filter : drop-shadow(2px 2px 2px black);
|
||||||
|
position : absolute;
|
||||||
|
left : 0;
|
||||||
|
bottom : 4.2cm;
|
||||||
|
background-image : url('/assets/coverPageBanner.svg');
|
||||||
|
height : 1.7cm;
|
||||||
|
width : 10.5cm;
|
||||||
|
color : white;
|
||||||
|
font-family : NodestoCapsCondensed;
|
||||||
|
font-weight : normal;
|
||||||
|
font-size : 1cm;
|
||||||
|
letter-spacing : 0.014cm;
|
||||||
|
text-align : left;
|
||||||
|
padding-left : 1cm;
|
||||||
|
display : flex;
|
||||||
|
justify-content : center;
|
||||||
|
flex-direction : column;
|
||||||
|
padding-top : 0.1cm;
|
||||||
|
}
|
||||||
|
.footnote {
|
||||||
|
--shadow-x0 : #000 0px 0px 0.05cm;
|
||||||
|
--shadow-x1 : var(--shadow-x0), var(--shadow-x0), var(--shadow-x0);
|
||||||
|
--shadow-x2 : var(--shadow-x1), var(--shadow-x1), var(--shadow-x1);
|
||||||
|
text-shadow : var(--shadow-x2), var(--shadow-x2), var(--shadow-x2);
|
||||||
|
position : absolute;
|
||||||
|
text-align : center;
|
||||||
|
color : white;
|
||||||
|
font-size : 0.496cm;
|
||||||
|
bottom : 1.3cm;
|
||||||
|
left : 0;
|
||||||
|
right : 0;
|
||||||
|
margin-left : auto;
|
||||||
|
margin-right : auto;
|
||||||
|
width : 70%;
|
||||||
|
font-family : Overpass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
// * TABLE OF CONTENTS
|
// * TABLE OF CONTENTS
|
||||||
// *****************************/
|
// *****************************/
|
||||||
.page {
|
.page {
|
||||||
&:has(.toc):after {
|
&:has(.toc):after {
|
||||||
display: none;
|
display: none;
|
||||||
|
}
|
||||||
|
.toc {
|
||||||
|
-webkit-column-break-inside : avoid;
|
||||||
|
page-break-inside : avoid;
|
||||||
|
break-inside : avoid;
|
||||||
|
h1 {
|
||||||
|
text-align : center;
|
||||||
|
margin-bottom : 0.3cm;
|
||||||
}
|
}
|
||||||
.toc {
|
a{
|
||||||
-webkit-column-break-inside : avoid;
|
display : inline;
|
||||||
page-break-inside : avoid;
|
color : inherit;
|
||||||
break-inside : avoid;
|
text-decoration : none;
|
||||||
h1 {
|
&:hover{
|
||||||
text-align : center;
|
text-decoration : underline;
|
||||||
margin-bottom : 0.3cm;
|
|
||||||
}
|
|
||||||
a{
|
|
||||||
display : inline;
|
|
||||||
color : inherit;
|
|
||||||
text-decoration : none;
|
|
||||||
&:hover{
|
|
||||||
text-decoration : underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h4 {
|
|
||||||
margin-top : 0.2cm;
|
|
||||||
line-height : 0.4cm;
|
|
||||||
& + ul li {
|
|
||||||
line-height: 1.2em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ul{
|
|
||||||
padding-left : 0;
|
|
||||||
list-style-type : none;
|
|
||||||
margin-top : 0;
|
|
||||||
a {
|
|
||||||
width : 100%;
|
|
||||||
display : flex;
|
|
||||||
flex-flow : row nowrap;
|
|
||||||
justify-content : space-between;
|
|
||||||
}
|
|
||||||
li + li h3 {
|
|
||||||
margin-top : 0.26cm;
|
|
||||||
line-height : 1em
|
|
||||||
}
|
|
||||||
h3 span:first-child::after {
|
|
||||||
border : none;
|
|
||||||
}
|
|
||||||
span {
|
|
||||||
display : contents;
|
|
||||||
&:first-child::after {
|
|
||||||
content : "";
|
|
||||||
bottom : 0.08cm;
|
|
||||||
flex : 1;
|
|
||||||
margin-left : 0.08cm; /* Spacing before dot leaders */
|
|
||||||
margin-right : 0.16cm;
|
|
||||||
border-bottom : 0.05cm dotted #000;
|
|
||||||
margin-bottom : 0.08cm;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
display : inline-block;
|
|
||||||
align-self : flex-end;
|
|
||||||
font-family : "BookInsanityRemake";
|
|
||||||
font-size : 0.34cm;
|
|
||||||
font-weight : normal;
|
|
||||||
color : #000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ul { /*List indent*/
|
|
||||||
margin-left : 1em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&.wide{
|
|
||||||
.useColumns(0.96, @fillMode: balance);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
h4 {
|
||||||
|
margin-top : 0.2cm;
|
||||||
|
line-height : 0.4cm;
|
||||||
|
& + ul li {
|
||||||
|
line-height: 1.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul{
|
||||||
|
padding-left : 0;
|
||||||
|
list-style-type : none;
|
||||||
|
margin-top : 0;
|
||||||
|
a {
|
||||||
|
width : 100%;
|
||||||
|
display : flex;
|
||||||
|
flex-flow : row nowrap;
|
||||||
|
justify-content : space-between;
|
||||||
|
}
|
||||||
|
li + li h3 {
|
||||||
|
margin-top : 0.26cm;
|
||||||
|
line-height : 1em
|
||||||
|
}
|
||||||
|
h3 span:first-child::after {
|
||||||
|
border : none;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
display : contents;
|
||||||
|
&:first-child::after {
|
||||||
|
content : "";
|
||||||
|
bottom : 0.08cm;
|
||||||
|
flex : 1;
|
||||||
|
margin-left : 0.08cm; /* Spacing before dot leaders */
|
||||||
|
margin-right : 0.16cm;
|
||||||
|
border-bottom : 0.05cm dotted #000;
|
||||||
|
margin-bottom : 0.08cm;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
display : inline-block;
|
||||||
|
align-self : flex-end;
|
||||||
|
font-family : "BookInsanityRemake";
|
||||||
|
font-size : 0.34cm;
|
||||||
|
font-weight : normal;
|
||||||
|
color : #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul { /*List indent*/
|
||||||
|
margin-left : 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.wide{
|
||||||
|
.useColumns(0.96, @fillMode: balance);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
|
|||||||
BIN
themes/V3/Blank/dropdownPreview.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
@@ -1,9 +1,8 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
|
||||||
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
const WatercolorGen = require('./snippets/watercolor.gen.js');
|
||||||
const dedent = require('dedent-tabs').default;
|
const ImageMaskGen = require('./snippets/imageMask.gen.js');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
|
|
||||||
@@ -102,6 +101,62 @@ module.exports = [
|
|||||||
icon : 'fas fa-fill-drip',
|
icon : 'fas fa-fill-drip',
|
||||||
gen : WatercolorGen,
|
gen : WatercolorGen,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name : 'Watercolor Edge',
|
||||||
|
icon : 'fac mask-edge',
|
||||||
|
gen : ImageMaskGen.edge('bottom'),
|
||||||
|
experimental : true,
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Top',
|
||||||
|
icon : 'fac position-top',
|
||||||
|
gen : ImageMaskGen.edge('top'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Right',
|
||||||
|
icon : 'fac position-right',
|
||||||
|
gen : ImageMaskGen.edge('right'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Bottom',
|
||||||
|
icon : 'fac position-bottom',
|
||||||
|
gen : ImageMaskGen.edge('bottom'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Left',
|
||||||
|
icon : 'fac position-left',
|
||||||
|
gen : ImageMaskGen.edge('left'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Watercolor Corner',
|
||||||
|
icon : 'fac mask-corner',
|
||||||
|
gen : ImageMaskGen.corner,
|
||||||
|
experimental : true,
|
||||||
|
subsnippets : [
|
||||||
|
{
|
||||||
|
name : 'Top-Left',
|
||||||
|
icon : 'fac position-top-left',
|
||||||
|
gen : ImageMaskGen.corner('top', 'left'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Top-Right',
|
||||||
|
icon : 'fac position-top-right',
|
||||||
|
gen : ImageMaskGen.corner('top', 'right'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Bottom-Left',
|
||||||
|
icon : 'fac position-bottom-left',
|
||||||
|
gen : ImageMaskGen.corner('bottom', 'left'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name : 'Bottom-Right',
|
||||||
|
icon : 'fac position-bottom-right',
|
||||||
|
gen : ImageMaskGen.corner('bottom', 'right'),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name : 'Watermark',
|
name : 'Watermark',
|
||||||
icon : 'fas fa-id-card',
|
icon : 'fas fa-id-card',
|
||||||
|
|||||||
36
themes/V3/Blank/snippets/imageMask.gen.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
const _ = require('lodash');
|
||||||
|
const dedent = require('dedent-tabs').default;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
edge : (side = 'bottom')=>{
|
||||||
|
const rotation = {
|
||||||
|
'bottom' : 0,
|
||||||
|
'top' : 180,
|
||||||
|
'left' : 90,
|
||||||
|
'right' : 270
|
||||||
|
}[side];
|
||||||
|
return dedent`
|
||||||
|
{{imageMaskEdge${_.random(1, 8)},--offset:0cm,--rotation:${rotation}
|
||||||
|
{height:100%}
|
||||||
|
}}
|
||||||
|
<!-- Use --offset to shift the mask toward or away from the page center.
|
||||||
|
Use --rotation to set rotation angle in degrees. -->\n\n`;
|
||||||
|
},
|
||||||
|
|
||||||
|
corner : (y = 'top', x = 'left')=>{
|
||||||
|
const offsetX = (x == 'left' ? '-50%' : '50%');
|
||||||
|
const offsetY = (y == 'top' ? '50%' : '-50%');
|
||||||
|
return dedent`
|
||||||
|
{{imageMaskCorner${_.random(1, 37)},--offsetX:${offsetX},--offsetY:${offsetY},--rotation:0
|
||||||
|
{height:100%}
|
||||||
|
}}
|
||||||
|
<!-- Use --offsetX to shift the mask left or right (can use cm instead of %)
|
||||||
|
Use --offsetY to shift the mask up or down
|
||||||
|
Use --rotation to set rotation angle in degrees. -->\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
()=>{
|
||||||
|
|
||||||
|
};
|
||||||
@@ -15,6 +15,21 @@ body {
|
|||||||
-webkit-print-color-adjust : exact;
|
-webkit-print-color-adjust : exact;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//*****************************
|
||||||
|
// * MUSTACHE DIVS/SPANS
|
||||||
|
// *****************************/
|
||||||
|
.page {
|
||||||
|
.block {
|
||||||
|
break-inside : avoid;
|
||||||
|
display : inline-block;
|
||||||
|
width : 100%;
|
||||||
|
}
|
||||||
|
.inline-block {
|
||||||
|
display : inline-block;
|
||||||
|
text-indent : initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.useColumns(@multiplier : 1, @fillMode: balance){
|
.useColumns(@multiplier : 1, @fillMode: balance){
|
||||||
column-fill : @fillMode;
|
column-fill : @fillMode;
|
||||||
column-count : 2;
|
column-count : 2;
|
||||||
@@ -39,6 +54,7 @@ body {
|
|||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
page-break-before : always;
|
page-break-before : always;
|
||||||
page-break-after : always;
|
page-break-after : always;
|
||||||
|
contain : size;
|
||||||
}
|
}
|
||||||
//*****************************
|
//*****************************
|
||||||
// * BASE
|
// * BASE
|
||||||
@@ -114,6 +130,50 @@ body {
|
|||||||
margin-top : 0; // have vertical spacing.
|
margin-top : 0; // have vertical spacing.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//************************************
|
||||||
|
// * CODE BLOCKS
|
||||||
|
// ************************************/
|
||||||
|
code{
|
||||||
|
font-family : "Courier New", Courier, monospace;
|
||||||
|
white-space : pre-wrap;
|
||||||
|
overflow-wrap : break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code{
|
||||||
|
width : 100%;
|
||||||
|
display : inline-block;
|
||||||
|
}
|
||||||
|
//*****************************
|
||||||
|
// * EXTRAS
|
||||||
|
// *****************************/
|
||||||
|
.columnSplit {
|
||||||
|
visibility : hidden;
|
||||||
|
-webkit-column-break-after : always;
|
||||||
|
break-after : always;
|
||||||
|
-moz-column-break-after : always;
|
||||||
|
margin-top : 0;
|
||||||
|
& + * {
|
||||||
|
margin-top : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Avoid breaking up
|
||||||
|
blockquote,table{
|
||||||
|
z-index : 15;
|
||||||
|
-webkit-column-break-inside : avoid;
|
||||||
|
page-break-inside : avoid;
|
||||||
|
break-inside : avoid;
|
||||||
|
}
|
||||||
|
// Nested lists
|
||||||
|
ul ul,ol ol,ul ol,ol ul{
|
||||||
|
margin-bottom : 0px;
|
||||||
|
margin-left : 1.5em;
|
||||||
|
}
|
||||||
|
li{
|
||||||
|
-webkit-column-break-inside : avoid;
|
||||||
|
page-break-inside : avoid;
|
||||||
|
break-inside : avoid;
|
||||||
|
}
|
||||||
|
|
||||||
/* Watermark */
|
/* Watermark */
|
||||||
.watermark {
|
.watermark {
|
||||||
display : grid !important;
|
display : grid !important;
|
||||||
@@ -167,64 +227,139 @@ body {
|
|||||||
.watercolor11 { --wc : @watercolor11; }
|
.watercolor11 { --wc : @watercolor11; }
|
||||||
.watercolor12 { --wc : @watercolor12; }
|
.watercolor12 { --wc : @watercolor12; }
|
||||||
|
|
||||||
//************************************
|
/* Image Masks */
|
||||||
// * CODE BLOCKS
|
[class*="imageMask"] {
|
||||||
// ************************************/
|
position : absolute;
|
||||||
code{
|
height : 200%;
|
||||||
font-family : "Courier New", Courier, monospace;
|
width : 200%;
|
||||||
white-space : pre-wrap;
|
left : 50%;
|
||||||
overflow-wrap : break-word;
|
bottom : 50%;
|
||||||
}
|
--rotation : 0;
|
||||||
|
--revealer : none;
|
||||||
pre code{
|
--checkerboard : none;
|
||||||
width : 100%;
|
--scaleX : 1;
|
||||||
display : inline-block;
|
--scaleY : 1;
|
||||||
}
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
//*****************************
|
-webkit-mask-repeat : repeat-x;
|
||||||
// * EXTRAS
|
-webkit-mask-size : 50%; //Scale only X to fit page width, leave height at aspect ratio, designed to hang off the edge
|
||||||
// *****************************/
|
-webkit-mask-position : 50% calc(50% - var(--offset));
|
||||||
.columnSplit {
|
mask-image : var(--wc);
|
||||||
visibility : hidden;
|
mask-repeat : repeat-x;
|
||||||
-webkit-column-break-after : always;
|
mask-size : 50%;
|
||||||
break-after : always;
|
mask-position : 50% calc(50% - var(--offset));
|
||||||
-moz-column-break-after : always;
|
background-image : var(--checkerboard);
|
||||||
margin-top : 0;
|
background-size : 20px;
|
||||||
& + * {
|
z-index : -1;
|
||||||
margin-top : 0;
|
transform : translateY(50%) translateX(-50%) rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));
|
||||||
|
transition : transform 2s;
|
||||||
|
& > p:has(img) {
|
||||||
|
position : absolute;
|
||||||
|
width : 50%;
|
||||||
|
height : 50%;
|
||||||
|
bottom : 50%;
|
||||||
|
left : 50%;
|
||||||
|
transform : translateX(-50%) translateY(50%) rotate(calc(-1deg * var(--rotation))) scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)));
|
||||||
|
transition : transform 2s;
|
||||||
|
}
|
||||||
|
& img {
|
||||||
|
position : absolute;
|
||||||
|
display : block;
|
||||||
|
bottom : 0;
|
||||||
|
}
|
||||||
|
&.bottom {
|
||||||
|
--rotation : 0;
|
||||||
|
& img {bottom: 0;}
|
||||||
|
}
|
||||||
|
&.top {
|
||||||
|
--rotation : 180;
|
||||||
|
& img {top: 0;}
|
||||||
|
}
|
||||||
|
&.left {
|
||||||
|
--rotation : 90;
|
||||||
|
& img {left: 0;}
|
||||||
|
}
|
||||||
|
&.right {
|
||||||
|
--rotation : -90;
|
||||||
|
& img {right: 0;}
|
||||||
|
}
|
||||||
|
&.revealImage {
|
||||||
|
--revealer : linear-gradient(0deg, rgba(0,0,0,.2) 0%, rgba(0,0,0,0.2));
|
||||||
|
--checkerboard : url(/assets/waterColorMasks/missingImage.png); //shows any masked regions not filled by image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//Avoid breaking up
|
|
||||||
blockquote,table{
|
|
||||||
z-index : 15;
|
|
||||||
-webkit-column-break-inside : avoid;
|
|
||||||
page-break-inside : avoid;
|
|
||||||
break-inside : avoid;
|
|
||||||
}
|
|
||||||
// Nested lists
|
|
||||||
ul ul,ol ol,ul ol,ol ul{
|
|
||||||
margin-bottom : 0px;
|
|
||||||
margin-left : 1.5em;
|
|
||||||
}
|
|
||||||
li{
|
|
||||||
-webkit-column-break-inside : avoid;
|
|
||||||
page-break-inside : avoid;
|
|
||||||
break-inside : avoid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//*****************************
|
.imageMaskEdge1 { --wc : url(/assets/waterColorMasks/edge/0001.webp); }
|
||||||
// * MUSTACHE DIVS/SPANS
|
.imageMaskEdge2 { --wc : url(/assets/waterColorMasks/edge/0002.webp); }
|
||||||
// *****************************/
|
.imageMaskEdge3 { --wc : url(/assets/waterColorMasks/edge/0003.webp); }
|
||||||
.page {
|
.imageMaskEdge4 { --wc : url(/assets/waterColorMasks/edge/0004.webp); }
|
||||||
.block {
|
.imageMaskEdge5 { --wc : url(/assets/waterColorMasks/edge/0005.webp); }
|
||||||
break-inside : avoid;
|
.imageMaskEdge6 { --wc : url(/assets/waterColorMasks/edge/0006.webp); }
|
||||||
display : inline-block;
|
.imageMaskEdge7 { --wc : url(/assets/waterColorMasks/edge/0007.webp); }
|
||||||
width : 100%;
|
.imageMaskEdge8 { --wc : url(/assets/waterColorMasks/edge/0008.webp); }
|
||||||
}
|
|
||||||
.inline-block {
|
[class*="imageMaskCorner"] {
|
||||||
display : inline-block;
|
height : 200%;
|
||||||
text-indent : initial;
|
width : 200%;
|
||||||
|
left : calc(-50% + var(--offsetX));
|
||||||
|
bottom : calc(-50% + var(--offsetY));
|
||||||
|
-webkit-mask-image : var(--wc), var(--revealer);
|
||||||
|
-webkit-mask-repeat : no-repeat;
|
||||||
|
-webkit-mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
|
-webkit-mask-position : 50% 50%;
|
||||||
|
mask-image : var(--wc), var(--revealer);
|
||||||
|
mask-repeat : no-repeat;
|
||||||
|
mask-size : 100% 100%; //Scale both dimensions to fit page size
|
||||||
|
mask-position : 50% 50%;
|
||||||
|
transform : rotate(calc(1deg * var(--rotation))) scaleX(var(--scaleX)) scaleY(var(--scaleY));;
|
||||||
|
& > p:has(img) {
|
||||||
|
width : 50%;
|
||||||
|
height : 50%; //Complex transform below to handle mix of % and cm offsets
|
||||||
|
left : 25%;
|
||||||
|
bottom : 25%;
|
||||||
|
transform : scaleX(calc(1 / var(--scaleX))) scaleY(calc(1 / var(--scaleY)))
|
||||||
|
rotate(calc(-1deg * var(--rotation)))
|
||||||
|
translateX(calc(-1 * var(--offsetX)))
|
||||||
|
translateY(calc(1 * var(--offsetY)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.imageMaskCorner1 { --wc : url(/assets/waterColorMasks/corner/0001.webp); }
|
||||||
|
.imageMaskCorner2 { --wc : url(/assets/waterColorMasks/corner/0002.webp); }
|
||||||
|
.imageMaskCorner3 { --wc : url(/assets/waterColorMasks/corner/0003.webp); }
|
||||||
|
.imageMaskCorner4 { --wc : url(/assets/waterColorMasks/corner/0004.webp); }
|
||||||
|
.imageMaskCorner5 { --wc : url(/assets/waterColorMasks/corner/0005.webp); }
|
||||||
|
.imageMaskCorner6 { --wc : url(/assets/waterColorMasks/corner/0006.webp); }
|
||||||
|
.imageMaskCorner7 { --wc : url(/assets/waterColorMasks/corner/0007.webp); }
|
||||||
|
.imageMaskCorner8 { --wc : url(/assets/waterColorMasks/corner/0008.webp); }
|
||||||
|
.imageMaskCorner9 { --wc : url(/assets/waterColorMasks/corner/0009.webp); }
|
||||||
|
.imageMaskCorner10 { --wc : url(/assets/waterColorMasks/corner/0010.webp); }
|
||||||
|
.imageMaskCorner11 { --wc : url(/assets/waterColorMasks/corner/0011.webp); }
|
||||||
|
.imageMaskCorner12 { --wc : url(/assets/waterColorMasks/corner/0012.webp); }
|
||||||
|
.imageMaskCorner13 { --wc : url(/assets/waterColorMasks/corner/0013.webp); }
|
||||||
|
.imageMaskCorner14 { --wc : url(/assets/waterColorMasks/corner/0014.webp); }
|
||||||
|
.imageMaskCorner15 { --wc : url(/assets/waterColorMasks/corner/0015.webp); }
|
||||||
|
.imageMaskCorner16 { --wc : url(/assets/waterColorMasks/corner/0016.webp); }
|
||||||
|
.imageMaskCorner17 { --wc : url(/assets/waterColorMasks/corner/0017.webp); }
|
||||||
|
.imageMaskCorner18 { --wc : url(/assets/waterColorMasks/corner/0018.webp); }
|
||||||
|
.imageMaskCorner19 { --wc : url(/assets/waterColorMasks/corner/0019.webp); }
|
||||||
|
.imageMaskCorner20 { --wc : url(/assets/waterColorMasks/corner/0020.webp); }
|
||||||
|
.imageMaskCorner21 { --wc : url(/assets/waterColorMasks/corner/0021.webp); }
|
||||||
|
.imageMaskCorner22 { --wc : url(/assets/waterColorMasks/corner/0022.webp); }
|
||||||
|
.imageMaskCorner23 { --wc : url(/assets/waterColorMasks/corner/0023.webp); }
|
||||||
|
.imageMaskCorner24 { --wc : url(/assets/waterColorMasks/corner/0024.webp); }
|
||||||
|
.imageMaskCorner25 { --wc : url(/assets/waterColorMasks/corner/0025.webp); }
|
||||||
|
.imageMaskCorner26 { --wc : url(/assets/waterColorMasks/corner/0026.webp); }
|
||||||
|
.imageMaskCorner27 { --wc : url(/assets/waterColorMasks/corner/0027.webp); }
|
||||||
|
.imageMaskCorner28 { --wc : url(/assets/waterColorMasks/corner/0028.webp); }
|
||||||
|
.imageMaskCorner29 { --wc : url(/assets/waterColorMasks/corner/0029.webp); }
|
||||||
|
.imageMaskCorner30 { --wc : url(/assets/waterColorMasks/corner/0030.webp); }
|
||||||
|
.imageMaskCorner31 { --wc : url(/assets/waterColorMasks/corner/0031.webp); }
|
||||||
|
.imageMaskCorner32 { --wc : url(/assets/waterColorMasks/corner/0032.webp); }
|
||||||
|
.imageMaskCorner33 { --wc : url(/assets/waterColorMasks/corner/0033.webp); }
|
||||||
|
.imageMaskCorner34 { --wc : url(/assets/waterColorMasks/corner/0034.webp); }
|
||||||
|
.imageMaskCorner35 { --wc : url(/assets/waterColorMasks/corner/0035.webp); }
|
||||||
|
.imageMaskCorner36 { --wc : url(/assets/waterColorMasks/corner/0036.webp); }
|
||||||
|
.imageMaskCorner37 { --wc : url(/assets/waterColorMasks/corner/0037.webp); }
|
||||||
}
|
}
|
||||||
|
|
||||||
//*****************************
|
//*****************************
|
||||||
|
|||||||
BIN
themes/V3/Journal/dropdownPreview.png
Normal file
|
After Width: | Height: | Size: 809 KiB |
@@ -10,6 +10,9 @@
|
|||||||
@monsterBorderImage : url('/assets/monsterBorderFancy.png');
|
@monsterBorderImage : url('/assets/monsterBorderFancy.png');
|
||||||
@codeBorderImage : url('/assets/codeBorder.png');
|
@codeBorderImage : url('/assets/codeBorder.png');
|
||||||
@classTableDecoration : url('/assets/classTableDecoration.png');
|
@classTableDecoration : url('/assets/classTableDecoration.png');
|
||||||
|
@naturalCritLogo : url('/assets/naturalCritLogo.svg');
|
||||||
|
@coverPageBanner : url('/assets/coverPageBanner.svg');
|
||||||
|
@horizontalRule : url('/assets/horizontalRule.svg');
|
||||||
|
|
||||||
// Watercolor Images
|
// Watercolor Images
|
||||||
@watercolor1 : url('/assets/watercolor/watercolor1.png');
|
@watercolor1 : url('/assets/watercolor/watercolor1.png');
|
||||||
|
|||||||
1
themes/assets/coverPageBanner.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 970.25 154.67"><defs><style>.cls-1{fill:#ed1f24;}</style></defs><title>Asset 2</title><g id="Layer_2" data-name="Layer 2"><g id="banner"><path id="mainShape" class="cls-1" d="M0,154.67V6.42l188.38,0s137.26,9.45,141.43,9.79c0,.14-96.52,6.76-96.52,6.76l47.09,2.24,39.48-3.05L479.43,34s-36.75,2.68-50.19,3.71c0,.29,178.19,14.48,178.19,14.48s34-3.95,34-4.24c-22.33-1.59-74.72-5.66-74.72-5.66l94-6.19s33.14,2.2,46.39,3.18c12.39.92,40.61,2.82,40.61,2.82l-24.34,2.07,35.24,3,47.36-3.62L762.5,40.26l95-6.94s84.83,6.11,96.45,7c0,.06-65.05,4.32-92.07,6.19-.29,0,39.6,3.57,54.16,4.73,0,.19-108.33,6.75-159.44,10.21,40.47,4.1,86.29,8.6,126,12.62,0,.15-41,4.34-58.14,6.16,0,.21,65.56,5.15,93.67,7.41-.2,0-41.27,2.79-56.81,4,0,.23,44.76,3.66,62.76,5.09,0,.11-103.55,7.17-150.95,10.53,0,.25,47.29,3.67,66.36,5.19,0,.1-62.19,4.45-89.84,6.47,0,.27,10.77,2.61,10.77,2.61L532.16,139.22,459.29,134l44.38-3.57L434.1,125l-58.43,4.31,59.76,4.36-123.38,11,44.19,3.14S337.9,149.34,330,150c-9.83.77-59.3,4.72-59.3,4.72Z"/><polygon id="diamondF" class="cls-1" points="552.6 154.33 469.43 147.19 550.72 142.21 633.24 148.52 552.6 154.33"/><polygon id="diamondE" class="cls-1" points="631.14 140.38 741.62 130.66 782.71 133.62 755.63 135.82 812.67 140.14 722.1 146.81 631.14 140.38"/><polygon id="diamondD" class="cls-1" points="812.57 62.87 892.92 57.13 970.25 63.1 894.05 68.76 812.57 62.87"/><path id="diamondC" class="cls-1" d="M480.21,29.59c4.21-.38,71.22-4.68,71.22-4.68l67.15,4.94-68.22,4.87S480.22,29.68,480.21,29.59Z"/><polygon id="diamondB" class="cls-1" points="450.19 23.52 344.58 16.3 449.17 8.62 556.38 16.36 450.19 23.52"/><polygon id="diamondA" class="cls-1" points="297.87 0 350.13 3.9 296.54 7.79 241.43 3.9 297.87 0"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
themes/assets/dragonBackground.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
1
themes/assets/horizontalRule.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 762.29 18.4"><defs><style>.cls-1{fill:#ed1f24;}</style></defs><title>Asset 2</title><g id="Layer_2" data-name="Layer 2"><g id="horizontalRule"><path id="mainShape" class="cls-1" d="M0,9.06S406.1,0,381.53,0,762.29,8.7,762.29,8.7s-350.49,10-381.53,9.69S0,9.06,0,9.06Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 335 B |
1
themes/assets/naturalCritLogo.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94.65 94.6"><defs><style>.cls-1{fill:#ed1f24;}</style></defs><title>NaturalCritLogo</title><g id="Layer_2" data-name="Layer 2"><g id="base"><path id="D20" class="cls-1" d="M63.45.09s-45.91,12.4-46,12.45a.71.71,0,0,0-.15.08l-.15.1-.12.11a1.07,1.07,0,0,0-.14.16l-.09.11-.12.23,0,.06L.2,54.9a1.59,1.59,0,0,0,.11,1.69L29.36,94h0l0,0,.08.08.08.08.09.09.08.06.13.07a0,0,0,0,0,0,0,1.59,1.59,0,0,0,.27.12l.13.05.06,0a1.55,1.55,0,0,0,.37,0,1.63,1.63,0,0,0,.31,0l45.67-8.3.16,0,.11,0,.12,0,.06,0s0,0,0,0l.06,0a1.65,1.65,0,0,0,.36-.28l0-.06a1.6,1.6,0,0,0,.26-.38s0,0,0,0v0h0a.14.14,0,0,1,0-.06L94.52,43.74a1.4,1.4,0,0,0,.11-.4.41.41,0,0,0,0-.11,1.13,1.13,0,0,0,0-.26.66.66,0,0,0,0-.14,2,2,0,0,0-.06-.26l0-.11a2.68,2.68,0,0,0-.18-.33v0L65.29.6C64.77-.31,63.45.09,63.45.09ZM74.9,81.7l-28.81-18L78.5,38.49ZM44.1,61l-11-40.17L77,35.39ZM82,37.78l8.92,5.95L79,73.48Zm4.46-1.1-4.6-3.06L75.69,21.36Zm-9.26-4.8-42.07-14,28.05-14ZM30.56,16.34l-6.49-2.16L47.85,7.7Zm-11.35-.21L27.88,19,7.64,45Zm10.73,5.76L40.78,61.64,4.64,54.42Zm10.82,43.2L30.26,89.6,5.75,58.09Zm3.16,1.24L71.74,83.72l-38.26,7Z"/></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
themes/assets/waterColorMasks/corner/0001.webp
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
themes/assets/waterColorMasks/corner/0002.webp
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
themes/assets/waterColorMasks/corner/0003.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
themes/assets/waterColorMasks/corner/0004.webp
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
themes/assets/waterColorMasks/corner/0005.webp
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
themes/assets/waterColorMasks/corner/0006.webp
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
themes/assets/waterColorMasks/corner/0007.webp
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
themes/assets/waterColorMasks/corner/0008.webp
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
themes/assets/waterColorMasks/corner/0009.webp
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
themes/assets/waterColorMasks/corner/0010.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
themes/assets/waterColorMasks/corner/0011.webp
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
themes/assets/waterColorMasks/corner/0012.webp
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
themes/assets/waterColorMasks/corner/0013.webp
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
themes/assets/waterColorMasks/corner/0014.webp
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
themes/assets/waterColorMasks/corner/0015.webp
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
themes/assets/waterColorMasks/corner/0016.webp
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
themes/assets/waterColorMasks/corner/0017.webp
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
themes/assets/waterColorMasks/corner/0018.webp
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
themes/assets/waterColorMasks/corner/0019.webp
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
themes/assets/waterColorMasks/corner/0020.webp
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
themes/assets/waterColorMasks/corner/0021.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
themes/assets/waterColorMasks/corner/0022.webp
Normal file
|
After Width: | Height: | Size: 334 KiB |
BIN
themes/assets/waterColorMasks/corner/0023.webp
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
themes/assets/waterColorMasks/corner/0024.webp
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
themes/assets/waterColorMasks/corner/0025.webp
Normal file
|
After Width: | Height: | Size: 174 KiB |
BIN
themes/assets/waterColorMasks/corner/0026.webp
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
themes/assets/waterColorMasks/corner/0027.webp
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
themes/assets/waterColorMasks/corner/0028.webp
Normal file
|
After Width: | Height: | Size: 319 KiB |