0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-06 14:22:52 +00:00

Merge branch 'master' into pr/2622

This commit is contained in:
Trevor Buckner
2023-01-27 14:26:05 -05:00
23 changed files with 1984 additions and 1929 deletions

View File

@@ -52,16 +52,38 @@ pre {
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).
### v3.6.0 ### Friday 23/01/2023 - v3.6.0
{{taskList {{taskList
##### calculuschild
* [x] Fix Google Drive brews sometimes duplicating
Fixes issues [#2603](https://github.com/naturalcrit/homebrewery/issues/2603)
##### Jeddai ##### Jeddai
* [x] Add unit tests with full coverage for the Homebrewery API * [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
}} }}
### Friday 23/12/2022 - v3.5.0 ### Friday 23/12/2022 - v3.5.0
@@ -79,22 +101,6 @@ Fixes issues [#1987](https://github.com/naturalcrit/homebrewery/issues/1987)
\page \page
### 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)
}}
### Saturday 10/12/2022 - v3.4.2 ### Saturday 10/12/2022 - v3.4.2
{{taskList {{taskList

View File

@@ -32,6 +32,7 @@ const Editor = createClass({
onTextChange : ()=>{}, onTextChange : ()=>{},
onStyleChange : ()=>{}, onStyleChange : ()=>{},
onMetaChange : ()=>{}, onMetaChange : ()=>{},
reportError : ()=>{},
renderer : 'legacy' renderer : 'legacy'
}; };
@@ -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}/>
</>; </>;
} }
}, },

View File

@@ -4,7 +4,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 Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx'); const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
@@ -37,7 +37,8 @@ const MetadataEditor = createClass({
renderer : 'legacy', renderer : 'legacy',
theme : '5ePHB' theme : '5ePHB'
}, },
onChange : ()=>{} onChange : ()=>{},
reportError : ()=>{}
}; };
}, },
@@ -121,8 +122,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 = '/';
}
}); });
}, },

View 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;

View 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;
}
}
}
}

View File

@@ -4,7 +4,7 @@ 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.png');
const dedent = require('dedent-tabs').default; const dedent = require('dedent-tabs').default;
@@ -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();
}
}); });
}, },

View File

@@ -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}/>;
}); });
}, },

View File

@@ -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;
@@ -45,7 +46,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,
@@ -60,7 +61,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 }, ()=>{
@@ -157,7 +157,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){
@@ -173,24 +176,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)
})); }));
@@ -205,8 +200,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}`);
@@ -266,77 +262,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>;
}
if(this.state.errors.response.error.status === 409) {
const message = this.state.errors.response.body?.message;
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops!
<div className='errorContainer'>
{message ? message : 'Conflict: please refresh to get latest changes'}
</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);
@@ -380,6 +305,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>
@@ -424,10 +355,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>
@@ -465,6 +399,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 text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors} /> <BrewRenderer text={this.state.brew.text} style={this.state.brew.style} renderer={this.state.brew.renderer} theme={this.state.brew.theme} errors={this.state.htmlErrors} />

View File

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

View File

@@ -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,6 +12,7 @@ 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');
@@ -31,14 +32,18 @@ const HomePage = createClass({
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}`;
}); });
@@ -54,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 />

View File

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

View File

@@ -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');
@@ -39,7 +40,7 @@ const NewPage = createClass({
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)
}; };
}, },
@@ -122,14 +123,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
@@ -152,7 +145,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;
@@ -164,67 +157,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...
@@ -254,7 +186,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 />

View File

@@ -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;
}
}
} }
} }

View File

@@ -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>;
} }
}); });

View 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;

3218
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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.5.0", "version": "3.6.0",
"engines": { "engines": {
"node": "16.11.x" "node": "16.11.x"
}, },
@@ -14,6 +14,7 @@
"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",
@@ -50,10 +51,10 @@
"lines" : 25 "lines" : 25
}, },
"server/homebrew.api.js" : { "server/homebrew.api.js" : {
"statements" : 71, "statements" : 65,
"branches" : 54, "branches" : 50,
"functions" : 66, "functions" : 60,
"lines" : 73 "lines" : 70
} }
} }
}, },
@@ -86,26 +87,26 @@
"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.5", "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.8.3", "mongoose": "^6.9.0",
"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": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-frame-component": "4.1.3", "react-frame-component": "4.1.3",
"react-router-dom": "6.6.1", "react-router-dom": "6.8.0",
"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.31.0", "eslint": "^8.32.0",
"eslint-plugin-react": "^7.31.11", "eslint-plugin-react": "^7.32.1",
"jest": "^29.2.2", "jest": "^29.4.1",
"supertest": "^6.3.3" "supertest": "^6.3.3"
} }
} }

View File

@@ -137,6 +137,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
}); });
} }

View File

@@ -294,6 +294,7 @@ 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.views = 0;
req.brew.title = `CLONE - ${req.brew.title}`; req.brew.title = `CLONE - ${req.brew.title}`;
req.ogMeta = { ...defaultMetaTags, req.ogMeta = { ...defaultMetaTags,

View File

@@ -109,7 +109,7 @@ If you believe you should have access to this brew, ask the file owner to invite
excludePropsFromUpdate : (brew)=>{ excludePropsFromUpdate : (brew)=>{
// Remove undesired properties // Remove undesired properties
const modified = _.clone(brew); const modified = _.clone(brew);
const propsToExclude = ['_id', 'views', 'lastViewed', 'editId', 'shareId', 'googleId']; const propsToExclude = ['_id', 'views', 'lastViewed'];
for (const prop of propsToExclude) { for (const prop of propsToExclude) {
delete modified[prop]; delete modified[prop];
} }
@@ -189,17 +189,18 @@ If you believe you should have access to this brew, ask the file owner to invite
res.status(200).send(saved); res.status(200).send(saved);
}, },
updateBrew : async (req, res)=>{ 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 // 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 brewFromClient = api.excludePropsFromUpdate(req.body);
if(req.brew.version && brewFromClient.version && req.brew.version > brewFromClient.version) { const brewFromServer = req.brew;
console.log(`Version mismatch on brew ${req.body.editId}`); if(brewFromServer.version && brewFromClient.version && brewFromServer.version > brewFromClient.version) {
console.log(`Version mismatch on brew ${brewFromClient.editId}`);
res.setHeader('Content-Type', 'application/json'); 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.` })); 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(req.brew, brewFromClient); let brew = _.assign(brewFromServer, brewFromClient);
const { saveToGoogle, removeFromGoogle } = req.query;
const googleId = brew.googleId; const googleId = brew.googleId;
const { saveToGoogle, removeFromGoogle } = req.query;
let afterSave = async ()=>true; let afterSave = async ()=>true;
brew.text = api.mergeBrewText(brew); brew.text = api.mergeBrewText(brew);
@@ -337,6 +338,7 @@ If you believe you should have access to this brew, ask the file owner to invite
} }
}; };
router.use('/api', require('./middleware/check-client-version.js'));
router.post('/api', asyncHandler(api.newBrew)); router.post('/api', asyncHandler(api.newBrew));
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew)); router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew)); router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));

View File

@@ -342,9 +342,6 @@ brew`);
expect(result._id).toBeUndefined(); expect(result._id).toBeUndefined();
expect(result.views).toBeUndefined(); expect(result.views).toBeUndefined();
expect(result.lastViewed).toBeUndefined(); expect(result.lastViewed).toBeUndefined();
expect(result.editId).toBeUndefined();
expect(result.shareId).toBeUndefined();
expect(result.googleId).toBeUndefined();
}); });
it('excludeGoogleProps removes the correct keys', ()=>{ it('excludeGoogleProps removes the correct keys', ()=>{

View 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();
};

View File

@@ -81,28 +81,28 @@
/* Cover Page */ /* Cover Page */
@font-face { @font-face {
font-family: NodestoCapsCondensed; font-family: NodestoCapsCondensed;
src: url('../fonts/5e/Nodesto Caps Condensed.woff2'); src: url('../../../fonts/5e/Nodesto Caps Condensed.woff2');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: NodestoCapsCondensed; font-family: NodestoCapsCondensed;
src: url('../fonts/5e/Nodesto Caps Condensed Bold.woff2'); src: url('../../../fonts/5e/Nodesto Caps Condensed Bold.woff2');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: NodestoCapsCondensed; font-family: NodestoCapsCondensed;
src: url('../fonts/5e/Nodesto Caps Condensed Italic.woff2'); src: url('../../../fonts/5e/Nodesto Caps Condensed Italic.woff2');
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: NodestoCapsCondensed; font-family: NodestoCapsCondensed;
src: url('../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2'); src: url('../../../fonts/5e/Nodesto Caps Condensed Bold Italic.woff2');
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
} }