0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2025-12-31 04:22:44 +00:00

Merge branch 'master' into SwappableThemes-ReorganizeFolderStructure

This commit is contained in:
Trevor Buckner
2022-04-02 13:50:18 -04:00
22 changed files with 912 additions and 822 deletions

View File

@@ -2,17 +2,23 @@
# #
# Check https://circleci.com/docs/2.0/language-javascript/ for more details # Check https://circleci.com/docs/2.0/language-javascript/ for more details
# #
version: 2 version: 2.1
orbs:
node: circleci/node@3.0.0
jobs: jobs:
build: build:
docker: docker:
- image: circleci/node:16.10.0 - image: cimg/node:16.11.0
- image: circleci/mongo:4.4 - image: mongo:4.4
working_directory: ~/repo working_directory: ~/homebrewery
executor: node/default
steps: steps:
- checkout - checkout:
path: ~/homebrewery
# Download and cache dependencies # Download and cache dependencies
- restore_cache: - restore_cache:
@@ -21,12 +27,48 @@ jobs:
# fallback to using the latest cache if no exact match is found # fallback to using the latest cache if no exact match is found
- v1-dependencies- - v1-dependencies-
- run: npm install - node/install-npm
- node/install-packages:
app-dir: ~/homebrewery
cache-path: node_modules
override-ci-command: npm i
- save_cache: - save_cache:
paths: paths:
- node_modules - node_modules
key: v1-dependencies-{{ checksum "package.json" }} key: v1-dependencies-{{ checksum "package.json" }}
- persist_to_workspace:
root: .
paths:
- .
test:
docker:
- image: cimg/node:16.11.0
working_directory: ~/homebrewery
parallelism: 4
steps:
- attach_workspace:
at: .
# run tests! # run tests!
- run: npm run circleci - run:
name: Test - Basic
command: npm run test:basic
- run:
name: Test - Mustache Spans
command: npm run test:mustache-span
- run:
name: Test - Routes
command: npm run test:route
workflows:
build_and_test:
jobs:
- build
- test:
requires:
- build

View File

@@ -9,37 +9,37 @@ using [Markdown][markdown-url]. It is distributed under the terms of the [MIT Li
[markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet [markdown-url]: https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
## Quick Start ## Quick Start
The easiest way to get started using the Homebrewery is to use it The easiest way to get started using The Homebrewery is to use it
[on our website][homebrewery-url]. The code is open source, so feel free to [on our website][homebrewery-url]. The code is open source, so feel free to
clone it, tinker with it. If you want to make changes to the code, you can run clone it and tinker with it. If you want to make changes to the code, you can run
your own local version for testing by following the installation instructions your own local version for testing by following the installation instructions
below. below.
[homebrewery-url]: https://homebrewery.naturalcrit.com [homebrewery-url]: https://homebrewery.naturalcrit.com
### Installation ### Installation
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/)
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 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. 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\4.4\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. 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 `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. 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", "OK", "OK" to close all the windows 1. Click "OK" three times to close all the windows.
1. install [git](https://git-scm.com/downloads) (select the option that allows Git to run from the command prompt) 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]):
``` ```
@@ -54,7 +54,7 @@ the project to run locally.
You can set this temporarily in your shell of choice: You can set this temporarily in your shell of choice:
* 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 / OSX: `export NODE_ENV=local` * Linux / macOS: `export NODE_ENV=local`
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:
@@ -63,7 +63,7 @@ it using the two commands:
1. `npm start` 1. `npm start`
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.
### Running the application via Docker ### Running the application via Docker
@@ -95,11 +95,11 @@ You can check out the [changelog](./changelog.md).
## License ## License
This project is licensed under the [MIT license](./license). Which means you This project is licensed under the [MIT license](./license), which means you
are free to use The Homebrewery in any way that you want, except for claiming are free to use The Homebrewery in any way that you want, except for claiming
that you made it yourself. that you made it yourself.
If you wish to sell or in some way gain profit for what's created on this site, If you wish to sell, or in some way gain profit for, what's created on this site,
it's your responsibility to ensure you have the proper licenses/rights for any it's your responsibility to ensure you have the proper licenses/rights for any
images or resources used. images or resources used.
@@ -108,13 +108,12 @@ images or resources used.
You are welcome to contribute to the development and maintenance of the You are welcome to contribute to the development and maintenance of the
project! There are several ways of doing that: project! There are several ways of doing that:
- At the moment, we have a huge backlog of [issues][repo-issues-url] and some - At the moment, we have a huge backlog of [issues][repo-issues-url] and some
of them are outdated, duplicates or doesn't contain any useful info. In order of them are outdated, duplicates, or don't contain any useful info. To help, you can [mark duplicates][github-mark-duplicate-url], try to
to help you can [mark duplicates][github-mark-duplicate-url], try to reproduce some complex or weird issues, try finding a workaround for a
reproduce some complex or weird issues, try with finding a workaround for a reported bug, or just mention our issue managers team to let them know about
reported bug or just mention issue managers team to let them know about outdated issues via `@naturalcrit/issue-managers`.
outdated issue via `@naturalcrit/issue-managers`.
- Our [subreddit][subreddit-url] is constantly growing and there are number of - Our [subreddit][subreddit-url] is constantly growing and there are number of
bug reports: any help with sorting them out is very welcome. bug reports. Any help with sorting them out is very welcome.
- And of course you can contribute by fixing a bug or implementing a new - And of course you can contribute by fixing a bug or implementing a new
feature by yourself, we are waiting for your feature by yourself, we are waiting for your
[pull requests][github-pr-docs-url]! [pull requests][github-pr-docs-url]!

View File

@@ -29,11 +29,43 @@ pre {
.page p + pre { .page p + pre {
margin-top : 0.1cm; margin-top : 0.1cm;
} }
.page .openSans {
font-family: 'Open Sans';
font-size: 0.9em;
}
``` ```
## 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).
### Wednesday 27/03/2022 - v3.0.8
{{taskList
* [x] Style updates to user page.
* [x] Added a logout button (finally)! You can find it under {{openSans **USERNAME {{fa,fa-user}} → LOGOUT {{fas,fa-power-off}}**}}
Fixes issues: [#303](https://github.com/naturalcrit/homebrewery/issues/303)
* [x] Clarified the default text when submitting an issue via Reddit post.
* [x] Fixed broken Table of Contents links in PDFs. (Thanks lucastucious!)
Fixes issues: [#1749](https://github.com/naturalcrit/homebrewery/issues/1749)
* [x] Fixed window resizing causing the edit page divider to get lost off of the edge of the page.
Fixes issues: [#2053](https://github.com/naturalcrit/homebrewery/issues/2053)
* [x] Fixed Class Table decorations overlapping main text.
Fixes issues: [#1985](https://github.com/naturalcrit/homebrewery/issues/1985)
* [x] Updated {{openSans **STYLE EDITOR {{fa,fa-pencil-alt}} → REMOVE DROP CAP {{fas,fa-remove-format}}**}} snippet to also remove small-caps first line font.
* [x] Background work in preparation for brew themes.
}}
### Wednesday 02/02/2022 - v3.0.7 ### Wednesday 02/02/2022 - v3.0.7
{{taskList {{taskList
* [x] Revert active line highlighting. * [x] Revert active line highlighting.
@@ -50,7 +82,7 @@ For a full record of development, visit our [Github Page](https://github.com/nat
Fixes issues: [#1943](https://github.com/naturalcrit/homebrewery/issues/1943) Fixes issues: [#1943](https://github.com/naturalcrit/homebrewery/issues/1943)
* [x] Added a Legacy to V3 migration guide under **NEED HELP? {{fa,fa-question-circle}} → MIGRATE {{fas,fa-file-import}}** * [x] Added a Legacy to V3 migration guide under {{openSans **NEED HELP? {{fa,fa-question-circle}} → MIGRATE {{fas,fa-file-import}}**}}
* [x] Background refactoring and unit tests. * [x] Background refactoring and unit tests.
}} }}
@@ -61,7 +93,7 @@ For a full record of development, visit our [Github Page](https://github.com/nat
Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736) Fixes issues: [#1736](https://github.com/naturalcrit/homebrewery/issues/1736)
* [x] Code search/replace `CTRL F / CTRL SHIFT F` * [x] Code search/replace PC: `CTRL F / CTRL SHIFT F` / Mac: `CMD F / OPTION CMD F`
Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201) Fixes issues: [#1201](https://github.com/naturalcrit/homebrewery/issues/1201)

View File

@@ -77,7 +77,7 @@ const MetadataEditor = createClass({
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return; if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
} }
request.delete(`/api/${this.props.metadata.editId}`) request.delete(`/api/${this.props.metadata.googleId}${this.props.metadata.editId}`)
.send() .send()
.end(function(err, res){ .end(function(err, res){
window.location.href = '/'; window.location.href = '/';

View File

@@ -32,11 +32,14 @@ const Homebrew = createClass({
} }
}; };
}, },
componentWillMount : function() {
global.account = this.props.account; getInitialState : function(){
global.version = this.props.version; global.version = this.props.version;
global.account = this.props.account;
global.enable_v3 = this.props.enable_v3; global.enable_v3 = this.props.enable_v3;
return {};
}, },
render : function (){ render : function (){
return ( return (
<Router location={this.props.url}> <Router location={this.props.url}>
@@ -46,7 +49,7 @@ const Homebrew = createClass({
<Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/> <Route path='/share/:id' component={(routeProps)=><SharePage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/> <Route path='/new/:id' component={(routeProps)=><NewPage id={routeProps.match.params.id} brew={this.props.brew} />}/>
<Route path='/new' exact component={(routeProps)=><NewPage />}/> <Route path='/new' exact component={(routeProps)=><NewPage />}/>
<Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} />}/> <Route path='/user/:username' component={(routeProps)=><UserPage username={routeProps.match.params.username} brews={this.props.brews} query={queryString.parse(routeProps.location.search)}/>}/>
<Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/> <Route path='/print/:id' component={(routeProps)=><PrintPage brew={this.props.brew} query={queryString.parse(routeProps.location.search)} />}/>
<Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/> <Route path='/print' exact component={(routeProps)=><PrintPage query={queryString.parse(routeProps.location.search)} />}/>
<Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/> <Route path='/changelog' exact component={()=><SharePage brew={this.props.brew} />}/>

View File

@@ -18,11 +18,50 @@ const Account = createClass({
} }
}, },
handleLogout : function(){
if(confirm('Are you sure you want to log out?')) {
// Reset divider position
window.localStorage.removeItem('naturalcrit-pane-split');
// Clear login cookie
let domain = '';
if(window.location?.hostname) {
let domainArray = window.location.hostname.split('.');
if(domainArray.length > 2){
domainArray = [''].concat(domainArray.slice(-2));
}
domain = domainArray.join('.');
}
document.cookie = `nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;samesite=lax;${domain ? `domain=${domain}` : ''}`;
window.location = '/';
}
},
render : function(){ render : function(){
if(global.account){ if(global.account){
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fas fa-user'> return <Nav.dropdown>
{global.account.username} <Nav.item
</Nav.item>; className='account'
color='orange'
icon='fas fa-user'
>
{global.account.username}
</Nav.item>
<Nav.item
href={`/user/${global.account.username}`}
color='yellow'
icon='fas fa-beer'
>
brews
</Nav.item>
<Nav.item
className='logout'
color='red'
icon='fas fa-power-off'
onClick={this.handleLogout}
>
logout
</Nav.item>
</Nav.dropdown>;
} }
return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'> return <Nav.item href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`} color='teal' icon='fas fa-sign-in-alt'>

View File

@@ -1,6 +1,7 @@
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 dedent = require('dedent-tabs').default;
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
@@ -10,7 +11,11 @@ module.exports = function(props){
need help? need help?
</Nav.item> </Nav.item>
<Nav.item color='red' icon='fas fa-fw fa-bug' <Nav.item color='red' icon='fas fa-fw fa-bug'
href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&title=${encodeURIComponent('[Issue] Describe Your Issue Here')}`} href={`https://www.reddit.com/r/homebrewery/submit?selftext=true&text=${encodeURIComponent(dedent`
**Browser(s)** :
**Operating System** :
**Legacy or v3 Renderer** :
**Issue** : `)}`}
newTab={true} newTab={true}
rel='noopener noreferrer'> rel='noopener noreferrer'>
report issue report issue

View File

@@ -14,12 +14,10 @@ const Navbar = createClass({
}; };
}, },
componentDidMount : function() { getInitialState : function() {
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); return {
this.setState({ ver : global.version
//showNonChromeWarning : !isChrome, };
ver : window.version
});
}, },
/* /*

View File

@@ -142,4 +142,7 @@
text-align : center; text-align : center;
} }
} }
.account.navItem{
min-width: 100px;
}
} }

View File

@@ -31,19 +31,11 @@ const BrewItem = createClass({
if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return; if(!confirm('Are you REALLY sure? You will lose editor access to this document.')) return;
} }
if(this.props.brew.googleId) { request.delete(`/api/${this.props.brew.googleId}${this.props.brew.editId}`)
request.get(`/api/removeGoogle/${this.props.brew.googleId}${this.props.brew.editId}`) .send()
.send() .end(function(err, res){
.end(function(err, res){ location.reload();
location.reload(); });
});
} else {
request.delete(`/api/${this.props.brew.editId}`)
.send()
.end(function(err, res){
location.reload();
});
}
}, },
renderDeleteBrewLink : function(){ renderDeleteBrewLink : function(){

View File

@@ -24,7 +24,8 @@ const ListPage = createClass({
return { return {
sortType : 'alpha', sortType : 'alpha',
sortDir : 'asc', sortDir : 'asc',
filterString : '' filterString : this.props.query?.filter || '',
query : this.props.query
}; };
}, },
@@ -74,19 +75,35 @@ const ListPage = createClass({
handleFilterTextChange : function(e){ handleFilterTextChange : function(e){
this.setState({ this.setState({
filterString : e.target.value filterString : e.target.value,
}); });
this.updateUrl(e.target.value);
return; return;
}, },
updateUrl : function(filterTerm){
const url = new URL(window.location.href);
const urlParams = new URLSearchParams(url.search);
if(urlParams.get('filter') == filterTerm)
return;
if(!filterTerm)
urlParams.delete('filter');
else
urlParams.set('filter', filterTerm);
url.search = urlParams;
window.history.replaceState(null, null, url);
},
renderFilterOption : function(){ renderFilterOption : function(){
return <td> return <td>
<label> <label>
<i className='fas fa-search'></i> <i className='fas fa-search'></i>
<input <input
type='search' type='search'
placeholder='search title/description' autoFocus={true}
placeholder='filter title/description'
onChange={this.handleFilterTextChange} onChange={this.handleFilterTextChange}
value={this.state.filterString}
/> />
</label> </label>
</td>; </td>;

View File

@@ -200,73 +200,18 @@ const EditPage = createClass({
const brew = this.state.brew; const brew = this.state.brew;
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
if(this.state.saveGoogle) { const params = `${transfer ? `?transfer${this.state.saveGoogle ? 'To' : 'From'}Google=true` : ''}`;
if(transfer) { const res = await request
const res = await request .put(`/api/update/${brew.editId}${params}`)
.post('/api/newGoogle/') .send(brew)
.send(brew) .catch((err)=>{
.catch((err)=>{ console.log('Error Updating Local Brew');
console.log(err.status === 401 this.setState({ errors: err });
? 'Not signed in!' });
: 'Error Transferring to Google!');
this.setState({ errors: err, saveGoogle: false });
});
if(!res) { return; } this.savedBrew = res.body;
if(transfer) {
console.log('Deleting Local Copy'); history.replaceState(null, null, `/edit/${this.savedBrew.googleId ?? ''}${this.savedBrew.editId}`);
await request.delete(`/api/${brew.editId}`)
.send()
.catch((err)=>{
console.log('Error deleting Local Copy');
});
this.savedBrew = res.body;
history.replaceState(null, null, `/edit/${this.savedBrew.googleId}${this.savedBrew.editId}`); //update URL to match doc ID
} else {
const res = await request
.put(`/api/updateGoogle/${brew.editId}`)
.send(brew)
.catch((err)=>{
console.log(err.status === 401
? 'Not signed in!'
: 'Error Saving to Google!');
this.setState({ errors: err });
return;
});
this.savedBrew = res.body;
}
} else {
if(transfer) {
const res = await request.post('/api')
.send(brew)
.catch((err)=>{
console.log('Error creating Local Copy');
this.setState({ errors: err });
return;
});
await request.get(`/api/removeGoogle/${brew.googleId}${brew.editId}`)
.send()
.catch((err)=>{
console.log('Error Deleting Google Brew');
});
this.savedBrew = res.body;
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`); //update URL to match doc ID
} else {
const res = await request
.put(`/api/update/${brew.editId}`)
.send(brew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ errors: err });
return;
});
this.savedBrew = res.body;
}
} }
this.setState((prevState)=>({ this.setState((prevState)=>({
@@ -331,26 +276,26 @@ const EditPage = createClass({
console.log(errMsg); console.log(errMsg);
} catch (e){} } catch (e){}
if(this.state.errors.status == '401'){ // if(this.state.errors.status == '401'){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> // return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! // Oops!
<div className='errorContainer' onClick={this.clearErrors}> // <div className='errorContainer' onClick={this.clearErrors}>
You must be signed in to a Google account // You must be signed in to a Google account
to save this to<br />Google Drive!<br /> // to save this to<br />Google Drive!<br />
<a target='_blank' rel='noopener noreferrer' // <a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}> // href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'> // <div className='confirm'>
Sign In // Sign In
</div> // </div>
</a> // </a>
<div className='deny'> // <div className='deny'>
Not Now // Not Now
</div> // </div>
</div> // </div>
</Nav.item>; // </Nav.item>;
} // }
if(this.state.errors.response.req.url.match(/^\/api\/.*Google.*$/m)){ if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer' onClick={this.clearErrors}> <div className='errorContainer' onClick={this.clearErrors}>

View File

@@ -162,45 +162,24 @@ const NewPage = createClass({
const index = brew.text.indexOf('```\n\n'); const index = brew.text.indexOf('```\n\n');
brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`; brew.style = `${brew.style ? `${brew.style}\n` : ''}${brew.text.slice(7, index - 1)}`;
brew.text = brew.text.slice(index + 5); brew.text = brew.text.slice(index + 5);
}; }
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1; brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
if(this.state.saveGoogle) { const res = await request
const res = await request .post(`/api${this.state.saveGoogle ? '?transferToGoogle=true' : ''}`)
.post('/api/newGoogle/')
.send(brew) .send(brew)
.catch((err)=>{ .catch((err)=>{
console.log(err.status === 401 console.log(err);
? 'Not signed in!'
: 'Error Creating New Google Brew!');
this.setState({ isSaving: false, errors: err }); this.setState({ isSaving: false, errors: err });
return;
}); });
if(!res) return;
brew = res.body; brew = res.body;
localStorage.removeItem(BREWKEY); localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY); localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY); localStorage.removeItem(METAKEY);
window.location = `/edit/${brew.googleId}${brew.editId}`; window.location = `/edit/${brew.googleId ?? ''}${brew.editId}`;
} else {
request.post('/api')
.send(brew)
.end((err, res)=>{
if(err){
this.setState({
isSaving : false
});
return;
}
window.onbeforeunload = function(){};
brew = res.body;
localStorage.removeItem(BREWKEY);
localStorage.removeItem(STYLEKEY);
localStorage.removeItem(METAKEY);
window.location = `/edit/${brew.editId}`;
});
}
}, },
renderSaveButton : function(){ renderSaveButton : function(){
@@ -213,26 +192,26 @@ const NewPage = createClass({
console.log(errMsg); console.log(errMsg);
} catch (e){} } catch (e){}
if(this.state.errors.status == '401'){ // if(this.state.errors.status == '401'){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> // return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! // Oops!
<div className='errorContainer' onClick={this.clearErrors}> // <div className='errorContainer' onClick={this.clearErrors}>
You must be signed in to a Google account // You must be signed in to a Google account
to save this to<br />Google Drive!<br /> // to save this to<br />Google Drive!<br />
<a target='_blank' rel='noopener noreferrer' // <a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}> // href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'> // <div className='confirm'>
Sign In // Sign In
</div> // </div>
</a> // </a>
<div className='deny'> // <div className='deny'>
Not Now // Not Now
</div> // </div>
</div> // </div>
</Nav.item>; // </Nav.item>;
} // }
if(this.state.errors.response.req.url.match(/^\/api\/.*Google.*$/m)){ if(this.state.errors.response.req.url.match(/^\/api.*Google.*$/m)){
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'> return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
Oops! Oops!
<div className='errorContainer' onClick={this.clearErrors}> <div className='errorContainer' onClick={this.clearErrors}>

View File

@@ -19,6 +19,7 @@ const UserPage = createClass({
return { return {
username : '', username : '',
brews : [], brews : [],
query : ''
}; };
}, },
getInitialState : function() { getInitialState : function() {
@@ -62,7 +63,7 @@ const UserPage = createClass({
}, },
render : function(){ render : function(){
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()}></ListPage>; return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query}></ListPage>;
} }
}); });

744
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.0.7", "version": "3.0.8",
"engines": { "engines": {
"node": "16.11.x" "node": "16.11.x"
}, },
@@ -20,6 +20,9 @@
"verify": "npm run lint && npm test", "verify": "npm run lint && npm test",
"test": "jest", "test": "jest",
"test:dev": "jest --verbose --watch", "test:dev": "jest --verbose --watch",
"test:basic": "jest tests/markdown/basic.test.js --verbose",
"test:mustache-span": "jest tests/markdown/mustache-span.test.js --verbose",
"test:route": "jest tests/routes/static-pages.test.js --verbose",
"phb": "node scripts/phb.js", "phb": "node scripts/phb.js",
"prod": "set NODE_ENV=production && npm run build", "prod": "set NODE_ENV=production && npm run build",
"postinstall": "npm run buildall", "postinstall": "npm run buildall",
@@ -31,6 +34,7 @@
"build/*" "build/*"
], ],
"jest": { "jest": {
"testTimeout" : 15000,
"modulePaths": [ "modulePaths": [
"mode_modules", "mode_modules",
"shared", "shared",
@@ -47,21 +51,21 @@
] ]
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.17.2", "@babel/core": "^7.17.8",
"@babel/plugin-transform-runtime": "^7.17.0", "@babel/plugin-transform-runtime": "^7.17.0",
"@babel/preset-env": "^7.16.11", "@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7", "@babel/preset-react": "^7.16.7",
"body-parser": "^1.19.1", "body-parser": "^1.19.2",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"codemirror": "^5.65.1", "codemirror": "^5.65.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"create-react-class": "^15.7.0", "create-react-class": "^15.7.0",
"dedent-tabs": "^0.10.1", "dedent-tabs": "^0.10.1",
"express": "^4.17.2", "express": "^4.17.3",
"express-async-handler": "^1.2.0", "express-async-handler": "^1.2.0",
"express-static-gzip": "2.1.3", "express-static-gzip": "2.1.5",
"fs-extra": "10.0.0", "fs-extra": "10.0.1",
"googleapis": "95.0.0", "googleapis": "100.0.0",
"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",
@@ -70,8 +74,8 @@
"marked-extended-tables": "^1.0.3", "marked-extended-tables": "^1.0.3",
"markedLegacy": "npm:marked@^0.3.19", "markedLegacy": "npm:marked@^0.3.19",
"moment": "^2.29.1", "moment": "^2.29.1",
"mongoose": "^6.2.1", "mongoose": "^6.2.9",
"nanoid": "3.2.0", "nanoid": "3.3.2",
"nconf": "^0.11.3", "nconf": "^0.11.3",
"query-string": "7.1.1", "query-string": "7.1.1",
"react": "^16.14.0", "react": "^16.14.0",
@@ -83,8 +87,8 @@
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git" "vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.8.0", "eslint": "^8.12.0",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.29.4",
"jest": "^27.5.1", "jest": "^27.5.1",
"supertest": "^6.2.2" "supertest": "^6.2.2"
} }

View File

@@ -25,7 +25,7 @@ const getBrewFromId = asyncHandler(async (id, accessType)=>{
if(id.length > 12) { if(id.length > 12) {
const googleId = id.slice(0, -12); const googleId = id.slice(0, -12);
id = id.slice(-12); id = id.slice(-12);
brew = await GoogleActions.readFileMetadata(config.get('google_api_key'), googleId, id, accessType); brew = await GoogleActions.getGoogleBrew(googleId, id, accessType);
} else { } else {
brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id }); brew = await HomebrewModel.get(accessType == 'edit' ? { editId: id } : { shareId: id });
brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object brew = brew.toObject(); // Convert MongoDB object to standard Javascript Object
@@ -201,13 +201,16 @@ app.get('/user/:username', async (req, res, next)=>{
}); });
if(ownAccount && req?.account?.googleId){ if(ownAccount && req?.account?.googleId){
const googleBrews = await GoogleActions.listGoogleBrews(req, res) const auth = await GoogleActions.authCheck(req.account, res);
.catch((err)=>{ let googleBrews = await GoogleActions.listGoogleBrews(auth)
console.error(err); .catch((err)=>{
}); console.error(err);
});
if(googleBrews) if(googleBrews) {
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
brews = _.concat(brews, googleBrews); brews = _.concat(brews, googleBrews);
}
} }
req.brews = _.map(brews, (brew)=>{ req.brews = _.map(brews, (brew)=>{

View File

@@ -5,7 +5,20 @@ const { nanoid } = require('nanoid');
const token = require('./token.js'); const token = require('./token.js');
const config = require('./config.js'); const config = require('./config.js');
//let oAuth2Client; const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) :
config.get('service_account');
let serviceAuth;
try {
serviceAuth = google.auth.fromJSON(keys);
serviceAuth.scopes = [
'https://www.googleapis.com/auth/drive'
];
} catch (err) {
console.warn(err);
console.log('Please make sure that a Google Service Account is set up properly in your config files.');
}
google.options({ auth: serviceAuth || config.get('google_api_key') });
const GoogleActions = { const GoogleActions = {
@@ -43,7 +56,7 @@ const GoogleActions = {
}, },
getGoogleFolder : async (auth)=>{ getGoogleFolder : async (auth)=>{
const drive = google.drive({ version: 'v3', auth: auth }); const drive = google.drive({ version: 'v3', auth });
fileMetadata = { fileMetadata = {
'name' : 'Homebrewery', 'name' : 'Homebrewery',
@@ -79,17 +92,8 @@ const GoogleActions = {
return folderId; return folderId;
}, },
listGoogleBrews : async (req, res)=>{ listGoogleBrews : async (auth)=>{
const drive = google.drive({ version: 'v3', auth });
oAuth2Client = GoogleActions.authCheck(req.account, res);
//TODO: Change to service account to allow non-owners to view published files.
// Requires a driveId parameter in the drive.files.list command
// const keys = JSON.parse(config.get('service_account'));
// const auth = google.auth.fromJSON(keys);
// auth.scopes = ['https://www.googleapis.com/auth/drive'];
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
const obj = await drive.files.list({ const obj = await drive.files.list({
pageSize : 1000, pageSize : 1000,
@@ -97,18 +101,18 @@ const GoogleActions = {
q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false' q : 'mimeType != \'application/vnd.google-apps.folder\' and trashed = false'
}) })
.catch((err)=>{ .catch((err)=>{
console.log(`Error Listing Google Brews`); console.log(`Error Listing Google Brews`);
console.error(err); console.error(err);
throw (err); throw (err);
//TODO: Should break out here, but continues on for some reason. //TODO: Should break out here, but continues on for some reason.
}); });
if(!obj.data.files.length) { if(!obj.data.files.length) {
console.log('No files found.'); console.log('No files found.');
} }
const brews = obj.data.files.map((file)=>{ const brews = obj.data.files.map((file)=>{
return { return {
text : '', text : '',
shareId : file.properties.shareId, shareId : file.properties.shareId,
editId : file.properties.editId, editId : file.properties.editId,
@@ -122,65 +126,47 @@ const GoogleActions = {
views : parseInt(file.properties.views), views : parseInt(file.properties.views),
tags : '', tags : '',
published : file.properties.published ? file.properties.published == 'true' : false, published : file.properties.published ? file.properties.published == 'true' : false,
authors : [req.account.username], //TODO: properly save and load authors to google drive
systems : [] systems : []
}; };
}); });
return brews; return brews;
}, },
existsGoogleBrew : async (auth, id)=>{ updateGoogleBrew : async (brew)=>{
const drive = google.drive({ version: 'v3', auth: auth }); const drive = google.drive({ version: 'v3' });
const result = await drive.files.get({ fileId: id }) await drive.files.update({
.catch((err)=>{ fileId : brew.googleId,
console.log('error checking file exists...'); resource : {
console.error(err); name : `${brew.title}.txt`,
return false; description : `${brew.description}`,
}); properties : {
title : brew.title,
if(result){return true;} published : brew.published,
version : brew.version,
return false; renderer : brew.renderer,
}, tags : brew.tags,
pageCount : brew.pageCount,
updateGoogleBrew : async (auth, brew)=>{ systems : brew.systems.join()
const drive = google.drive({ version: 'v3', auth: auth });
if(await GoogleActions.existsGoogleBrew(auth, brew.googleId) == true) {
await drive.files.update({
fileId : brew.googleId,
resource : {
name : `${brew.title}.txt`,
description : `${brew.description}`,
properties : {
title : brew.title,
published : brew.published,
version : brew.version,
renderer : brew.renderer,
tags : brew.tags,
pageCount : brew.pageCount,
systems : brew.systems.join()
}
},
media : {
mimeType : 'text/plain',
body : brew.text
} }
}) },
.catch((err)=>{ media : {
console.log('Error saving to google'); mimeType : 'text/plain',
console.error(err); body : brew.text
throw (err); }
//return res.status(500).send('Error while saving'); })
}); .catch((err)=>{
} console.log('Error saving to google');
console.error(err);
throw (err);
//return res.status(500).send('Error while saving');
});
return (brew); return (brew);
}, },
newGoogleBrew : async (auth, brew)=>{ newGoogleBrew : async (auth, brew)=>{
const drive = google.drive({ version: 'v3', auth: auth }); const drive = google.drive({ version: 'v3', auth });
const media = { const media = {
mimeType : 'text/plain', mimeType : 'text/plain',
@@ -194,8 +180,8 @@ const GoogleActions = {
'description' : `${brew.description}`, 'description' : `${brew.description}`,
'parents' : [folderId], 'parents' : [folderId],
'properties' : { //AppProperties is not accessible 'properties' : { //AppProperties is not accessible
'shareId' : nanoid(12), 'shareId' : brew.shareId || nanoid(12),
'editId' : nanoid(12), 'editId' : brew.editId || nanoid(12),
'title' : brew.title, 'title' : brew.title,
'views' : '0', 'views' : '0',
'pageCount' : brew.pageCount, 'pageCount' : brew.pageCount,
@@ -248,9 +234,8 @@ const GoogleActions = {
return newHomebrew; return newHomebrew;
}, },
readFileMetadata : async (auth, id, accessId, accessType)=>{ getGoogleBrew : async (id, accessId, accessType)=>{
const drive = google.drive({ version: 'v3' });
const drive = google.drive({ version: 'v3', auth: auth });
const obj = await drive.files.get({ const obj = await drive.files.get({
fileId : id, fileId : id,
@@ -269,16 +254,7 @@ const GoogleActions = {
throw ('Share ID does not match'); throw ('Share ID does not match');
} }
//Access file using service account. Using API key only causes "automated query" lockouts after a while. const serviceDrive = google.drive({ version: 'v3' });
const keys = typeof(config.get('service_account')) == 'string' ?
JSON.parse(config.get('service_account')) :
config.get('service_account');
const serviceAuth = google.auth.fromJSON(keys);
serviceAuth.scopes = ['https://www.googleapis.com/auth/drive'];
const serviceDrive = google.drive({ version: 'v3', auth: serviceAuth });
const file = await serviceDrive.files.get({ const file = await serviceDrive.files.get({
fileId : id, fileId : id,
@@ -319,10 +295,8 @@ const GoogleActions = {
} }
}, },
deleteGoogleBrew : async (req, res, id)=>{ deleteGoogleBrew : async (auth, id)=>{
const drive = google.drive({ version: 'v3', auth });
oAuth2Client = GoogleActions.authCheck(req.account, res);
const drive = google.drive({ version: 'v3', auth: oAuth2Client });
const googleId = id.slice(0, -12); const googleId = id.slice(0, -12);
const accessId = id.slice(-12); const accessId = id.slice(-12);
@@ -334,7 +308,6 @@ const GoogleActions = {
.catch((err)=>{ .catch((err)=>{
console.log('Error loading from Google'); console.log('Error loading from Google');
console.error(err); console.error(err);
return;
}); });
if(obj && obj.data.properties.editId != accessId) { if(obj && obj.data.properties.editId != accessId) {
@@ -349,21 +322,10 @@ const GoogleActions = {
console.log('Can\'t delete Google file'); console.log('Can\'t delete Google file');
console.error(err); console.error(err);
}); });
return res.status(200).send();
}, },
increaseView : async (id, accessId, accessType, brew)=>{ increaseView : async (id, accessId, accessType, brew)=>{
//service account because this is modifying another user's file properties const drive = google.drive({ version: 'v3' });
//so we need extended scope
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'];
const drive = google.drive({ version: 'v3', auth: auth });
await drive.files.update({ await drive.files.update({
fileId : brew.googleId, fileId : brew.googleId,
@@ -380,8 +342,6 @@ const GoogleActions = {
console.error(err); console.error(err);
//return res.status(500).send('Error while saving'); //return res.status(500).send('Error while saving');
}); });
return;
} }
}; };

View File

@@ -5,6 +5,7 @@ const zlib = require('zlib');
const GoogleActions = require('./googleActions.js'); const GoogleActions = require('./googleActions.js');
const Markdown = require('../shared/naturalcrit/markdown.js'); const Markdown = require('../shared/naturalcrit/markdown.js');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const asyncHandler = require('express-async-handler');
// 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) {
@@ -41,154 +42,195 @@ const excludePropsFromUpdate = (brew)=>{
const propsToExclude = ['views', 'lastViewed']; const propsToExclude = ['views', 'lastViewed'];
for (const prop of propsToExclude) { for (const prop of propsToExclude) {
delete brew[prop]; delete brew[prop];
}; }
return brew; return brew;
}; };
const newBrew = (req, res)=>{ const beforeNewSave = (account, brew)=>{
const brew = req.body;
if(!brew.title) { if(!brew.title) {
brew.title = getGoodBrewTitle(brew.text); brew.title = getGoodBrewTitle(brew.text);
} }
brew.authors = (req.account) ? [req.account.username] : []; brew.authors = (account) ? [account.username] : [];
brew.text = mergeBrewText(brew); brew.text = mergeBrewText(brew);
};
delete brew.editId; const newLocalBrew = async (brew)=>{
delete brew.shareId;
delete brew.googleId;
const newHomebrew = new HomebrewModel(brew); const newHomebrew = new HomebrewModel(brew);
// Compress brew text to binary before saving // Compress brew text to binary before saving
newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text); newHomebrew.textBin = zlib.deflateRawSync(newHomebrew.text);
// Delete the non-binary text field since it's not needed anymore // Delete the non-binary text field since it's not needed anymore
newHomebrew.text = undefined; newHomebrew.text = undefined;
newHomebrew.save((err, obj)=>{ let saved = await newHomebrew.save()
if(err) {
console.error(err, err.toString(), err.stack);
return res.status(500).send(`Error while creating new brew, ${err.toString()}`);
}
obj = obj.toObject();
obj.gDrive = false;
return res.status(200).send(obj);
});
};
const updateBrew = (req, res)=>{
HomebrewModel.get({ editId: req.params.id })
.then((brew)=>{
const updateBrew = excludePropsFromUpdate(req.body);
brew = _.merge(brew, updateBrew);
brew.text = mergeBrewText(brew);
// 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));
}
brew.markModified('authors');
brew.markModified('systems');
brew.save((err, obj)=>{
if(err) throw err;
return res.status(200).send(obj);
});
})
.catch((err)=>{ .catch((err)=>{
console.error(err); console.error(err, err.toString(), err.stack);
return res.status(500).send('Error while saving'); throw `Error while creating new brew, ${err.toString()}`;
}); });
saved = saved.toObject();
saved.gDrive = false;
return saved;
}; };
const deleteBrew = (req, res)=>{ const newGoogleBrew = async (account, brew, res)=>{
HomebrewModel.find({ editId: req.params.id }, (err, objs)=>{ const oAuth2Client = GoogleActions.authCheck(account, res);
if(!objs.length || err) {
return res.status(404).send('Can not find homebrew with that id');
}
const brew = objs[0]; return await GoogleActions.newGoogleBrew(oAuth2Client, brew);
if(req.account) {
// Remove current user as author
brew.authors = _.pull(brew.authors, req.account.username);
brew.markModified('authors');
}
if(brew.authors.length === 0) {
// Delete brew if there are no authors left
brew.remove((err)=>{
if(err) return res.status(500).send('Error while removing');
return res.status(200).send();
});
} else {
// Otherwise, save the brew with updated author list
brew.save((err, savedBrew)=>{
if(err) throw err;
return res.status(200).send(savedBrew);
});
}
});
}; };
const newGoogleBrew = async (req, res, next)=>{ const newBrew = async (req, res)=>{
let oAuth2Client;
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); }
const brew = req.body; const brew = req.body;
const { transferToGoogle } = req.query;
if(!brew.title) {
brew.title = getGoodBrewTitle(brew.text);
}
brew.authors = (req.account) ? [req.account.username] : [];
brew.text = mergeBrewText(brew);
delete brew.editId; delete brew.editId;
delete brew.shareId; delete brew.shareId;
delete brew.googleId; delete brew.googleId;
req.body = brew; beforeNewSave(req.account, brew);
try { let saved;
const newBrew = await GoogleActions.newGoogleBrew(oAuth2Client, brew); if(transferToGoogle) {
return res.status(200).send(newBrew); saved = await newGoogleBrew(req.account, brew, res)
} catch (err) { .catch((err)=>{
return res.status(err.response.status).send(err); res.status(err.status || err.response.status).send(err.message || err);
});
} else {
saved = await newLocalBrew(brew)
.catch((err)=>{
res.status(500).send(err);
});
}
if(!saved) return;
return res.status(200).send(saved);
};
const updateBrew = async (req, res)=>{
let brew = excludePropsFromUpdate(req.body);
const { transferToGoogle, transferFromGoogle } = req.query;
let saved;
if(brew.googleId && transferFromGoogle) {
beforeNewSave(req.account, brew);
saved = await newLocalBrew(brew)
.catch((err)=>{
console.error(err);
res.status(500).send(err);
});
if(!saved) return;
await deleteGoogleBrew(req.account, `${brew.googleId}${brew.editId}`, res)
.catch((err)=>{
console.error(err);
res.status(err.status || err.response.status).send(err.message || err);
});
} else if(!brew.googleId && transferToGoogle) {
saved = await newGoogleBrew(req.account, brew, res)
.catch((err)=>{
console.error(err);
res.status(err.status || err.response.status).send(err.message || err);
});
if(!saved) return;
await deleteLocalBrew(req.account, brew.editId)
.catch((err)=>{
console.error(err);
res.status(err.status).send(err.message);
});
} else if(brew.googleId) {
brew.text = mergeBrewText(brew);
saved = await GoogleActions.updateGoogleBrew(brew)
.catch((err)=>{
console.error(err);
res.status(err.response?.status || 500).send(err);
});
} else {
const dbBrew = await HomebrewModel.get({ editId: req.params.id })
.catch((err)=>{
console.error(err);
return res.status(500).send('Error while saving');
});
brew = _.merge(dbBrew, brew);
brew.text = mergeBrewText(brew);
// 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));
}
brew.markModified('authors');
brew.markModified('systems');
saved = await brew.save();
}
if(!saved) return;
if(!res.headersSent) return res.status(200).send(saved);
};
const deleteBrew = async (req, res)=>{
if(req.params.id.length > 12) {
const deleted = await deleteGoogleBrew(req.account, req.params.id, res)
.catch((err)=>{
res.status(500).send(err);
});
if(deleted) return res.status(200).send();
} else {
const deleted = await deleteLocalBrew(req.account, req.params.id)
.catch((err)=>{
res.status(err.status).send(err.message);
});
if(deleted) return res.status(200).send(deleted);
return res.status(200).send();
} }
}; };
const updateGoogleBrew = async (req, res, next)=>{ const deleteLocalBrew = async (account, id)=>{
let oAuth2Client; const brew = await HomebrewModel.findOne({ editId: id });
if(!brew) {
throw { status: 404, message: 'Can not find homebrew with that id' };
}
try { oAuth2Client = GoogleActions.authCheck(req.account, res); } catch (err) { return res.status(err.status).send(err.message); } if(account) {
// Remove current user as author
brew.authors = _.pull(brew.authors, account.username);
brew.markModified('authors');
}
const brew = excludePropsFromUpdate(req.body); if(brew.authors.length === 0) {
brew.text = mergeBrewText(brew); // Delete brew if there are no authors left
await brew.remove()
try { .catch((err)=>{
const updatedBrew = await GoogleActions.updateGoogleBrew(oAuth2Client, brew); console.error(err);
return res.status(200).send(updatedBrew); throw { status: 500, message: 'Error while removing' };
} catch (err) { });
return res.status(err.response?.status || 500).send(err); } else {
// Otherwise, save the brew with updated author list
return await brew.save()
.catch((err)=>{
throw { status: 500, message: err };
});
} }
}; };
router.post('/api', newBrew); const deleteGoogleBrew = async (account, id, res)=>{
router.post('/api/newGoogle/', newGoogleBrew); const auth = await GoogleActions.authCheck(account, res);
router.put('/api/:id', updateBrew); await GoogleActions.deleteGoogleBrew(auth, id);
router.put('/api/update/:id', updateBrew); return true;
router.put('/api/updateGoogle/:id', updateGoogleBrew); };
router.delete('/api/:id', deleteBrew);
router.get('/api/remove/:id', deleteBrew); router.post('/api', asyncHandler(newBrew));
router.get('/api/removeGoogle/:id', (req, res)=>{GoogleActions.deleteGoogleBrew(req, res, req.params.id);}); router.put('/api/:id', asyncHandler(updateBrew));
router.put('/api/update/:id', asyncHandler(updateBrew));
router.delete('/api/:id', asyncHandler(deleteBrew));
router.get('/api/remove/:id', asyncHandler(deleteBrew));
module.exports = router; module.exports = router;

View File

@@ -10,43 +10,69 @@ const SplitPane = createClass({
return { return {
storageKey : 'naturalcrit-pane-split', storageKey : 'naturalcrit-pane-split',
onDragFinish : function(){} //fires when dragging onDragFinish : function(){} //fires when dragging
}; };
}, },
getInitialState : function() { getInitialState : function() {
return { return {
size : null, currentDividerPos : null,
isDragging : false windowWidth : 0,
isDragging : false
}; };
}, },
componentDidMount : function() { componentDidMount : function() {
const paneSize = window.localStorage.getItem(this.props.storageKey); const dividerPos = window.localStorage.getItem(this.props.storageKey);
if(paneSize){ if(dividerPos){
this.setState({ this.setState({
size : paneSize currentDividerPos : this.limitPosition(dividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13)),
userSetDividerPos : dividerPos,
windowWidth : window.innerWidth
}); });
} }
window.addEventListener('resize', this.handleWindowResize);
},
componentWillUnmount : function() {
window.removeEventListener('resize', this.handleWindowResize);
},
handleWindowResize : function() {
// Allow divider to increase in size to last user-set position
// Limit current position to between 10% and 90% of visible space
const newLoc = this.limitPosition(this.state.userSetDividerPos, 0.1*(window.innerWidth-13), 0.9*(window.innerWidth-13));
this.setState({
currentDividerPos : newLoc,
windowWidth : window.innerWidth
});
},
limitPosition : function(x, min = 1, max = window.innerWidth - 13) {
const result = Math.round(Math.min(max, Math.max(min, x)));
return result;
}, },
handleUp : function(){ handleUp : function(){
if(this.state.isDragging){ if(this.state.isDragging){
this.props.onDragFinish(this.state.size); this.props.onDragFinish(this.state.currentDividerPos);
window.localStorage.setItem(this.props.storageKey, this.state.size); window.localStorage.setItem(this.props.storageKey, this.state.currentDividerPos);
} }
this.setState({ isDragging: false }); this.setState({ isDragging: false });
}, },
handleDown : function(){ handleDown : function(){
this.setState({ isDragging: true }); this.setState({ isDragging: true });
//this.unFocus() //this.unFocus()
}, },
handleMove : function(e){ handleMove : function(e){
if(!this.state.isDragging) return; if(!this.state.isDragging) return;
const minWidth = 1; const newSize = this.limitPosition(e.pageX);
const maxWidth = window.innerWidth - 13;
const newSize = Math.min(maxWidth, Math.max(minWidth, e.pageX));
this.setState({ this.setState({
size : newSize currentDividerPos : newSize,
userSetDividerPos : newSize
}); });
}, },
/* /*
@@ -70,7 +96,7 @@ const SplitPane = createClass({
render : function(){ render : function(){
return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}> return <div className='splitPane' onMouseMove={this.handleMove} onMouseUp={this.handleUp}>
<Pane ref='pane1' width={this.state.size}>{this.props.children[0]}</Pane> <Pane ref='pane1' width={this.state.currentDividerPos}>{this.props.children[0]}</Pane>
{this.renderDivider()} {this.renderDivider()}
<Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane> <Pane ref='pane2' isDragging={this.state.isDragging}>{this.props.children[1]}</Pane>
</div>; </div>;

View File

@@ -98,7 +98,11 @@ module.exports = [
gen : dedent`/* Removes Drop Caps */ gen : dedent`/* Removes Drop Caps */
.page h1+p:first-letter { .page h1+p:first-letter {
all: unset; all: unset;
}\n\n` }\n\n
/* Removes Small-Caps in first line */
.page h1+p:first-line {
all: unset;
}`
}, },
{ {
name : 'Tweak Drop Cap', name : 'Tweak Drop Cap',

View File

@@ -621,6 +621,8 @@ body {
} }
&.decoration { &.decoration {
transform-style : preserve-3d; transform-style : preserve-3d;
z-index: -1;
position:relative;
} }
&.decoration::before { &.decoration::before {
content :''; content :'';
@@ -656,7 +658,7 @@ body {
margin-bottom : 0.3cm; margin-bottom : 0.3cm;
} }
a{ a{
display : table; display : inline;
color : inherit; color : inherit;
text-decoration : none; text-decoration : none;
&:hover{ &:hover{