0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-26 22:43:07 +00:00

Merge pull request #1342 from naturalcrit/master

v2.11.2
This commit is contained in:
Trevor Buckner
2021-05-02 22:15:43 -04:00
committed by GitHub
32 changed files with 1481 additions and 1780 deletions

69
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 99
ignore:
- dependency-name: eslint
versions:
- 7.19.0
- 7.22.0
- 7.23.0
- 7.24.0
- dependency-name: "@babel/core"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.13
- 7.13.14
- 7.13.15
- dependency-name: googleapis
versions:
- 68.0.0
- 70.0.0
- 71.0.0
- dependency-name: "@babel/preset-env"
versions:
- 7.12.13
- 7.12.16
- 7.12.17
- 7.13.0
- 7.13.12
- 7.13.8
- dependency-name: mongoose
versions:
- 5.11.14
- 5.11.15
- 5.11.16
- 5.11.17
- 5.11.18
- 5.11.19
- 5.12.1
- 5.12.2
- 5.12.3
- dependency-name: eslint-plugin-react
versions:
- 7.23.0
- 7.23.1
- dependency-name: query-string
versions:
- 7.0.0
- dependency-name: nanoid
versions:
- 3.1.22
- dependency-name: "@babel/preset-react"
versions:
- 7.13.13
- dependency-name: codemirror
versions:
- 5.59.3
- 5.60.0
- dependency-name: classnames
versions:
- 2.3.0
- dependency-name: marked
versions:
- 1.2.8

View File

@@ -6,6 +6,11 @@ h5 {
# changelog
### Saturday, 02/5/2021 - v2.11.2
- Fix for edge case where brews could accidentally transfer from Google Drive back to Homebrewery.
- Move cursor to end of snippet after insertion
### Saturday, 20/3/2021 - v2.11.1
- Warning when opening brew in your Google Drive trash
@@ -52,10 +57,6 @@ h5 {
### Monday, 19/10/2020 - v2.10.2
- Fixed issue with "recent" item links not updating when transferring between Google Drive.
```
```
### Monday, 12/10/2020 - v2.10.1
- Fixed issue with users unable to create new brews
- Fixing brews being lost when loaded via back button
@@ -73,6 +74,13 @@ h5 {
### Wednesday, 20/05/2020 - v2.9.0
- Major refactoring of site backend to work with updated dependencies for security (should be invisible to users)
\page
### Wednesday, 11/03/2020 - v2.8.2
- Fixed delete button removing everyone's copy for brews with multiple authors
- Compressed homebrew text in database
@@ -100,9 +108,6 @@ h5 {
### Saturday, 22/04/2017 - v2.7.4
- Give ability to hide the render warning notification
\page
### Friday, 03/03/2017 - v2.7.3
- Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews.
@@ -144,9 +149,6 @@ h5 {
- Added in a snippet for a split table
- Added an account nav item to new page
```
```
### Sunday, 27/11/2016 - v2.5.1
- Fixed the column rendering on the new user page. Really should have tested that better
- Added a hover tooltip to fully read the brew description
@@ -167,6 +169,8 @@ h5 {
- You can now print from a new page without saving
- Added the ability to use ctrl+p and ctrl+s to print and save respectively.
\page
### Monday, 07/11/2016
- Added final touches to the html validator and updating the rest of the branch
- If anyone finds issues with the new HTML validator, please let me know. I hope this will bring a more consistent feel to Homebrewery rendering.
@@ -186,8 +190,6 @@ h5 {
- Fixed the noteblock overlapping into titles (thanks u/dsompura!)
- Fixed a bad search route in the admin panel (thanks u/SnappyTom!)
\page
### Friday, 29/07/2016 - v2.2.7
- Adding in descriptive note blocks. (Thanks calculuschild!)

View File

@@ -64,7 +64,7 @@ const Editor = createClass({
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
this.handleTextChange(lines.join('\n'));
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line + injectText.split('\n').length, this.cursorPosition.ch + injectText.length);
},
handgleToggle : function(){
this.setState({

View File

@@ -88,7 +88,7 @@ module.exports = {
`- **Components:** ${components}`,
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
'',
'A flame, equivalent in brightness to a torch, springs from from an object that you touch. ',
'A flame, equivalent in brightness to a torch, springs from an object that you touch. ',
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
'A *continual flame* can be covered or hidden but not smothered or quenched.',
'\n\n\n'

View File

@@ -1,4 +1,5 @@
const _ = require('lodash');
const dedent = require('dedent-tabs').default;
const genList = function(list, max){
return _.sampleSize(list, _.random(0, max)).join(', ') || 'None';
@@ -86,7 +87,7 @@ const getAlignment = function(){
};
const getStats = function(){
return `>|${_.times(6, function(){
return `|${_.times(6, function(){
const num = _.random(1, 20);
const mod = Math.ceil(num/2 - 5);
return `${num} (${mod >= 0 ? `+${mod}` : mod})`;
@@ -95,12 +96,12 @@ const getStats = function(){
const genAbilities = function(){
return _.sample([
'> ***Pack Tactics.*** These guys work together. Like super well, you don\'t even know.',
'> ***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
'> ***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
'> ***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
'> ***Sassiness.*** When questioned, this creature will talk back instead of answering.',
'> ***Big Jerk.*** Thinks he is just *waaaay* better than you.',
'***Pack Tactics.*** These guys work together like peanut butter and jelly.',
'***Fowl Appearance.*** While the creature remains motionless, it is indistinguishable from a normal chicken.',
'***Onion Stench.*** Any creatures within 5 feet of this thing develops an irrational craving for onion rings.',
'***Enormous Nose.*** This creature gains advantage on any check involving putting things in its nose.',
'***Sassiness.*** When questioned, this creature will talk back instead of answering.',
'***Big Jerk.*** Whenever this creature makes an attack, it starts telling you how much cooler it is than you.',
]);
};
@@ -133,68 +134,37 @@ const genAction = function(){
'Turnbuckle Roll'
]);
return `> ***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
return `***${name}.*** *Melee Weapon Attack:* +4 to hit, reach 5ft., one target. *Hit* 5 (1d6 + 2) `;
};
module.exports = {
full : function(){
return `${[
'___',
'___',
`> ## ${getMonsterName()}`,
`>*${getType()}, ${getAlignment()}*`,
'> ___',
`> - **Armor Class** ${_.random(10, 20)}`,
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
`> - **Speed** ${_.random(0, 50)}ft.`,
'>___',
'>|STR|DEX|CON|INT|WIS|CHA|',
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
getStats(),
'>___',
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
`> - **Senses** passive Perception ${_.random(3, 20)}`,
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
'> ___',
_.times(_.random(3, 6), function(){
return genAbilities();
}).join('\n>\n'),
'> ### Actions',
_.times(_.random(4, 6), function(){
return genAction();
}).join('\n>\n'),
].join('\n')}\n\n\n`;
},
half : function(){
return `${[
'___',
`> ## ${getMonsterName()}`,
`>*${getType()}, ${getAlignment()}*`,
'> ___',
`> - **Armor Class** ${_.random(10, 20)}`,
`> - **Hit Points** ${_.random(1, 150)}(1d4 + 5)`,
`> - **Speed** ${_.random(0, 50)}ft.`,
'>___',
'>|STR|DEX|CON|INT|WIS|CHA|',
'>|:---:|:---:|:---:|:---:|:---:|:---:|',
getStats(),
'>___',
`> - **Condition Immunities** ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}`,
`> - **Senses** passive Perception ${_.random(3, 20)}`,
`> - **Languages** ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}`,
`> - **Challenge** ${_.random(0, 15)} (${_.random(10, 10000)} XP)`,
'> ___',
_.times(_.random(2, 3), function(){
return genAbilities();
}).join('\n>\n'),
'> ### Actions',
_.times(_.random(1, 2), function(){
return genAction();
}).join('\n>\n'),
].join('\n')}\n\n\n`;
monster : function(classes, genLines){
return dedent`
{{${classes}
## ${getMonsterName()}
*${getType()}, ${getAlignment()}*
___
: **Armor Class** : ${_.random(10, 20)} (chain mail, shield)
: **Hit Points** : ${_.random(1, 150)}(1d4 + 5)
: **Speed** : ${_.random(0, 50)}ft.
___
| STR | DEX | CON | INT | WIS | CHA |
|:-----:|:-----:|:-----:|:-----:|:-----:|:-----:|
${getStats()}
___
: **Condition Immunities** : ${genList(['groggy', 'swagged', 'weak-kneed', 'buzzed', 'groovy', 'melancholy', 'drunk'], 3)}
: **Senses** : darkvision 60 ft., passive Perception ${_.random(3, 20)}
: **Languages** : ${genList(['Common', 'Pottymouth', 'Gibberish', 'Latin', 'Jive'], 2)}
: **Challenge** : ${_.random(0, 15)} (${_.random(10, 10000)} XP)
___
:
${_.times(_.random(genLines, genLines + 2), function(){return genAbilities();}).join('\n\t\t\t\n\t\t\t')}
:
### Actions
${_.times(_.random(genLines, genLines + 2), function(){return genAction();}).join('\n\t\t\t\n\t\t\t')}
}}
\n`;
}
};

View File

@@ -6,6 +6,7 @@ const MonsterBlockGen = require('./monsterblock.gen.js');
const ClassFeatureGen = require('./classfeature.gen.js');
const CoverPageGen = require('./coverpage.gen.js');
const TableOfContentsGen = require('./tableOfContents.gen.js');
const dedent = require('dedent-tabs').default;
module.exports = [
@@ -151,15 +152,20 @@ module.exports = [
].join('\n');
},
},
{
name : 'Monster Stat Block (unframed)',
icon : 'fas fa-paw',
gen : MonsterBlockGen.monster('monster', 2),
},
{
name : 'Monster Stat Block',
icon : 'fas fa-spider',
gen : MonsterBlockGen.half,
gen : MonsterBlockGen.monster('monster,frame', 2),
},
{
name : 'Wide Monster Stat Block',
icon : 'fas fa-dragon',
gen : MonsterBlockGen.full,
gen : MonsterBlockGen.monster('monster,frame,wide', 4),
},
{
name : 'Cover Page',
@@ -196,63 +202,61 @@ module.exports = [
name : 'Table',
icon : 'fas fa-th-list',
gen : function(){
return [
'##### Cookie Tastiness',
'| Tastiness | Cookie Type |',
'|:----:|:-------------|',
'| -5 | Raisin |',
'| 8th | Chocolate Chip |',
'| 11th | 2 or lower |',
'| 14th | 3 or lower |',
'| 17th | 4 or lower |\n\n',
].join('\n');
},
return dedent`
##### Character Advancement
| Experience Points | Level | Proficiency Bonus |
|:------------------|:-----:|:-----------------:|
| 0 | 1 | +2 |
| 300 | 2 | +2 |
| 900 | 3 | +2 |
| 2,700 | 4 | +2 |
| 6,500 | 5 | +3 |
| 14,000 | 6 | +3 |
\n`;
}
},
{
name : 'Wide Table',
icon : 'fas fa-list',
gen : function(){
return [
'<div class=\'wide\'>',
'##### Cookie Tastiness',
'| Tastiness | Cookie Type |',
'|:----:|:-------------|',
'| -5 | Raisin |',
'| 8th | Chocolate Chip |',
'| 11th | 2 or lower |',
'| 14th | 3 or lower |',
'| 17th | 4 or lower |',
'</div>\n\n'
].join('\n');
},
return dedent`
{{wide
##### Weapons
| Name | Cost | Damage | Weight | Properties |
|:------------------------|:-----:|:----------------|--------:|:-----------|
| *Simple Melee Weapons* | | | | |
| &emsp; Club | 1 sp | 1d4 bludgeoning | 2 lb. | Light |
| &emsp; Dagger | 2 gp | 1d4 piercing | 1 lb. | Finesse |
| &emsp; Spear | 1 gp | 1d6 piercing | 3 lb. | Thrown |
| *Simple Ranged Weapons* | | | | |
| &emsp; Dart | 5 cp | 1d4 piercig | 1/4 lb. | Finesse |
| &emsp; Shortbow | 25 gp | 1d6 piercing | 2 lb. | Ammunition |
| &emsp; Sling | 1 sp | 1d4 bludgeoning | &mdash; | Ammunition |
}}
\n`;
}
},
{
name : 'Split Table',
icon : 'fas fa-th-large',
gen : function(){
return [
'<div style=\'column-count:2\'>',
'| d10 | Damage Type |',
'|:---:|:------------|',
'| 1 | Acid |',
'| 2 | Cold |',
'| 3 | Fire |',
'| 4 | Force |',
'| 5 | Lightning |',
'',
'```',
'```',
'',
'| d10 | Damage Type |',
'|:---:|:------------|',
'| 6 | Necrotic |',
'| 7 | Poison |',
'| 8 | Psychic |',
'| 9 | Radiant |',
'| 10 | Thunder |',
'</div>\n\n',
].join('\n');
},
return dedent`
##### Typical Difficulty Classes
{{column-count="2"
| Task Difficulty | DC |
|:----------------|:--:|
| Very easy | 5 |
| Easy | 10 |
| Medium | 15 |
| Task Difficulty | DC |
|:------------------|:--:|
| Hard | 20 |
| Very hard | 25 |
| Nearly impossible | 30 |
}}
\n`;
}
}
]
},

View File

@@ -82,7 +82,7 @@ module.exports = {
`- **Components:** ${components}`,
`- **Duration:** ${_.sample(['Until dispelled', '1 round', 'Instantaneous', 'Concentration, up to 10 minutes', '1 hour'])}`,
'',
'A flame, equivalent in brightness to a torch, springs from from an object that you touch. ',
'A flame, equivalent in brightness to a torch, springs from an object that you touch. ',
'The effect look like a regular flame, but it creates no heat and doesn\'t use oxygen. ',
'A *continual flame* can be covered or hidden but not smothered or quenched.',
'\n\n\n'

View File

@@ -55,6 +55,7 @@ const EditPage = createClass({
isSaving : false,
isPending : false,
alertTrashedGoogleBrew : this.props.brew.trashed,
alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
errors : null,
@@ -140,15 +141,25 @@ const EditPage = createClass({
},
handleGoogleClick : function(){
console.log('handlegoogleclick');
if(!global.account?.googleId) {
this.setState({
alertLoginToTransfer : true
});
return;
}
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}));
this.clearErrors();
},
closeAlerts : function(){
closeAlerts : function(event){
event.stopPropagation(); //Only handle click once so alert doesn't reopen
this.setState({
alertTrashedGoogleBrew : false
alertTrashedGoogleBrew : false,
alertLoginToTransfer : false,
confirmGoogleTransfer : false
});
},
@@ -187,7 +198,7 @@ const EditPage = createClass({
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
: 'Error Saving to Google!');
: 'Error Transferring to Google!');
this.setState({ errors: err, saveGoogle: false });
});
@@ -210,7 +221,7 @@ const EditPage = createClass({
console.log(err.status === 401
? 'Not signed in!'
: 'Error Saving to Google!');
this.setState({ errors: err, saveGoogle: false });
this.setState({ errors: err });
return;
});
@@ -260,39 +271,44 @@ const EditPage = createClass({
},
renderGoogleDriveIcon : function(){
if(this.state.saveGoogle) {
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
<img src={googleDriveActive} alt='googleDriveActive' />
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
{this.state.saveGoogle
? <img src={googleDriveActive} alt='googleDriveActive'/>
: <img src={googleDriveInactive} alt='googleDriveInactive'/>
}
{this.state.confirmGoogleTransfer &&
<div className='errorContainer'>
Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?<br />
<div className='confirm' onClick={this.toggleGoogleStorage}>
Yes
</div>
<div className='deny'>
No
</div>
{this.state.confirmGoogleTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
{ this.state.saveGoogle
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
}
<br />
<div className='confirm' onClick={this.toggleGoogleStorage}>
Yes
</div>
}
</Nav.item>;
} else {
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
<img src={googleDriveInactive} alt='googleDriveInactive' />
<div className='deny'>
No
</div>
</div>
}
{this.state.confirmGoogleTransfer &&
<div className='errorContainer'>
Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?<br />
<div className='confirm' onClick={this.toggleGoogleStorage}>
Yes
</div>
<div className='deny'>
No
{this.state.alertLoginToTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
You must be signed in to a Google account to transfer
between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer'
href={`http://naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
</a>
<div className='deny'>
Not Now
</div>
}
</Nav.item>;
}
</div>
}
</Nav.item>;
},
renderSaveButton : function(){
@@ -311,7 +327,7 @@ const EditPage = createClass({
to save this to<br />Google Drive!<br />
<a target='_blank' rel='noopener noreferrer'
href={`http://naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm' onClick={this.toggleGoogleStorage}>
<div className='confirm'>
Sign In
</div>
</a>

View File

@@ -43,14 +43,14 @@ const NewPage = createClass({
getInitialState : function() {
return {
brew : {
text : this.props.brew.text,
text : this.props.brew.text || '',
gDrive : false,
title : '',
description : '',
tags : '',
title : this.props.brew.title || '',
description : this.props.brew.description || '',
tags : this.props.brew.tags || '',
published : false,
authors : [],
systems : []
systems : this.props.brew.systems || []
},
isSaving : false,

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 864 B

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@
@headerUnderline : #c9ad6a;
@horizontalRule : #9c2b1b;
@headerText : #58180D;
@monsterStatBackground : #FDF1DC;
@monsterStatBackground : #EEDBAB;
@page { margin: 0; }
body {
counter-reset : phb-page-numbers;
@@ -17,7 +17,11 @@ body {
}
.useSansSerif(){
font-family : ScalySansRemake;
font-size : 10pt;
font-size : 0.325cm;
line-height : 1.2em;
p,dl,ul {
line-height : 1.2em;
}
em{
font-style : italic;
}
@@ -29,14 +33,14 @@ body {
.useColumns(@multiplier : 1){
column-count : 2;
column-fill : auto;
column-gap : 1cm;
column-gap : 0.9cm;
column-width : 8cm * @multiplier;
-webkit-column-count : 2;
-moz-column-count : 2;
-webkit-column-width : 8cm * @multiplier;
-moz-column-width : 8cm * @multiplier;
-webkit-column-gap : 1cm;
-moz-column-gap : 1cm;
-webkit-column-gap : 0.9cm;
-moz-column-gap : 0.9cm;
}
.phb3{
.useColumns();
@@ -47,11 +51,11 @@ body {
overflow : hidden;
height : 279.4mm;
width : 215.9mm;
padding : 1.0cm 1.7cm 1.5cm;
padding : 1.4cm 1.9cm 1.7cm;
background-color : @background;
background-image : @backgroundImage;
font-family : BookInsanityRemake;
font-size : 0.317cm;
font-size : 0.34cm;
text-rendering : optimizeLegibility;
page-break-before : always;
page-break-after : always;
@@ -59,11 +63,11 @@ body {
// * BASE
// *****************************/
p{
overflow-wrap : break-word;
padding-top : 0em;
line-height : 1.3em;
overflow-wrap : break-word; //TODO: MAKE ALL MARGINS TOP-ONLY. USE * + * STYLE SELECTORS
margin-bottom : 1em;
line-height : 1.3em;
&+p{
padding-top : 0em;
margin-top : -1em;
}
}
ul{
@@ -89,7 +93,7 @@ body {
}
strong{
font-weight : bold;
letter-spacing : 0.03em;
letter-spacing : -0.02em;
}
em{
font-style : italic;
@@ -108,15 +112,14 @@ body {
// * HEADERS
// *****************************/
h1,h2,h3,h4{
margin-top : 0.2em;
margin-bottom : 0.2em;
font-family : MrEavesRemake;
font-weight : 800;
color : @headerText;
}
h1{
margin-bottom : 0.18cm;
column-span : all;
font-size : 0.987cm;
font-size : 0.89cm;
-webkit-column-span : all;
-moz-column-span : all;
&+p::first-letter{
@@ -136,23 +139,28 @@ body {
color: rgba(0, 0, 0, 0);
}
&+p::first-line{
font-size : .385cm;
font-variant : small-caps;
}
}
h2{
font-size : 0.705cm;
margin-top : 0px;
margin-bottom : 0.05cm;
font-size : 0.75cm;
}
h3{
font-size : 0.529cm;
margin-top : -0.1cm;
margin-bottom : 0.1cm;
font-size : 0.575cm;
border-bottom : 2px solid @headerUnderline;
}
h4{
margin-bottom : 0.00em;
margin-top : -0.02cm;
margin-bottom : 0.02cm;
font-size : 0.458cm;
}
h5{
margin-bottom : 0.2em;
margin-top : -0.02cm;
margin-bottom : 0.02cm;
font-family : ScalySansSmallCapsRemake;
font-size : 0.423cm;
font-weight : 900;
@@ -164,21 +172,18 @@ body {
.useSansSerif();
width : 100%;
margin-bottom : 1em;
font-size : 10pt;
thead{
display: table-row-group;
font-weight : 800;
th{
vertical-align : bottom;
padding-bottom : 0.3em;
padding-right : 0.1em;
padding-left : 0.1em;
padding : 0.14em 0.4em;
}
}
tbody{
tr{
td{
padding : 0.3em 0.1em;
padding : 0.14em 0.4em;
}
&:nth-child(odd){
background-color : @noteGreen;
@@ -213,66 +218,101 @@ body {
//*****************************
// * MONSTER STAT BLOCK
// *****************************/
hr+blockquote{
position : relative;
padding-top : 15px;
background-color : @monsterStatBackground;
border-style : solid;
border-width : 10px;
border-image : @monsterBorderImage 10;
.monster {
&.frame {
border-style : solid;
border-width : 7px 6px;
background-color : @monsterStatBackground;
background-image : @monsterBlockBackground;
border-image : @monsterBorderImage 14 round;
border-image-outset : 0px 2px;
background-blend-mode : overlay;
background-attachment : fixed;
box-shadow : 1px 4px 14px #888;
padding : 4px 2px;
margin : 0px -6px 1em;
}
.useSansSerif();
//-webkit-transform : translateZ(0); //Prevents shadows from breaking across columns, but breaks internal columns...
position : relative;
padding : 0px;
margin-bottom : 1em;
p{
margin-bottom : 0.3cm;
}
p+p {
margin-top : 0; //May not be needed
text-indent : 0;
}
p:last-of-type {
margin-bottom: 0;
}
//Headers
h2{
margin-top : -8px;
margin-bottom : 0px;
&+p{
padding-bottom : 0px;
font-size : 0.62cm;
line-height : 1em;
margin : 0;
&+p {
font-size : 0.304cm; //Monster size and type subtext
margin-bottom : 0;
}
}
h3{
font-family : ScalySansRemake;
font-weight : 400;
border-bottom : 1px solid @headerText;
}
hr+ul{
color : @headerText;
}
ul{
.useSansSerif();
padding-left : 1em;
font-size : 0.352cm;
}
// Monster Ability table
hr+table{
margin : 0;
column-span : 1;
background-color : transparent;
border-style : none;
border-image : none;
-webkit-column-span : 1;
tbody{
tr:nth-child(odd), tr:nth-child(even){
background-color : transparent;
}
}
}
table{
color : @headerText;
}
p+p{
margin-top : 0em;
padding-bottom : 0.5em;
text-indent : 0em;
font-family : ScalySansRemake;
font-weight : 800;
font-variant : small-caps;
border-bottom : 2px solid @headerText;
margin-top : 0.05cm;
padding-bottom : 0.05cm;
}
//Triangle dividers
hr{
visibility : visible;
height : 6px;
margin : 4px 0px;
margin : 0.12cm 0cm;
background-image : @redTriangleImage;
background-size : 100% 100%;
border : none;
}
//Attribute Lists
dl {
.useSansSerif();
color : @headerText;
padding-left :1.3em;
text-indent :-1.3em;
}
dd {
text-indent : 0px;
}
// Monster Ability table
hr + table:first-of-type{
margin : 0;
column-span : 1;
color : @headerText;
background-color : transparent;
border-style : none;
border-image : none;
-webkit-column-span : 1;
tr {
background-color : transparent;
}
td,th {
padding: 0px;
}
}
}
//Full Width
.monster.wide{
.useColumns(0.96);
}
hr+hr+blockquote{
.useColumns(0.96);
}
@@ -485,9 +525,15 @@ body {
// * MUSTACHE DIVS/SPANS
// *****************************/
.phb3 {
.block {
break-inside : avoid;
}
.inline-block {
display : block;
}
div {
column-gap : 0.5cm; //Default spacing if a div uses multicolumns
}
}
//*****************************
@@ -512,6 +558,9 @@ body {
// *****************************/
.phb3 {
.blank {
height: 0.8em;
height: 0.75em;
}
p + .blank {
margin-top: -1em;
}
}

View File

@@ -206,7 +206,7 @@ body {
background-color : @monsterStatBackground;
border-style : solid;
border-width : 10px;
border-image : @monsterBorderImage 10;
border-image : @monsterBorderImageLegacy 10;
h2{
margin-top : -8px;
margin-bottom : 0px;

2354
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "homebrewery",
"description": "Create authentic looking D&D homebrews using only markdown",
"version": "2.11.1",
"version": "2.11.2",
"engines": {
"node": "14.15.x"
},
@@ -40,40 +40,42 @@
]
},
"dependencies": {
"@babel/core": "^7.13.10",
"@babel/preset-env": "^7.13.10",
"@babel/preset-react": "^7.12.13",
"@babel/core": "^7.14.0",
"@babel/preset-env": "^7.14.0",
"@babel/preset-react": "^7.13.13",
"body-parser": "^1.19.0",
"classnames": "^2.2.6",
"codemirror": "^5.59.4",
"classnames": "^2.3.1",
"codemirror": "^5.61.0",
"cookie-parser": "^1.4.5",
"create-react-class": "^15.7.0",
"dedent-tabs": "^0.9.0",
"express": "^4.17.1",
"express-async-handler": "^1.1.4",
"express-static-gzip": "2.1.1",
"fs-extra": "9.1.0",
"googleapis": "67.1.1",
"googleapis": "73.0.0",
"jwt-simple": "^0.5.6",
"less": "^3.13.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"mongoose": "^5.12.0",
"nanoid": "3.1.21",
"marked": "2.0.3",
"markedLegacy": "npm:marked@^0.3.19",
"marked": "2.0.1",
"moment": "^2.29.1",
"mongoose": "^5.12.7",
"nanoid": "3.1.22",
"nconf": "^0.11.2",
"prop-types": "15.7.2",
"query-string": "6.14.1",
"query-string": "7.0.0",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-frame-component": "4.1.3",
"react-router-dom": "5.2.0",
"sanitize-filename": "1.6.3",
"superagent": "^6.1.0",
"vitreum": "github:calculuschild/vitreum#21a8e1c9421f1d3a3b474c12f480feb2fbd28c5b"
"vitreum": "git+https://github.com/calculuschild/vitreum.git"
},
"devDependencies": {
"eslint": "^7.21.0",
"eslint-plugin-react": "^7.22.0",
"eslint": "^7.25.0",
"eslint-plugin-react": "^7.23.2",
"pico-check": "^2.0.3"
}
}

262
server.js
View File

@@ -8,6 +8,23 @@ const homebrewApi = require('./server/homebrew.api.js');
const GoogleActions = require('./server/googleActions.js');
const serveCompressedStaticAssets = require('./server/static-assets.mv.js');
const sanitizeFilename = require('sanitize-filename');
const asyncHandler = require('express-async-handler');
//Get the brew object from the HB database or Google Drive
const getBrewFromId = asyncHandler(async (id, accessType)=>{
if(accessType !== 'edit' && accessType !== 'share')
throw ('Invalid Access Type when getting brew');
let brew;
if(id.length > 12) {
const googleId = id.slice(0, -12);
id = id.slice(-12);
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType);
} else {
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
brew.sanatize(true);
}
return brew;
});
app.use('/', serveCompressedStaticAssets(`${__dirname}/build`));
@@ -65,84 +82,33 @@ app.get('/robots.txt', (req, res)=>{
return res.sendFile(`${__dirname}/robots.txt`);
});
//Source page
app.get('/source/:id', (req, res)=>{
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
.then((brew)=>{
const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text;
for (const replaceStr in replaceStrings) {
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
}
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
res.status(200).send(text);
})
.catch((err)=>{
console.log(err);
return res.status(400).send('Can\'t get brew from Google');
});
} else {
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text;
for (const replaceStr in replaceStrings) {
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
}
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
res.status(200).send(text);
})
.catch((err)=>{
console.log(err);
return res.status(404).send('Could not find Homebrew with that id');
});
app.get('/source/:id', asyncHandler(async (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'share');
const replaceStrings = { '&': '&amp;', '<': '&lt;', '>': '&gt;' };
let text = brew.text;
for (const replaceStr in replaceStrings) {
text = text.replaceAll(replaceStr, replaceStrings[replaceStr]);
}
});
text = `<code><pre style="white-space: pre-wrap;">${text}</pre></code>`;
res.status(200).send(text);
}));
//Download brew source page
app.get('/download/:id', (req, res)=>{
app.get('/download/:id', asyncHandler(async (req, res)=>{
const brew = await getBrewFromId(req.params.id, 'share');
const prefix = 'HB - ';
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
.then((brew)=>{
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/plain',
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
});
res.status(200).send(brew.text);
})
.catch((err)=>{
console.log(err);
return res.status(400).send('Can\'t get brew from Google');
});
} else {
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/plain',
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
});
res.status(200).send(brew.text);
})
.catch((err)=>{
console.log(err);
return res.status(404).send('Could not find Homebrew with that id');
});
}
});
let fileName = sanitizeFilename(`${prefix}${brew.title}`).replaceAll(' ', '');
if(!fileName || !fileName.length) { fileName = `${prefix}-Untitled-Brew`; };
res.set({
'Cache-Control' : 'no-cache',
'Content-Type' : 'text/plain',
'Content-Disposition' : `attachment; filename="${fileName}.txt"`
});
res.status(200).send(brew.text);
}));
//User Page
app.get('/user/:username', async (req, res, next)=>{
@@ -170,123 +136,45 @@ app.get('/user/:username', async (req, res, next)=>{
});
//Edit Page
app.get('/edit/:id', (req, res, next)=>{
app.get('/edit/:id', asyncHandler(async (req, res, next)=>{
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const editId = req.params.id.slice(-12);
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, editId, 'edit')
.then((brew)=>{
req.brew = brew; //TODO Need to sanitize later
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send('Can\'t get brew from Google');
});
} else {
HomebrewModel.get({ editId: req.params.id })
.then((brew)=>{
req.brew = brew.sanatize();
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
}
});
const brew = await getBrewFromId(req.params.id, 'edit');
req.brew = brew;
return next();
}));
//New Page
app.get('/new/:id', (req, res, next)=>{
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
.then((brew)=>{
req.brew = brew; //TODO Need to sanitize later
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send('Can\'t get brew from Google');
});
} else {
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
req.brew = brew;
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
}
});
app.get('/new/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
req.brew = brew;
return next();
}));
//Share Page
app.get('/share/:id', (req, res, next)=>{
app.get('/share/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
.then((brew)=>{
GoogleActions.increaseView(googleId, shareId, 'share', brew);
return brew;
})
.then((brew)=>{
req.brew = brew; //TODO Need to sanitize later
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send('Can\'t get brew from Google');
});
await GoogleActions.increaseView(googleId, shareId, 'share', brew)
.catch((err)=>{next(err);});
} else {
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
return brew.increaseView();
})
.then((brew)=>{
req.brew = brew.sanatize(true);
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
await brew.increaseView();
}
});
req.brew = brew;
return next();
}));
//Print Page
app.get('/print/:id', (req, res, next)=>{
if(req.params.id.length > 12) {
const googleId = req.params.id.slice(0, -12);
const shareId = req.params.id.slice(-12);
GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, shareId, 'share')
.then((brew)=>{
req.brew = brew; //TODO Need to sanitize later
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send('Can\'t get brew from Google');
});
} else {
HomebrewModel.get({ shareId: req.params.id })
.then((brew)=>{
req.brew = brew.sanatize(true);
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
}
});
app.get('/print/:id', asyncHandler(async (req, res, next)=>{
const brew = await getBrewFromId(req.params.id, 'share');
req.brew = brew;
return next();
}));
//Render the page
//const render = require('.build/render');
const templateFn = require('./client/template.js');
app.use((req, res)=>{
const props = {
@@ -303,11 +191,35 @@ app.use((req, res)=>{
templateFn('homebrew', title = req.brew ? req.brew.title : '', props)
.then((page)=>{ res.send(page); })
.catch((err)=>{
console.log('TEMPLATE ERROR');
console.log(err);
return res.sendStatus(500);
});
});
//v=====----- Error-Handling Middleware -----=====v//
//Format Errors so all fields will be sent
const replaceErrors = (key, value)=>{
if(value instanceof Error) {
const error = {};
Object.getOwnPropertyNames(value).forEach(function (key) {
error[key] = value[key];
});
return error;
}
return value;
};
const getPureError = (error)=>{
return JSON.parse(JSON.stringify(error, replaceErrors));
};
app.use((err, req, res, next)=>{
const status = err.status || 500;
console.error(err);
res.status(status).send(getPureError(err));
});
//^=====--------------------------------------=====^//
const PORT = process.env.PORT || config.get('web_port') || 8000;
app.listen(PORT);

View File

@@ -240,6 +240,7 @@ GoogleActions = {
},
readFileMetadata : async (auth, id, accessId, accessType)=>{
const drive = google.drive({ version: 'v3', auth: auth });
const obj = await drive.files.get({
@@ -248,7 +249,7 @@ GoogleActions = {
})
.catch((err)=>{
console.log('Error loading from Google');
console.error(err);
throw (err);
return;
});
@@ -345,7 +346,10 @@ GoogleActions = {
increaseView : async (id, accessId, accessType, brew)=>{
//service account because this is modifying another user's file properties
//so we need extended scope
const keys = JSON.parse(config.get('service_account'));
const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) :
config.get('service_account');
const auth = google.auth.fromJSON(keys);
auth.scopes = ['https://www.googleapis.com/auth/drive'];

View File

@@ -190,7 +190,6 @@ module.exports = {
.replace(/(<dt>.*<\/dt><dd>.*<\/dd>\n?)+/gm, `<dl>$1</dl>\n\n`)
.replace(/^}}/gm, '\n}}')
.replace(/^({{[^\n]*)$/gm, '$1\n');
console.log(rawBrewText);
return Markdown(
sanatizeScriptTags(rawBrewText),
{ renderer: renderer }