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

Compare commits

..

54 Commits

Author SHA1 Message Date
Scott Tolksdorf
cbab4f4959 added todo 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
22d9982888 Added support for title description and thumbnail images 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
76ced9ca49 Added comma parsing to the block code 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
b1db8040a4 Added a todo for generic line styling 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
c8b089f7fb Added new lexer for handling the new block syntax 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
97c0443c76 Fixed bug where new page was storing null brews 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
c470bed591 'Created 2017-01-28 16:38:51 -05:00
Scott Tolksdorf
4593099914 Merge branch 'newAdmin' into v3 2017-01-28 16:36:25 -05:00
Scott Tolksdorf
6030134de2 New brew search finished on admin page 2017-01-28 16:35:48 -05:00
Scott Tolksdorf
7e6f42f062 Merge branch 'search' into newAdmin 2017-01-28 15:39:05 -05:00
Scott Tolksdorf
75111acefb All tests should be done, phew 2017-01-28 12:25:26 -05:00
Scott Tolksdorf
26bcb3395a Fixing edge cases in the search tests 2017-01-27 19:47:45 -05:00
Scott Tolksdorf
a826aaffd9 created a brew generator and chai plugin for easier testing 2017-01-27 18:38:51 -05:00
Scott Tolksdorf
8018442f25 Upgrading the brew generation for testing 2017-01-27 10:47:38 -05:00
Scott Tolksdorf
8e58e5aca9 MOved the brews search to its own file, writing out more tests 2017-01-27 10:36:07 -05:00
Scott Tolksdorf
2f82d3875e Adding a new script from populating the DB with a bunch of random brews 2017-01-27 09:56:44 -05:00
Scott Tolksdorf
efee8ff05c Basic search is working 2017-01-23 00:35:30 -05:00
Scott Tolksdorf
a405c7cfb2 Stubbing out tests for searching 2017-01-22 13:42:40 -05:00
Scott Tolksdorf
dfcb04fd09 Setting up search tests 2017-01-22 13:42:32 -05:00
Scott Tolksdorf
728277f861 Removing invalid brews is working 2017-01-22 13:41:16 -05:00
Scott Tolksdorf
0878439750 Fixed issue with arrays not being saved 2017-01-22 13:41:08 -05:00
Scott Tolksdorf
e77532acef Added tests for admin routes, lookup is working 2017-01-22 13:41:00 -05:00
Scott Tolksdorf
cd2eb5fdce Adding in lookup route 2017-01-22 13:40:39 -05:00
Scott Tolksdorf
37de888f03 Creating a brand new admin page 2017-01-22 13:40:29 -05:00
Scott Tolksdorf
1aa79b32d9 Remove chrome warnings from after rebase 2017-01-22 12:55:00 -05:00
Scott Tolksdorf
894e345a44 Fixed bug where new page loaded null brews sometimes 2017-01-22 12:49:35 -05:00
Scott Tolksdorf
07f249b23e Local login now working great 2017-01-22 12:49:35 -05:00
Scott Tolksdorf
baaa82ed34 Added in a logout to the user page 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
0d0f0d8eb0 Adding in env configs and aextra protection on dev routes 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
d77fa0a3dc Backend of local login working 2017-01-22 12:49:22 -05:00
Scott Tolksdorf
a26c4e2092 Cleaned up the admin routes 2017-01-22 12:48:48 -05:00
Scott Tolksdorf
ca40ec5a2d Added in full test coverage of current spec 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
987363ed41 Fixing the pathing to the build folder 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
7b38bccec1 config file for tests 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
174c2973f7 Split off app into own file 2017-01-22 12:47:33 -05:00
Scott Tolksdorf
66ca09b36d Both types of tests are now working 2017-01-22 12:46:16 -05:00
Scott Tolksdorf
5820564894 added nodemon'd test npm task 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
3dc4c13178 Something is up 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
537a75b2ab Triyng to setup api tests 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
a0bc4fddf8 moving the db setup out 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
25e0a1607a whatever 2017-01-22 12:45:46 -05:00
Scott Tolksdorf
68ecf749ea Stubbing out test files 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
10f4759471 adding in some api tests 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
5ba3f98696 Finally testing, things should be working a bit better now 2017-01-22 12:45:32 -05:00
Scott Tolksdorf
95c09ba7ad moved welcome message and adding in egads errors 2017-01-22 12:45:16 -05:00
Scott Tolksdorf
1173af5803 'Created 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
924b398768 Added new navitems 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
41303e6918 'Share 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
f75f60aa1e Edit page finally converted over 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
f4cf288f27 newPage is now working, working on editpage 2017-01-22 12:44:38 -05:00
Scott Tolksdorf
8abf6abf99 Updating pico-flux and vitreum to latest 2017-01-22 12:39:42 -05:00
Scott Tolksdorf
95aa803c61 Trying to fix prod builds breaking 2017-01-22 12:39:27 -05:00
Scott Tolksdorf
47396e5c7e Added smart componenets, page line number highlighting 2017-01-22 12:39:06 -05:00
Scott Tolksdorf
7581d155a6 Updating libs and adding basic flux 2017-01-22 12:36:45 -05:00
99 changed files with 2830 additions and 1330 deletions

View File

@@ -1,6 +0,0 @@
{
"presets": [
"env",
"react"
]
}

View File

@@ -1,15 +1,5 @@
# changelog # changelog
### Saturday, 22/04/217 - v2.7.4
- Give ability to hide the render warning notification
### Friday, 03/03/2017 - v2.7.3
- Increasing the range on the Partial Page Rendering for a quick-fix for it getting out of sync on long brews.
### Saturday, 18/02/2017 - v2.7.2
- Adding ability to delete a brew from the user page, incase the user creates a brew that makes the edit page unrender-able. (re:309)
## BIG NEWS ## BIG NEWS
With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues. With the next major release of Homebrewery, v3.0.0, this tool *will no longer support raw HTML input for brew code*. Most issues and errors users are having are because of this feature and it's become too taxing to help and fix these issues.
@@ -36,7 +26,8 @@ All brews made previous to the release of v3.0.0 will still render normally.
- Fixed realtime renderer not functioning if loaded with malformed html on load (thanks u/RattiganIV re:247) - Fixed realtime renderer not functioning if loaded with malformed html on load (thanks u/RattiganIV re:247)
- Removed a lot of unused files in shared - Removed a lot of unused files in shared
- vitreum v4 now lets me use codemirror as a pure node dependacy - vitreum v4 now lets me use codemirror as a pure node dependacy
- Moved the brew editor and renderer into shared, and made a new hybrid component for them
- Added a line highlighter to lines with new pages
### Saturday, 03/12/2016 - v2.6.0 ### Saturday, 03/12/2016 - v2.6.0

View File

@@ -1,38 +1,41 @@
var React = require('react'); const React = require('react');
var _ = require('lodash'); const _ = require('lodash');
var cx = require('classnames');
var HomebrewAdmin = require('./homebrewAdmin/homebrewAdmin.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
var Admin = React.createClass({ const BrewLookup = require('./brewLookup/brewLookup.jsx');
const AdminSearch = require('./adminSearch/adminSearch.jsx');
const InvalidBrew = require('./invalidBrew/invalidBrew.jsx');
const Admin = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
url : "", admin_key : '',
admin_key : "",
homebrews : [],
}; };
}, },
renderNavbar : function(){
return <Nav.base>
<Nav.section>
<Nav.item icon='fa-magic' className='homebreweryLogo'>
Homebrewery Admin
</Nav.item>
</Nav.section>
</Nav.base>
},
render : function(){ render : function(){
var self = this; return <div className='admin'>
return( {this.renderNavbar()}
<div className='admin'> <main className='content'>
<BrewLookup adminKey={this.props.admin_key} />
<AdminSearch adminKey={this.props.admin_key} />
<header> <div className='dangerZone'>Danger Zone</div>
<div className='container'>
<i className='fa fa-rocket' />
naturalcrit admin
</div>
</header>
<div className='container'> <InvalidBrew adminKey={this.props.admin_key} />
</main>
<HomebrewAdmin homebrews={this.props.homebrews} admin_key={this.props.admin_key} /> </div>
</div>
</div>
);
} }
}); });

View File

@@ -1,39 +1,53 @@
@import 'naturalcrit/styles/reset.less';
@import 'naturalcrit/styles/elements.less';
@import 'naturalcrit/styles/animations.less';
@import 'naturalcrit/styles/colors.less';
@import 'naturalcrit/styles/tooltip.less';
@import 'font-awesome/css/font-awesome.css'; @import 'naturalcrit/styles/core.less';
html,body, #reactRoot{
html,body, #reactContainer, .naturalCrit{
min-height : 100%; min-height : 100%;
} }
@sidebarWidth : 250px;
body{ body{
background-color : #eee; height : 100%;
font-family : 'Open Sans', sans-serif;
color : #4b5055;
font-weight : 100;
text-rendering : optimizeLegibility;
margin : 0; margin : 0;
padding : 0; padding : 0;
height : 100%; background-color : #ddd;
font-family : 'Open Sans', sans-serif;
font-weight : 100;
color : #4b5055;
text-rendering : optimizeLegibility;
} }
.admin {
.admin{ nav {
header{
background-color : @red; background-color : @red;
font-size: 2em; .navItem{
padding : 20px 0px; background-color : @red;
color : white; }
margin-bottom: 30px; .homebreweryLogo{
i{ font-family : CodeBold;
margin-right: 30px; font-size : 12px;
color : white;
div{
margin-top : 2px;
margin-bottom : -2px;
}
}
}
h1{
margin-bottom : 10px;
font-size : 2em;
font-weight : 800;
border-bottom : 1px solid #ddd;
}
main.content{
width : 1000px;
margin : 0 auto;
padding : 50px 20px;
background-color : white;
.dangerZone{
margin : 30px 0px;
padding : 10px 20px;
background : repeating-linear-gradient(45deg, @yellow, @yellow 10px, darken(#333, 10%) 10px, darken(#333, 10%) 20px);
font-size : 1em;
font-weight : 800;
color : white;
text-transform : uppercase;
} }
} }
} }

View File

@@ -0,0 +1,86 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const BrewTable = require('../brewTable/brewTable.jsx');
const LIMIT = 10;
const AdminSearch = React.createClass({
getDefaultProps: function() {
return {
adminKey : '',
};
},
getInitialState: function() {
return {
totalBrews : 1,
brews: [],
searching : false,
error : null,
page : 1,
searchTerms : ''
};
},
handleSearch : function(e){
this.setState({
searchTerms : e.target.value
});
},
handlePage : function(e){
this.setState({
page : e.target.value
});
},
search : function(){
this.setState({ searching : true, error : null });
request.get(`/api/brew`)
.query({
terms : this.state.searchTerms,
limit : LIMIT,
page : this.state.page - 1
})
.set('x-homebrew-admin', this.props.adminKey)
.end((err, res) => {
if(err){
this.setState({
searching : false,
error : err && err.toString()
});
}else{
this.setState({
brews : res.body.brews,
totalBrews : res.body.total
});
}
});
},
render: function(){
return <div className='adminSearch'>
<h1>Admin Search</h1>
<div className='controls'>
<input className='search' type='text' value={this.state.searchTerms} onChange={this.handleSearch} />
<button onClick={this.search}> <i className='fa fa-search' /> search </button>
<div className='page'>
page:
<input type='text' value={this.state.page} onChange={this.handlePage} />
/ {Math.ceil(this.state.totalBrews / LIMIT)}
</div>
</div>
<BrewTable brews={this.state.brews} />
</div>
}
});
module.exports = AdminSearch;

View File

@@ -0,0 +1,17 @@
.adminSearch{
.controls{
margin-bottom : 20px;
input.search{
height : 33px;
padding : 10px;
}
.page {
float : right;
font-weight : 800;
input{
width : 20px;
}
}
}
}

View File

@@ -3,8 +3,7 @@ const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require('superagent'); const request = require('superagent');
const Moment = require('moment'); const BrewTable = require('../brewTable/brewTable.jsx');
const BrewLookup = React.createClass({ const BrewLookup = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
@@ -16,7 +15,8 @@ const BrewLookup = React.createClass({
return { return {
query:'', query:'',
resultBrew : null, resultBrew : null,
searching : false searching : false,
error : null
}; };
}, },
@@ -26,13 +26,14 @@ const BrewLookup = React.createClass({
}) })
}, },
lookup : function(){ lookup : function(){
this.setState({ searching : true }); this.setState({ searching : true, error : null });
request.get(`/admin/lookup/${this.state.query}`) request.get(`/admin/lookup/${this.state.query}`)
.query({ admin_key : this.props.adminKey }) .set('x-homebrew-admin', this.props.adminKey)
.end((err, res) => { .end((err, res) => {
this.setState({ this.setState({
searching : false, searching : false,
error : err && err.toString(),
resultBrew : (err ? null : res.body) resultBrew : (err ? null : res.body)
}); });
}) })
@@ -42,14 +43,31 @@ const BrewLookup = React.createClass({
if(this.state.searching) return <div className='searching'><i className='fa fa-spin fa-spinner' /></div>; if(this.state.searching) return <div className='searching'><i className='fa fa-spin fa-spinner' /></div>;
if(!this.state.resultBrew) return <div className='noBrew'>No brew found.</div>; if(!this.state.resultBrew) return <div className='noBrew'>No brew found.</div>;
return <BrewTable brews={[this.state.resultBrew ]} />
/*
const brew = this.state.resultBrew; const brew = this.state.resultBrew;
return <div className='brewRow'> return <div className='brewRow'>
<div>{brew.title}</div> <div>{brew.title}</div>
<div>{brew.authors.join(', ')}</div> <div>{brew.authors.join(', ')}</div>
<div><a href={'/edit/' + brew.editId} target='_blank'>/edit/{brew.editId}</a></div> <div><a href={'/edit/' + brew.editId} target='_blank'>{brew.editId}</a></div>
<div><a href={'/share/' + brew.shareId} target='_blank'>/share/{brew.shareId}</a></div> <div><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></div>
<div>{Moment(brew.updatedAt).fromNow()}</div> <div>{Moment(brew.updatedAt).fromNow()}</div>
<div>{brew.views}</div> <div>{brew.views}</div>
<div>
<div className='deleteButton'>
<i className='fa fa-trash' />
</div>
</div>
</div>
*/
},
renderError : function(){
if(!this.state.error) return;
return <div className='error'>
{this.state.error}
</div> </div>
}, },
@@ -60,6 +78,7 @@ const BrewLookup = React.createClass({
<button onClick={this.lookup}><i className='fa fa-search'/></button> <button onClick={this.lookup}><i className='fa fa-search'/></button>
{this.renderFoundBrew()} {this.renderFoundBrew()}
{this.renderError()}
</div> </div>
} }
}); });

View File

@@ -0,0 +1,13 @@
.brewLookup{
height : 200px;
input{
height : 33px;
margin-bottom : 20px;
padding : 0px 10px;
}
.error{
font-weight : 800;
color : @red;
}
}

View File

@@ -0,0 +1,54 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Moment = require('moment');
//TODO: Add in delete
const BrewTable = React.createClass({
getDefaultProps: function() {
return {
brews : []
};
},
renderRows : function(){
return _.map(this.props.brews, (brew) => {
let authors = 'None.';
if(brew.authors && brew.authors.length) authors = brew.authors.join(', ');
return <tr className={cx('brewRow', {'isEmpty' : brew.text == "false"})} key={brew.shareId || brew}>
<td>{brew.title}</td>
<td>{authors}</td>
<td><a href={'/edit/' + brew.editId} target='_blank'>{brew.editId}</a></td>
<td><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></td>
<td>{Moment(brew.updatedAt).fromNow()}</td>
<td>{brew.views}</td>
<td className='deleteButton'>
<i className='fa fa-trash' />
</td>
</tr>
});
},
render: function(){
return <table className='brewTable'>
<thead>
<tr>
<th>Title</th>
<th>Authors</th>
<th>Edit Link</th>
<th>Share Link</th>
<th>Last Updated</th>
<th>Views</th>
<th>Remove</th>
</tr>
</thead>
<tbody>
{this.renderRows()}
</tbody>
</table>
}
});
module.exports = BrewTable;

View File

@@ -0,0 +1,44 @@
table.brewTable{
th{
padding : 10px;
background-color : fade(@blue, 20%);
font-weight : 800;
}
tr:nth-child(even){
background-color : fade(@green, 10%);
}
tr.isEmpty{
background-color : fade(@red, 30%);
}
td{
min-width : 100px;
padding : 10px;
text-align : center;
/*
&.preview{
position : relative;
&:hover{
.content{
display : block;
}
}
.content{
position : absolute;
display : none;
top : 100%;
left : 0px;
z-index : 1000;
max-height : 500px;
width : 300px;
padding : 30px;
background-color : white;
font-family : monospace;
text-align : left;
pointer-events : none;
}
}
*/
}
}

View File

@@ -1,8 +0,0 @@
.brewLookup{
height : 200px;
input{
height : 33px;
padding : 0px 10px;
margin-bottom: 20px;
}
}

View File

@@ -1,72 +0,0 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var request = require('superagent');
var BrewSearch = React.createClass({
getDefaultProps: function() {
return {
admin_key : ''
};
},
getInitialState: function() {
return {
searchTerm: '',
brew : null,
searching : false
};
},
search : function(){
this.setState({
searching : true
});
request.get('/homebrew/api/search?id=' + this.state.searchTerm)
.query({
admin_key : this.props.admin_key,
})
.end((err, res)=>{
console.log(err, res, res.body.brews[0]);
this.setState({
brew : res.body.brews[0],
searching : false
})
});
},
handleChange : function(e){
this.setState({
searchTerm : e.target.value
});
},
handleSearchClick : function(){
this.search();
},
renderBrew : function(){
if(!this.state.brew) return null;
return <div className='brew'>
<div>Edit id : {this.state.brew.editId}</div>
<div>Share id : {this.state.brew.shareId}</div>
</div>
},
render : function(){
return <div className='search'>
<input type='text' value={this.state.searchTerm} onChange={this.handleChange} />
<button onClick={this.handleSearchClick}>Search</button>
{this.renderBrew()}
</div>
},
});
module.exports = BrewSearch;

View File

@@ -1,172 +0,0 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var request = require('superagent');
var Moment = require('moment');
var BrewSearch = require('./brewSearch.jsx');
var BrewLookup = require('./brewLookup/brewLookup.jsx');
var HomebrewAdmin = React.createClass({
getDefaultProps: function() {
return {
admin_key : ''
};
},
getInitialState: function() {
return {
page: 0,
count : 20,
brewCache : {},
total : 0,
processingOldBrews : false
};
},
fetchBrews : function(page){
request.get('/api/search')
.query({
admin_key : this.props.admin_key,
count : this.state.count,
page : page
})
.end((err, res)=>{
if(err || !res.body || !res.body.brews) return;
this.state.brewCache[page] = res.body.brews;
this.setState({
brewCache : this.state.brewCache,
total : res.body.total,
count : res.body.count
})
})
},
componentDidMount: function() {
this.fetchBrews(this.state.page);
},
changePageTo : function(page){
if(!this.state.brewCache[page]){
this.fetchBrews(page);
}
this.setState({
page : page
})
},
clearInvalidBrews : function(){
request.get('/api/invalid')
.query({admin_key : this.props.admin_key})
.end((err, res)=>{
if(!confirm("This will remove " + res.body.count + " brews. Are you sure?")) return;
request.get('/api/invalid')
.query({admin_key : this.props.admin_key, do_it : true})
.end((err, res)=>{
alert("Done!")
});
});
},
deleteBrew : function(brewId){
if(!confirm("Are you sure you want to delete '" + brewId + "'?")) return;
request.get('/api/remove/' + brewId)
.query({admin_key : this.props.admin_key})
.end(function(err, res){
window.location.reload();
})
},
handlePageChange : function(dir){
this.changePageTo(this.state.page + dir);
},
renderPagnination : function(){
var outOf;
if(this.state.total){
outOf = this.state.page + ' / ' + Math.round(this.state.total/this.state.count);
}
return <div className='pagnination'>
<i className='fa fa-chevron-left' onClick={this.handlePageChange.bind(this, -1)}/>
{outOf}
<i className='fa fa-chevron-right' onClick={this.handlePageChange.bind(this, 1)}/>
</div>
},
renderBrews : function(){
var brews = this.state.brewCache[this.state.page] || _.times(this.state.count);
return _.map(brews, (brew)=>{
return <tr className={cx('brewRow', {'isEmpty' : brew.text == "false"})} key={brew.shareId || brew}>
<td><a href={'/edit/' + brew.editId} target='_blank'>{brew.editId}</a></td>
<td><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></td>
<td>{Moment(brew.createdAt).fromNow()}</td>
<td>{Moment(brew.updatedAt).fromNow()}</td>
<td>{Moment(brew.lastViewed).fromNow()}</td>
<td>{brew.views}</td>
<td>
<div className='deleteButton' onClick={this.deleteBrew.bind(this, brew.editId)}>
<i className='fa fa-trash' />
</div>
</td>
</tr>
});
},
renderBrewTable : function(){
return <div className='brewTable'>
<table>
<thead>
<tr>
<th>Edit Id</th>
<th>Share Id</th>
<th>Created At</th>
<th>Last Updated</th>
<th>Last Viewed</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{this.renderBrews()}
</tbody>
</table>
</div>
},
render : function(){
var self = this;
return <div className='homebrewAdmin'>
<BrewLookup adminKey={this.props.admin_key} />
{/*
<h2>
Homebrews - {this.state.total}
</h2>
{this.renderPagnination()}
{this.renderBrewTable()}
<button className='clearOldButton' onClick={this.clearInvalidBrews}>
Clear Old
</button>
<BrewSearch admin_key={this.props.admin_key} />
*/}
</div>
}
});
module.exports = HomebrewAdmin;

View File

@@ -1,53 +0,0 @@
.homebrewAdmin{
margin-bottom: 80px;
.brewTable{
table{
th{
padding : 10px;
font-weight : 800;
}
tr:nth-child(even){
background-color : fade(@green, 10%);
}
tr.isEmpty{
background-color : fade(@red, 30%);
}
td{
min-width : 100px;
padding : 10px;
text-align : center;
&.preview{
position : relative;
&:hover{
.content{
display : block;
}
}
.content{
position : absolute;
display : none;
top : 100%;
left : 0px;
z-index : 1000;
max-height : 500px;
width : 300px;
padding : 30px;
background-color : white;
font-family : monospace;
text-align : left;
pointer-events : none;
}
}
}
}
}
.deleteButton{
cursor: pointer;
}
button.clearOldButton{
float : right;
}
}

View File

@@ -0,0 +1,54 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
const BrewTable = require('../brewTable/brewTable.jsx');
const InvalidBrew = React.createClass({
getDefaultProps: function() {
return {
adminKey : '',
};
},
getInitialState: function() {
return {
brews: []
};
},
getInvalid : function(){
request.get(`/admin/invalid`)
.set('x-homebrew-admin', this.props.adminKey)
.end((err, res) => {
this.setState({
brews : res.body
});
})
},
removeInvalid : function(){
if(!this.state.brews.length) return;
if(!confirm(`Are you sure you want to remove ${this.state.brews.length} brews`)) return;
if(!confirm('Sure you are sure?')) return;
request.delete(`/admin/invalid`)
.set('x-homebrew-admin', this.props.adminKey)
.end((err, res) => {
console.log(err, res.body);
alert('Invalid brews removed!');
this.getInvalid();
})
},
render: function(){
return <div className='invalidBrew'>
<h1>Remove Invalid Brews</h1>
<div>This will removes all brews older than 3 days and shorter than a tweet.</div>
<button className='get' onClick={this.getInvalid}> Get Invalid Brews</button>
<button className='remove' disabled={this.state.brews.length == 0} onClick={this.removeInvalid}> Remove invalid Brews</button>
<BrewTable brews={this.state.brews} />
</div>
}
});
module.exports = InvalidBrew;

View File

@@ -0,0 +1,5 @@
.invalidBrew{
button{
margin: 10px 4px;
}
}

View File

@@ -3,13 +3,15 @@ const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const CreateRouter = require('pico-router').createRouter; const CreateRouter = require('pico-router').createRouter;
const BrewActions = require('homebrewery/brew.actions.js');
const AccountActions = require('homebrewery/account.actions.js');
const HomePage = require('./pages/homePage/homePage.jsx'); const HomePage = require('./pages/homePage/homePage.jsx');
const EditPage = require('./pages/editPage/editPage.jsx'); const EditPage = require('./pages/editPage/editPage.jsx');
const UserPage = require('./pages/userPage/userPage.jsx'); const UserPage = require('./pages/userPage/userPage.jsx');
const SharePage = require('./pages/sharePage/sharePage.jsx'); const SharePage = require('./pages/sharePage/sharePage.jsx');
const NewPage = require('./pages/newPage/newPage.jsx'); const NewPage = require('./pages/newPage/newPage.jsx');
const ErrorPage = require('./pages/errorPage/errorPage.jsx'); //const ErrorPage = require('./pages/errorPage/errorPage.jsx');
const PrintPage = require('./pages/printPage/printPage.jsx'); const PrintPage = require('./pages/printPage/printPage.jsx');
let Router; let Router;
@@ -17,45 +19,27 @@ const Homebrew = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
url : '', url : '',
welcomeText : '',
changelog : '',
version : '0.0.0', version : '0.0.0',
account : null, loginPath : '',
brew : {
title : '', user : undefined,
text : '', brew : undefined,
shareId : null, brews : []
editId : null,
createdAt : null,
updatedAt : null,
}
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
global.account = this.props.account; BrewActions.init({
global.version = this.props.version; version : this.props.version,
brew : this.props.brew
});
AccountActions.init({
user : this.props.user,
loginPath : this.props.loginPath
});
Router = CreateRouter({ Router = CreateRouter({
'/edit/:id' : (args) => { '/edit/:id' : <EditPage />,
if(!this.props.brew.editId){ '/share/:id' : <SharePage />,
return <ErrorPage errorId={args.id}/>
}
return <EditPage
id={args.id}
brew={this.props.brew} />
},
'/share/:id' : (args) => {
if(!this.props.brew.shareId){
return <ErrorPage errorId={args.id}/>
}
return <SharePage
id={args.id}
brew={this.props.brew} />
},
'/user/:username' : (args) => { '/user/:username' : (args) => {
return <UserPage return <UserPage
username={args.username} username={args.username}
@@ -68,15 +52,9 @@ const Homebrew = React.createClass({
'/print' : (args, query) => { '/print' : (args, query) => {
return <PrintPage query={query}/>; return <PrintPage query={query}/>;
}, },
'/new' : (args) => { '/new' : <NewPage />,
return <NewPage /> '/changelog' : <SharePage />,
}, '*' : <HomePage />,
'/changelog' : (args) => {
return <SharePage
brew={{title : 'Changelog', text : this.props.changelog}} />
},
'*' : <HomePage
welcomeText={this.props.welcomeText} />,
}); });
}, },
render : function(){ render : function(){

View File

@@ -1,17 +1,23 @@
const React = require('react'); const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/account.store.js');
const Actions = require('homebrewery/account.actions.js');
module.exports = function(props){ module.exports = function(props){
if(global.account){ const user = Store.getUser();
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fa-user'> if(user && user == props.userPage){
{global.account.username} return <Nav.item onClick={Actions.logout} color='yellow' icon='fa-user-times'>
logout
</Nav.item> </Nav.item>
} }
let url = ''; if(user){
if(typeof window !== 'undefined'){ return <Nav.item href={`/user/${user}`} color='yellow' icon='fa-user'>
url = window.location.href {user}
</Nav.item>
} }
return <Nav.item href={`http://naturalcrit.com/login?redirect=${url}`} color='teal' icon='fa-sign-in'> return <Nav.item onClick={Actions.login} color='teal' icon='fa-sign-in'>
login login
</Nav.item> </Nav.item>
}; };

View File

@@ -0,0 +1,9 @@
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
module.exports = Store.createSmartComponent((props) => {
return <Nav.item className='brewTitle'>{props.title}</Nav.item>
}, (props) => {
return {title : Store.getMetaData().title};
})

View File

@@ -0,0 +1,76 @@
const flux = require('pico-flux')
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const onStoreChange = () => {
return {
status : Store.getStatus(),
errors : Store.getErrors()
}
};
const ContinousSave = React.createClass({
getDefaultProps: function() {
return {
status : 'ready',
errors : undefined
};
},
componentDidMount: function() {
flux.actionEmitter.on('dispatch', this.actionHandler);
window.onbeforeunload = ()=>{
if(this.props.status !== 'ready') return 'You have unsaved changes!';
};
},
componentWillUnmount: function() {
flux.actionEmitter.removeListener('dispatch', this.actionHandler);
window.onbeforeunload = function(){};
},
actionHandler : function(actionType){
if(actionType == 'UPDATE_BREW_TEXT' || actionType == 'UPDATE_META'){
Actions.pendingSave();
}
},
handleClick : function(){
Actions.save();
},
renderError : function(){
let errMsg = '';
try{
errMsg += this.state.errors.toString() + '\n\n';
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
}catch(e){}
return <Nav.item className='continousSave error' icon="fa-warning">
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}>
here
</a>.
</div>
</Nav.item>
},
render : function(){
if(this.props.status == 'error') return this.renderError();
if(this.props.status == 'saving'){
return <Nav.item className='continousSave' icon="fa-spinner fa-spin">saving...</Nav.item>
}
if(this.props.status == 'pending'){
return <Nav.item className='continousSave' onClick={this.handleClick} color='blue' icon='fa-save'>Save Now</Nav.item>
}
if(this.props.status == 'ready'){
return <Nav.item className='continousSave saved'>saved.</Nav.item>
}
},
});
module.exports = Store.createSmartComponent(ContinousSave, onStoreChange);

View File

@@ -0,0 +1,76 @@
const flux = require('pico-flux')
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const onStoreChange = () => {
return {
status : Store.getStatus(),
errors : Store.getErrors()
}
};
const ContinousSave = React.createClass({
getDefaultProps: function() {
return {
status : 'ready',
errors : undefined
};
},
componentDidMount: function() {
flux.actionEmitter.on('dispatch', this.actionHandler);
window.onbeforeunload = ()=>{
if(this.props.status !== 'ready') return 'You have unsaved changes!';
};
},
componentWillUnmount: function() {
flux.actionEmitter.removeListener('dispatch', this.actionHandler);
window.onbeforeunload = function(){};
},
actionHandler : function(actionType){
if(actionType == 'UPDATE_BREW_TEXT' || actionType == 'UPDATE_META'){
Actions.pendingSave();
}
},
handleClick : function(){
Actions.save();
},
renderError : function(){
let errMsg = '';
try{
errMsg += this.state.errors.toString() + '\n\n';
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
}catch(e){}
return <Nav.item className='continousSave error' icon="fa-warning">
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}>
here
</a>.
</div>
</Nav.item>
},
render : function(){
if(this.props.status == 'error') return this.renderError();
if(this.props.status == 'saving'){
return <Nav.item className='continousSave' icon="fa-spinner fa-spin">saving...</Nav.item>
}
if(this.props.status == 'pending'){
return <Nav.item className='continousSave' onClick={this.handleClick} color='blue' icon='fa-save'>Save Now</Nav.item>
}
if(this.props.status == 'ready'){
return <Nav.item className='continousSave saved'>saved.</Nav.item>
}
},
});
module.exports = Store.createSmartComponent(ContinousSave, onStoreChange);

View File

@@ -2,34 +2,9 @@ const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const Nav = require('naturalcrit/nav/nav.jsx'); const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Navbar = React.createClass({ const Navbar = React.createClass({
getInitialState: function() {
return {
//showNonChromeWarning : false,
ver : '0.0.0'
};
},
componentDidMount: function() {
//const isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
this.setState({
//showNonChromeWarning : !isChrome,
ver : window.version
})
},
/*
renderChromeWarning : function(){
if(!this.state.showNonChromeWarning) return;
return <Nav.item className='warning' icon='fa-exclamation-triangle'>
Optimized for Chrome
<div className='dropdown'>
If you are experiencing rendering issues, use Chrome instead
</div>
</Nav.item>
},
*/
render : function(){ render : function(){
return <Nav.base> return <Nav.base>
<Nav.section> <Nav.section>
@@ -37,9 +12,7 @@ const Navbar = React.createClass({
<Nav.item href='/' className='homebrewLogo'> <Nav.item href='/' className='homebrewLogo'>
<div>The Homebrewery</div> <div>The Homebrewery</div>
</Nav.item> </Nav.item>
<Nav.item>{`v${this.state.ver}`}</Nav.item> <Nav.item>{`v${Store.getVersion()}`}</Nav.item>
{/*this.renderChromeWarning()*/}
</Nav.section> </Nav.section>
{this.props.children} {this.props.children}
</Nav.base> </Nav.base>

View File

@@ -13,32 +13,6 @@
color : @blue; color : @blue;
} }
} }
.editTitle.navItem{
padding : 2px 12px;
input{
width : 250px;
margin : 0;
padding : 2px;
background-color : #444;
font-family : 'Open Sans', sans-serif;
font-size : 12px;
font-weight : 800;
color : white;
text-align : center;
border : 1px solid @blue;
outline : none;
}
.charCount{
display : inline-block;
vertical-align : bottom;
margin-left : 8px;
color : #666;
text-align : right;
&.max{
color : @red;
}
}
}
.brewTitle.navItem{ .brewTitle.navItem{
font-size : 12px; font-size : 12px;
font-weight : 800; font-weight : 800;
@@ -125,4 +99,34 @@
text-align : center; text-align : center;
} }
} }
.staticSave.navItem{
background-color : @orange;
&:hover{
background-color : @green;
}
}
.continousSave.navItem{
width : 105px;
text-align : center;
&.saved{
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
.errorContainer{
position : absolute;
top : 29px;
left : -20px;
z-index : 1000;
width : 120px;
padding : 8px;
background-color : #333;
a{
color : @teal;
}
}
}
}
} }

View File

@@ -0,0 +1,10 @@
module.exports = {
Account : require('./account.navitem.jsx'),
BrewTitle : require('./brewTitle.navitem.jsx'),
ContinousSave : require('./continousSave.navitem.jsx'),
Issue : require('./issue.navitem.jsx'),
Patreon : require('./patreon.navitem.jsx'),
Print : require('./print.navitem.jsx'),
Recent : require('./recent.navitem.jsx'),
StaticSave : require('./staticSave.navitem.jsx'),
};

View File

@@ -1,51 +0,0 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
//var striptags = require('striptags');
var Nav = require('naturalcrit/nav/nav.jsx');
const MAX_URL_SIZE = 2083;
const MAIN_URL = "https://www.reddit.com/r/UnearthedArcana/submit?selftext=true"
var RedditShare = React.createClass({
getDefaultProps: function() {
return {
brew : {
title : '',
sharedId : '',
text : ''
}
};
},
getText : function(){
},
handleClick : function(){
var url = [
MAIN_URL,
'title=' + encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!'),
'text=' + encodeURIComponent(this.props.brew.text)
].join('&');
window.open(url, '_blank');
},
render : function(){
return <Nav.item icon='fa-reddit-alien' color='red' onClick={this.handleClick}>
share on reddit
</Nav.item>
},
});
module.exports = RedditShare;

View File

@@ -0,0 +1,37 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Nav = require('naturalcrit/nav/nav.jsx');
const Store = require('homebrewery/brew.store.js');
const Actions = require('homebrewery/brew.actions.js');
const StaticSave = React.createClass({
getDefaultProps: function() {
return {
status : 'ready'
};
},
handleClick : function(){
Actions.create();
},
render : function(){
if(this.props.status === 'saving'){
return <Nav.item icon='fa-spinner fa-spin' className='staticSave'>
save...
</Nav.item>
}
if(this.props.status === 'ready'){
return <Nav.item icon='fa-save' className='staticSave' onClick={this.handleClick}>
save
</Nav.item>
}
}
});
module.exports = Store.createSmartComponent(StaticSave, ()=>{
return {
status : Store.getStatus()
}
});

View File

@@ -1,230 +1,63 @@
const React = require('react'); const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require("superagent");
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 Items = require('../../navbar/navitems.js');
const ReportIssue = require('../../navbar/issue.navitem.jsx'); const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx'); const Utils = require('homebrewery/utils.js');
const Account = require('../../navbar/account.navitem.jsx');
//const RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const Store = require('homebrewery/brew.store.js');
const Editor = require('../../editor/editor.jsx'); const Actions = require('homebrewery/brew.actions.js');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Markdown = require('naturalcrit/markdown.js');
const SAVE_TIMEOUT = 3000;
const EditPage = React.createClass({ const EditPage = React.createClass({
getDefaultProps: function() {
return {
brew : {
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
}
};
},
getInitialState: function() {
return {
brew : this.props.brew,
isSaving : false,
isPending : false,
errors : null,
htmlErrors : Markdown.validate(this.props.brew.text),
lastUpdated : this.props.brew.updatedAt
};
},
savedBrew : null,
componentDidMount: function(){ componentDidMount: function(){
this.trySave();
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.isPending){
return 'You have unsaved changes!';
}
};
this.setState({
htmlErrors : Markdown.validate(this.state.brew.text)
})
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
window.onbeforeunload = function(){};
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
handleControlKeys : Utils.controlKeys({
s : Actions.save,
handleControlKeys : function(e){ p : Actions.print
if(!(e.ctrlKey || e.metaKey)) return; }),
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save();
if(e.keyCode == P_KEY) window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleMetadataChange : function(metadata){
this.setState({
brew : _.merge({}, this.state.brew, metadata),
isPending : true,
}, ()=>{
this.trySave();
});
},
handleTextChange : function(text){
//If there are errors, run the validator on everychange to give quick feedback
var htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState({
brew : _.merge({}, this.state.brew, {text : text}),
isPending : true,
htmlErrors : htmlErrors
});
this.trySave();
},
hasChanges : function(){
if(this.savedBrew){
return !_.isEqual(this.state.brew, this.savedBrew)
}else{
return !_.isEqual(this.state.brew, this.props.brew)
}
return false;
},
trySave : function(){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.hasChanges()){
this.debounceSave();
}else{
this.debounceSave.cancel();
}
},
save : function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
this.setState({
isSaving : true,
errors : null,
htmlErrors : Markdown.validate(this.state.brew.text)
});
request
.put('/api/update/' + this.props.brew.editId)
.send(this.state.brew)
.end((err, res) => {
if(err){
this.setState({
errors : err,
})
}else{
this.savedBrew = res.body;
this.setState({
isPending : false,
isSaving : false,
lastUpdated : res.body.updatedAt
})
}
})
},
renderSaveButton : function(){
if(this.state.errors){
var errMsg = '';
try{
errMsg += this.state.errors.toString() + '\n\n';
errMsg += '```\n' + JSON.stringify(this.state.errors.response.error, null, ' ') + '\n```';
}catch(e){}
return <Nav.item className='save error' icon="fa-warning">
Oops!
<div className='errorContainer'>
Looks like there was a problem saving. <br />
Report the issue <a target='_blank' href={'https://github.com/stolksdorf/naturalcrit/issues/new?body='+ encodeURIComponent(errMsg)}>
here
</a>.
</div>
</Nav.item>
}
if(this.state.isSaving){
return <Nav.item className='save' icon="fa-spinner fa-spin">saving...</Nav.item>
}
if(this.state.isPending && this.hasChanges()){
return <Nav.item className='save' onClick={this.save} color='blue' icon='fa-save'>Save Now</Nav.item>
}
if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</Nav.item>
}
},
renderNavbar : function(){
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
{/*<RecentlyEdited brew={this.props.brew} />*/}
<ReportIssue />
<Nav.item newTab={true} href={'/share/' + this.props.brew.shareId} color='teal' icon='fa-share-alt'>
Share
</Nav.item>
<PrintLink shareId={this.props.brew.shareId} />
<Account />
</Nav.section>
</Navbar>
},
render : function(){ render : function(){
return <div className='editPage page'> return <div className='editPage page'>
{this.renderNavbar()} <SmartNav />
<div className='content'> <div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'> <BrewInterface />
<Editor
ref='editor'
value={this.state.brew.text}
onChange={this.handleTextChange}
metadata={this.state.brew}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} />
</SplitPane>
</div> </div>
</div> </div>
} }
}); });
const SmartNav = Store.createSmartComponent(React.createClass({
getDefaultProps: function() {
return {
brew : {}
};
},
render : function(){
return <Navbar>
<Nav.section>
<Items.BrewTitle />
</Nav.section>
<Nav.section>
<Items.ContinousSave />
<Items.Issue />
<Nav.item newTab={true} href={'/share/' + Store.getBrew().shareId} color='teal' icon='fa-share-alt'>
Share
</Nav.item>
<Items.Print />
<Items.Account />
</Nav.section>
</Navbar>
}
}), ()=>{
return {brew : Store.getBrew()}
});
module.exports = EditPage; module.exports = EditPage;

View File

@@ -1,27 +1,4 @@
.editPage{ .editPage{
.navItem.save{
width : 105px;
text-align : center;
&.saved{
cursor : initial;
color : #666;
}
&.error{
position : relative;
background-color : @red;
.errorContainer{
position : absolute;
top : 29px;
left : -20px;
z-index : 1000;
width : 120px;
padding : 8px;
background-color : #333;
a{
color : @teal;
}
}
}
}
} }

View File

@@ -1,7 +1,6 @@
const React = require('react'); const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const request = require("superagent");
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');
@@ -11,45 +10,19 @@ const RecentNavItem = require('../../navbar/recent.navitem.jsx');
const AccountNavItem = require('../../navbar/account.navitem.jsx'); const AccountNavItem = require('../../navbar/account.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const Actions = require('homebrewery/brew.actions.js');
//
const HomePage = React.createClass({ const HomePage = React.createClass({
getDefaultProps: function() {
return {
welcomeText : '',
ver : '0.0.0'
};
},
getInitialState: function() {
return {
text: this.props.welcomeText
};
},
handleSave : function(){ handleSave : function(){
request.post('/api') Actions.saveNew();
.send({
text : this.state.text
})
.end((err, res)=>{
if(err) return;
var brew = res.body;
window.location = '/edit/' + brew.editId;
});
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleTextChange : function(text){
this.setState({
text : text
});
}, },
renderNavbar : function(){ renderNavbar : function(){
return <Navbar ver={this.props.ver}> return <Navbar>
<Nav.section> <Nav.section>
<PatreonNavItem /> <PatreonNavItem />
<IssueNavItem /> <IssueNavItem />
@@ -70,15 +43,13 @@ const HomePage = React.createClass({
render : function(){ render : function(){
return <div className='homePage page'> return <div className='homePage page'>
{this.renderNavbar()} {this.renderNavbar()}
<div className='content'> <div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'> <BrewInterface />
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<BrewRenderer text={this.state.text} />
</SplitPane>
</div> </div>
<div className={cx('floatingSaveButton', {show : this.props.welcomeText != this.state.text})} onClick={this.handleSave}> <div className={cx('floatingSaveButton', {
//show : Store.getBrewText() !== this.props.welcomeText
})} onClick={this.handleSave}>
Save current <i className='fa fa-save' /> Save current <i className='fa fa-save' />
</div> </div>

View File

@@ -1,158 +1,59 @@
const React = require('react'); const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames');
const request = require("superagent");
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 Items = require('../../navbar/navitems.js');
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx'); const Store = require('homebrewery/brew.store.js');
const Editor = require('../../editor/editor.jsx'); const Actions = require('homebrewery/brew.actions.js');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
const Utils = require('homebrewery/utils.js');
const KEY = 'homebrewery-new'; const KEY = 'homebrewery-new';
const NewPage = React.createClass({ const NewPage = React.createClass({
getInitialState: function() {
return {
metadata : {
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
},
text: '',
isSaving : false,
errors : []
};
},
componentDidMount: function() { componentDidMount: function() {
const storage = localStorage.getItem(KEY); try{
if(storage){ const storedBrew = JSON.parse(localStorage.getItem(KEY));
this.setState({ if(storedBrew && storedBrew.text) Actions.setBrew(storedBrew);
text : storage }catch(e){}
}) Store.updateEmitter.on('change', this.saveToLocal);
}
document.addEventListener('keydown', this.handleControlKeys); document.addEventListener('keydown', this.handleControlKeys);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
Store.updateEmitter.removeListener('change', this.saveToLocal);
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
handleControlKeys : function(e){ saveToLocal : function(){
if(!(e.ctrlKey || e.metaKey)) return; localStorage.setItem(KEY, JSON.stringify(Store.getBrew()));
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.save();
if(e.keyCode == P_KEY) this.print();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
handleSplitMove : function(){
this.refs.editor.update();
},
handleMetadataChange : function(metadata){
this.setState({
metadata : _.merge({}, this.state.metadata, metadata)
});
},
handleTextChange : function(text){
this.setState({
text : text,
errors : Markdown.validate(text)
});
localStorage.setItem(KEY, text);
},
save : function(){
this.setState({
isSaving : true
});
request.post('/api')
.send(_.merge({}, this.state.metadata, {
text : this.state.text
}))
.end((err, res)=>{
if(err){
this.setState({
isSaving : false
});
return;
}
window.onbeforeunload = function(){};
const brew = res.body;
localStorage.removeItem(KEY);
window.location = '/edit/' + brew.editId;
})
},
renderSaveButton : function(){
if(this.state.isSaving){
return <Nav.item icon='fa-spinner fa-spin' className='saveButton'>
save...
</Nav.item>
}else{
return <Nav.item icon='fa-save' className='saveButton' onClick={this.save}>
save
</Nav.item>
}
},
print : function(){
localStorage.setItem('print', this.state.text);
window.open('/print?dialog=true&local=print','_blank');
},
renderLocalPrintButton : function(){
return <Nav.item color='purple' icon='fa-file-pdf-o' onClick={this.print}>
get PDF
</Nav.item>
},
renderNavbar : function(){
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.metadata.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
{this.renderLocalPrintButton()}
<IssueNavItem />
<AccountNavItem />
</Nav.section>
</Navbar>
}, },
handleControlKeys : Utils.controlKeys({
s : Actions.saveNew,
p : Actions.localPrint
}),
render : function(){ render : function(){
return <div className='newPage page'> return <div className='newPage page'>
{this.renderNavbar()} <Navbar>
<Nav.section>
<Items.BrewTitle />
</Nav.section>
<Nav.section>
<Items.StaticSave />
<Nav.item color='purple' icon='fa-file-pdf-o' onClick={Actions.localPrint}>
get PDF
</Nav.item>
<Items.Issue />
<Items.Account />
</Nav.section>
</Navbar>
<div className='content'> <div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'> <BrewInterface />
<Editor
ref='editor'
value={this.state.text}
onChange={this.handleTextChange}
metadata={this.state.metadata}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.text} errors={this.state.errors} />
</SplitPane>
</div> </div>
</div> </div>
} }

View File

@@ -1,10 +1,4 @@
.newPage{ .newPage{
.saveButton{
background-color: @orange;
&:hover{
background-color: @green;
}
}
} }

View File

@@ -1,7 +1,7 @@
const React = require('react'); const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('homebrewery/markdown.js');
const PrintPage = React.createClass({ const PrintPage = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
@@ -12,21 +12,17 @@ const PrintPage = React.createClass({
} }
}; };
}, },
getInitialState: function() { getInitialState: function() {
return { return {
brewText: this.props.brew.text brewText: this.props.brew.text
}; };
}, },
componentDidMount: function() { componentDidMount: function() {
if(this.props.query.local){ if(this.props.query.local){
this.setState({ brewText : localStorage.getItem(this.props.query.local)}); this.setState({ brewText : localStorage.getItem(this.props.query.local)});
} }
if(this.props.query.dialog) window.print(); if(this.props.query.dialog) window.print();
}, },
renderPages : function(){ renderPages : function(){
return _.map(this.state.brewText.split('\\page'), (page, index) => { return _.map(this.state.brewText.split('\\page'), (page, index) => {
return <div return <div

View File

@@ -9,9 +9,13 @@ const ReportIssue = require('../../navbar/issue.navitem.jsx');
//const RecentlyViewed = require('../../navbar/recent.navitem.jsx').viewed; //const RecentlyViewed = require('../../navbar/recent.navitem.jsx').viewed;
const Account = require('../../navbar/account.navitem.jsx'); const Account = require('../../navbar/account.navitem.jsx');
const BrewRenderer = require('homebrewery/brewRenderer/brewRenderer.jsx');
const Utils = require('homebrewery/utils.js');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx'); const Actions = require('homebrewery/brew.actions.js');
const Store = require('homebrewery/brew.store.js');
const Headtags = require('vitreum/headtags');
const SharePage = React.createClass({ const SharePage = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
@@ -33,36 +37,48 @@ const SharePage = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
document.removeEventListener('keydown', this.handleControlKeys); document.removeEventListener('keydown', this.handleControlKeys);
}, },
handleControlKeys : function(e){ handleControlKeys : Utils.controlKeys({
if(!(e.ctrlKey || e.metaKey)) return; p : Actions.print
const P_KEY = 80; }),
if(e.keyCode == P_KEY){
window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus(); renderMetatags : function(brew){
e.stopPropagation(); let metatags = [
e.preventDefault(); <Headtags.meta key='site_name' property='og:site_name' content='Homebrewery'/>,
<Headtags.meta key='type' property='og:type' content='article' />
];
if(brew.title){
metatags.push(<Headtags.meta key='title' property='og:title' content={brew.title} />);
} }
if(brew.description){
metatags.push(<Headtags.meta key='description' name='description' content={brew.description} />);
}
if(brew.thumbnail){
metatags.push(<Headtags.meta key='image' property='og:image' content={brew.thumbnail} />);
}
return metatags;
}, },
render : function(){ render : function(){
const brew = Store.getBrew();
return <div className='sharePage page'> return <div className='sharePage page'>
{this.renderMetatags(brew)}
<Navbar> <Navbar>
<Nav.section> <Nav.section>
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item> <Nav.item className='brewTitle'>{brew.title}</Nav.item>
</Nav.section> </Nav.section>
<Nav.section> <Nav.section>
<ReportIssue /> <ReportIssue />
{/*<RecentlyViewed brew={this.props.brew} />*/} <PrintLink shareId={brew.shareId} />
<PrintLink shareId={this.props.brew.shareId} /> <Nav.item href={'/source/' + brew.shareId} color='teal' icon='fa-code'>
<Nav.item href={'/source/' + this.props.brew.shareId} color='teal' icon='fa-code'>
source source
</Nav.item> </Nav.item>
<Account /> <Account />
</Nav.section> </Nav.section>
</Navbar> </Navbar>
<div className='content'> <div className='content'>
<BrewRenderer text={this.props.brew.text} /> <BrewRenderer brewText={brew.text} />
</div> </div>
</div> </div>
} }

View File

@@ -2,7 +2,6 @@ const React = require('react');
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 BrewItem = React.createClass({ const BrewItem = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
@@ -16,24 +15,6 @@ const BrewItem = React.createClass({
}; };
}, },
deleteBrew : function(){
if(!confirm("are you sure you want to delete this brew?")) return;
if(!confirm("are you REALLY sure? You will not be able to recover it")) return;
request.get('/api/remove/' + this.props.brew.editId)
.send()
.end(function(err, res){
location.reload();
});
},
renderDeleteBrewLink: function(){
if(!this.props.brew.editId) return;
return <a onClick={this.deleteBrew}>
<i className='fa fa-trash' />
</a>
},
renderEditLink: function(){ renderEditLink: function(){
if(!this.props.brew.editId) return; if(!this.props.brew.editId) return;
@@ -66,7 +47,6 @@ const BrewItem = React.createClass({
<i className='fa fa-share-alt' /> <i className='fa fa-share-alt' />
</a> </a>
{this.renderEditLink()} {this.renderEditLink()}
{this.renderDeleteBrewLink()}
</div> </div>
</div> </div>
} }

View File

@@ -52,12 +52,13 @@ const UserPage = React.createClass({
render : function(){ render : function(){
const brews = this.getSortedBrews(); const brews = this.getSortedBrews();
console.log('user brews', brews);
return <div className='userPage page'> return <div className='userPage page'>
<Navbar> <Navbar>
<Nav.section> <Nav.section>
<RecentNavItem.both /> <RecentNavItem.both />
<Account /> <Account userPage={this.props.username} />
</Nav.section> </Nav.section>
</Navbar> </Navbar>

View File

@@ -415,7 +415,7 @@ body {
border : initial; border : initial;
border-style : solid; border-style : solid;
border-image-outset : 25px 17px; border-image-outset : 25px 17px;
border-image-repeat : stretch; border-image-repeat : round;
border-image-slice : 150 200 150 200; border-image-slice : 150 200 150 200;
border-image-source : @frameBorderImage; border-image-source : @frameBorderImage;
border-image-width : 47px; border-image-width : 47px;
@@ -423,9 +423,9 @@ body {
margin-bottom : 10px; margin-bottom : 10px;
} }
} }
//************************************ //*****************************
// * DESCRIPTIVE TEXT BOX // * CLASS TABLE
// ************************************/ // *****************************/
.phb .descriptive{ .phb .descriptive{
display : block-inline; display : block-inline;
margin-bottom : 1em; margin-bottom : 1em;
@@ -433,7 +433,7 @@ body {
font-family : ScalySans; font-family : ScalySans;
border-style : solid; border-style : solid;
border-width : 7px; border-width : 7px;
border-image : @descriptiveBoxImage 12 stretch; border-image : @descriptiveBoxImage 12 round;
border-image-outset : 4px; border-image-outset : 4px;
box-shadow : 0px 0px 6px #faf7ea; box-shadow : 0px 0px 6px #faf7ea;
p{ p{

View File

@@ -1,5 +1,10 @@
{ {
"host" : "homebrewery.local.naturalcrit.com:8000", "log_level" : "info",
"naturalcrit_url" : "local.naturalcrit.com:8010", "login_path" : "/dev/login",
"secret" : "secret" "jwt_secret" : "secretsecret",
"admin" : {
"user" : "admin",
"pass" : "password",
"key" : "adminadminadmin"
}
} }

4
config/production.json Normal file
View File

@@ -0,0 +1,4 @@
{
"login_path" : "http://naturalcrit.com/login",
"log_level" : "warn"
}

4
config/staging.json Normal file
View File

@@ -0,0 +1,4 @@
{
"login_path" : "http://staging.naturalcrit.com/login",
"log_level" : "trace"
}

View File

@@ -1,39 +1,53 @@
{ {
"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": "2.7.5", "version": "3.0.0",
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"quick": "node scripts/quick.js", "quick": "node scripts/quick.js",
"build": "node scripts/build.js", "build": "node scripts/build.js",
"phb": "node scripts/phb.js", "phb": "node scripts/phb.js",
"populate": "node scripts/populate.js",
"prod": "set NODE_ENV=production&& npm run build", "prod": "set NODE_ENV=production&& npm run build",
"postinstall": "npm run build", "postinstall": "npm run build",
"start": "node server.js" "start": "node server.js",
"test": "mocha test",
"test:dev": "nodemon -x mocha test || exit 0",
"test:markdown": "nodemon -x mocha test/markdown.test.js || exit 0"
}, },
"author": "stolksdorf", "author": "stolksdorf",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"babel-preset-env": "^1.1.8",
"basic-auth": "^1.0.3", "basic-auth": "^1.0.3",
"body-parser": "^1.14.2", "body-parser": "^1.14.2",
"classnames": "^2.2.0", "classnames": "^2.2.0",
"codemirror": "^5.22.0", "codemirror": "^5.22.0",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"egads": "^1.0.1",
"express": "^4.13.3", "express": "^4.13.3",
"jwt-simple": "^0.5.1", "jwt-simple": "^0.5.1",
"lodash": "^4.11.2", "lodash": "^4.17.3",
"loglevel": "^1.4.1",
"marked": "^0.3.5", "marked": "^0.3.5",
"moment": "^2.11.0", "moment": "^2.11.0",
"mongoose": "^4.3.3", "mongoose": "^4.3.3",
"nconf": "^0.8.4", "nconf": "^0.8.4",
"pico-flux": "^1.1.0", "pico-flux": "^2.1.2",
"pico-router": "^1.1.0", "pico-router": "^1.1.0",
"react": "^15.0.2", "react": "^15.4.1",
"react-dom": "^15.0.2", "react-dom": "^15.4.1",
"shortid": "^2.2.4", "shortid": "^2.2.4",
"striptags": "^2.1.1", "striptags": "^2.1.1",
"superagent": "^1.6.1", "superagent": "^1.6.1",
"vitreum": "^4.0.12" "vitreum": "^4.0.12"
},
"devDependencies": {
"app-module-path": "^2.1.0",
"chai": "^3.5.0",
"chai-as-promised": "^6.0.0",
"chai-subset": "^1.4.0",
"mocha": "^3.2.0",
"supertest": "^2.0.1",
"supertest-as-promised": "^4.0.2"
} }
} }

File diff suppressed because one or more lines are too long

View File

@@ -12,10 +12,8 @@ const Proj = require('./project.json');
Promise.resolve() Promise.resolve()
.then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared')) .then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared'))
.then(less('homebrew', './shared')) .then(less('homebrew', './shared'))
.then(jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared')) .then(jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared'))
.then(less('admin', './shared')) .then(less('admin', './shared'))
.then(assets(Proj.assets, ['./shared', './client'])) .then(assets(Proj.assets, ['./shared', './client']))
.then(livereload()) .then(livereload())
.then(server('./server.js', ['server'])) .then(server('./server.js', ['server']))

22
scripts/populate.js Normal file
View File

@@ -0,0 +1,22 @@
//Populates the DB with a bunch of brews for UI testing
const _ = require('lodash');
const DB = require('../server/db.js');
const BrewData = require('../server/brew.data.js');
const BrewGen = require('../test/brew.gen.js');
return Promise.resolve()
.then(DB.connect)
.then(BrewData.removeAll)
.then(() => {
console.log('Adding random brews...');
return BrewGen.populateDB(BrewGen.random(50));
})
.then(() => {
console.log('Adding specific brews...');
return BrewGen.populateDB(BrewGen.static());
})
.then(() => {
return DB.close();
})
.catch(console.error);

175
server.js
View File

@@ -1,140 +1,35 @@
const _ = require('lodash'); const config = require('nconf')
const jwt = require('jwt-simple'); .argv()
const express = require("express"); .env({ lowerCase: true })
const app = express(); .file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });
app.use(express.static(__dirname + '/build'));''
app.use(require('body-parser').json({limit: '25mb'})); const log = require('loglevel');
app.use(require('cookie-parser')()); log.setLevel(config.get('log_level'));
const config = require('nconf') //DB
.argv() require('./server/db.js').connect();
.env({ lowerCase: true })
.file('environment', { file: `config/${process.env.NODE_ENV}.json` }) //Server
.file('defaults', { file: 'config/default.json' }); const app = require('./server/app.js');
//DB /*
require('mongoose') app.use((req, res, next) => {
.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/naturalcrit') log.debug('---------------------------');
.connection.on('error', () => { log.debug(req.method, req.path);
console.log('Error : Could not connect to a Mongo Database.');
console.log(' If you are running locally, make sure mongodb.exe is running.'); if (req.params) {
}); log.debug('req params', req.params);
}
if (req.query) {
//Account MIddleware log.debug('req query', req.query);
app.use((req, res, next) => { }
if(req.cookies && req.cookies.nc_session){
try{ next();
req.account = jwt.decode(req.cookies.nc_session, config.get('secret')); });
}catch(e){} */
}
return next(); const PORT = process.env.PORT || 8000;
}); const httpServer = app.listen(PORT, () => {
log.info(`server on port:${PORT}`);
});
app.use(require('./server/homebrew.api.js'));
app.use(require('./server/admin.api.js'));
const HomebrewModel = require('./server/homebrew.model.js').model;
const welcomeText = require('fs').readFileSync('./client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
const changelogText = require('fs').readFileSync('./changelog.md', 'utf8');
//Source page
String.prototype.replaceAll = function(s,r){return this.split(s).join(r)}
app.get('/source/:id', (req, res)=>{
HomebrewModel.get({shareId : req.params.id})
.then((brew)=>{
const text = brew.text.replaceAll('<', '&lt;').replaceAll('>', '&gt;');
return res.send(`<code><pre>${text}</pre></code>`);
})
.catch((err)=>{
console.log(err);
return res.status(404).send('Could not find Homebrew with that id');
})
});
app.get('/user/:username', (req, res, next) => {
const fullAccess = req.account && (req.account.username == req.params.username);
HomebrewModel.getByUser(req.params.username, fullAccess)
.then((brews) => {
req.brews = brews;
return next();
})
.catch((err) => {
console.log(err);
})
})
app.get('/edit/:id', (req, res, next)=>{
HomebrewModel.get({editId : req.params.id})
.then((brew)=>{
req.brew = brew.sanatize();
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
});
//Share Page
app.get('/share/:id', (req, res, next)=>{
HomebrewModel.get({shareId : req.params.id})
.then((brew)=>{
return brew.increaseView();
})
.then((brew)=>{
req.brew = brew.sanatize(true);
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
});
//Print Page
app.get('/print/:id', (req, res, next)=>{
HomebrewModel.get({shareId : req.params.id})
.then((brew)=>{
req.brew = brew.sanatize(true);
return next();
})
.catch((err)=>{
console.log(err);
return res.status(400).send(`Can't get that`);
});
});
//Render Page
const render = require('vitreum/steps/render');
const templateFn = require('./client/template.js');
app.use((req, res) => {
render('homebrew', templateFn, {
version : require('./package.json').version,
url: req.originalUrl,
welcomeText : welcomeText,
changelog : changelogText,
brew : req.brew,
brews : req.brews,
account : req.account
})
.then((page) => {
return res.send(page)
})
.catch((err) => {
console.log(err);
return res.sendStatus(500);
});
});
const PORT = process.env.PORT || 8000;
app.listen(PORT);
console.log(`server on port:${PORT}`);

View File

@@ -1,85 +0,0 @@
const _ = require('lodash');
const auth = require('basic-auth');
const HomebrewModel = require('./homebrew.model.js').model;
const router = require('express').Router();
const mw = {
adminOnly : (req, res, next)=>{
if(req.query && req.query.admin_key == process.env.ADMIN_KEY) return next();
return res.status(401).send('Access denied');
}
};
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password';
process.env.ADMIN_KEY = process.env.ADMIN_KEY || 'admin_key';
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
router.get('/api/invalid', mw.adminOnly, (req, res)=>{
const invalidBrewQuery = HomebrewModel.find({
'$where' : "this.text.length < 140",
createdAt: {
$lt: Moment().subtract(3, 'days').toDate()
}
});
if(req.query.do_it){
invalidBrewQuery.remove().exec((err, objs)=>{
refreshCount();
return res.send(200);
})
}else{
invalidBrewQuery.exec((err, objs)=>{
if(err) console.log(err);
return res.json({
count : objs.length
})
})
}
});
router.get('/admin/lookup/:id', mw.adminOnly, (req, res, next) => {
//search for mathcing edit id
//search for matching share id
// search for partial match
HomebrewModel.findOne({ $or:[
{editId : { "$regex": req.params.id, "$options": "i" }},
{shareId : { "$regex": req.params.id, "$options": "i" }},
]}).exec((err, brew) => {
return res.json(brew);
});
});
//Admin route
const render = require('vitreum/steps/render');
const templateFn = require('../client/template.js');
router.get('/admin', function(req, res){
const credentials = auth(req)
if (!credentials || credentials.name !== process.env.ADMIN_USER || credentials.pass !== process.env.ADMIN_PASS) {
res.setHeader('WWW-Authenticate', 'Basic realm="example"')
return res.status(401).send('Access denied')
}
render('admin', templateFn, {
url: req.originalUrl,
admin_key : process.env.ADMIN_KEY,
})
.then((page) => {
return res.send(page)
})
.catch((err) => {
console.log(err);
return res.sendStatus(500);
});
});
module.exports = router;

60
server/admin.routes.js Normal file
View File

@@ -0,0 +1,60 @@
const _ = require('lodash');
const router = require('express').Router();
const vitreumRender = require('vitreum/steps/render');
const templateFn = require('../client/template.js');
const config = require('nconf');
const Moment = require('moment');
const mw = require('./middleware.js');
const BrewData = require('./brew.data.js');
const getInvalidBrewQuery = ()=>{
return BrewData.model.find({
'$where' : "this.text.length < 140",
createdAt: {
$lt: Moment().subtract(3, 'days').toDate()
}
}).select({ text : false });
}
router.get('/admin', mw.adminLogin, (req, res, next) => {
return vitreumRender('admin', templateFn, {
url : req.originalUrl,
admin_key : config.get('admin:key')
})
.then((page) => {
return res.send(page)
})
.catch(next);
});
//Removes all empty brews that are older than 3 days and that are shorter than a tweet
router.get('/admin/invalid', mw.adminOnly, (req, res, next)=>{
getInvalidBrewQuery().exec()
.then((brews) => {
return res.json(brews);
})
.catch(next);
});
router.delete('/admin/invalid', mw.adminOnly, (req, res, next)=>{
getInvalidBrewQuery().remove()
.then(()=>{
return res.status(200).send();
})
.catch(next);
});
router.get('/admin/lookup/:search', mw.adminOnly, (req, res, next) => {
BrewData.get({ $or:[
//Searches for partial matches on either edit or share
{editId : { "$regex": req.params.search, "$options": "i" }},
{shareId : { "$regex": req.params.search, "$options": "i" }},
]})
.then((brews) => {
return res.json(brews);
})
.catch(next);
});
module.exports = router;

28
server/app.js Normal file
View File

@@ -0,0 +1,28 @@
const config = require('nconf');
const express = require("express");
const app = express();
app.use(express.static(__dirname + '/../build'));
app.use(require('body-parser').json({limit: '25mb'}));
app.use(require('cookie-parser')());
//Middleware
const mw = require('./middleware.js');
app.use(mw.account);
app.use(mw.admin);
//Routes
app.use(require('./brew.api.js'));
app.use(require('./interface.routes.js'));
app.use(require('./admin.routes.js'));
if(config.get('NODE_ENV') !== 'staging' && config.get('NODE_ENV') !== 'production'){
app.use(require('./dev.routes.js'));
}
//Error Handler
app.use(require('./error.js').expressHandler);
module.exports = app;

68
server/brew.api.js Normal file
View File

@@ -0,0 +1,68 @@
const _ = require('lodash');
const router = require('express').Router();
const BrewData = require('./brew.data.js');
const mw = require('./middleware.js');
//Search
router.get('/api/brew', (req, res, next) => {
const opts = _.pick(req.query, ['limit', 'sort', 'page']);
BrewData.termSearch(req.query.terms, opts, req.admin)
.then((result) => {
return res.status(200).json(result);
})
.catch(next);
});
//User
router.get('/api/user/:username', (req, res, next) => {
const fullAccess = req.admin ||
!!(req.account && req.params.username == req.account.username);
BrewData.userSearch(req.params.username, fullAccess)
.then((result) => {
return res.status(200).json(result);
})
.catch(next);
});
//Get
router.get('/api/brew/:shareId', mw.viewBrew, (req, res, next) => {
return res.json(req.brew.toJSON());
});
//Create
router.post('/api/brew', (req, res, next)=>{
const brew = req.body;
if(req.account) brew.authors = [req.account.username];
BrewData.create(brew)
.then((brew) => {
return res.json(brew.toJSON());
})
.catch(next)
});
//Update
router.put('/api/brew/:editId', mw.loadBrew, (req, res, next)=>{
const brew = req.body || {};
if(req.account){
brew.authors = _.uniq(_.concat(brew.authors, req.account.username));
}
BrewData.update(req.params.editId, brew)
.then((brew) => {
return res.json(brew.toJSON());
})
.catch(next);
});
//Delete
router.delete('/api/brew/:editId', mw.loadBrew, (req, res, next) => {
BrewData.remove(req.params.editId)
.then(()=>{
return res.sendStatus(200);
})
.catch(next);
});
module.exports = router;

101
server/brew.data.js Normal file
View File

@@ -0,0 +1,101 @@
const _ = require('lodash');
const shortid = require('shortid');
const mongoose = require('./db.js').instance;
const Error = require('./error.js');
const utils = require('./utils.js');
const BrewSchema = mongoose.Schema({
shareId : {type : String, default: shortid.generate, index: { unique: true }},
editId : {type : String, default: shortid.generate, index: { unique: true }},
text : {type : String, default : ""},
title : {type : String, default : ""},
description : {type : String, default : ""},
tags : {type : String, default : ""},
thumbnail : {type : String, default : ""},
systems : [String],
authors : [String],
published : {type : Boolean, default : false},
createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now},
lastViewed : { type: Date, default: Date.now},
views : {type:Number, default:0},
version : {type: Number, default:1}
}, {
versionKey: false,
toJSON : {
transform: (doc, ret, options) => {
delete ret._id;
return ret;
}
}
});
//Index these fields for fast text searching
BrewSchema.index({ title: "text", description: "text" });
BrewSchema.methods.increaseView = function(){
this.views = this.views + 1;
return this.save();
};
const Brew = mongoose.model('Brew', BrewSchema);
const BrewData = {
schema : BrewSchema,
model : Brew,
get : (query) => {
return Brew.findOne(query).exec()
.then((brew) => {
if(!brew) throw Error.noBrew();
return brew;
});
},
create : (brew) => {
delete brew.shareId;
delete brew.editId;
if(!brew.title) brew.title = utils.getGoodBrewTitle(brew.text);
return (new Brew(brew)).save();
},
update : (editId, newBrew) => {
return BrewData.get({ editId })
.then((brew) => {
delete newBrew.shareId;
delete newBrew.editId;
brew = _.merge(brew, newBrew, { updatedAt : Date.now() });
brew.markModified('authors');
brew.markModified('systems');
return brew.save();
});
},
remove : (editId) => {
return Brew.find({ editId }).remove().exec();
},
removeAll : ()=>{
return Brew.find({}).remove().exec();
},
//////// Special
getByShare : (shareId) => {
return BrewData.get({ shareId : shareId})
.then((brew) => {
brew.increaseView();
const brewJSON = brew.toJSON();
delete brewJSON.editId;
return brewJSON;
});
},
getByEdit : (editId) => {
return BrewData.get({ editId });
},
};
const BrewSearch = require('./brew.search.js')(Brew);
module.exports = _.merge(BrewData, BrewSearch);

66
server/brew.search.js Normal file
View File

@@ -0,0 +1,66 @@
const _ = require('lodash');
module.exports = (Brew) => {
const cmds = {
termSearch : (terms='', opts, fullAccess) => {
let query = {};
if(terms){
query = {$text: {
//Wrap terms in quotes to perform an AND operation
$search: _.map(terms.split(' '), (term)=>{
return `\"${term}\"`;
}).join(' '),
$caseSensitive : false
}};
}
return cmds.search(query, opts, fullAccess);
},
userSearch : (username, fullAccess) => {
const query = {
authors : username
};
return cmds.search(query, {}, fullAccess);
},
search : (queryObj={}, options={}, fullAccess = true) => {
const opts = _.merge({
limit : 25,
page : 0,
sort : {}
}, options);
opts.limit = _.toNumber(opts.limit);
opts.page = _.toNumber(opts.page);
let filter = {
text : 0
};
if(!fullAccess){
filter.editId = 0;
queryObj.published = true;
}
const searchQuery = Brew
.find(queryObj)
.sort(opts.sort)
.select(filter)
.limit(opts.limit)
.skip(opts.page * opts.limit)
.lean()
.exec();
const countQuery = Brew.count(queryObj).exec();
return Promise.all([searchQuery, countQuery])
.then((result) => {
return {
brews : result[0],
total : result[1]
}
});
}
};
return cmds;
};

36
server/db.js Normal file
View File

@@ -0,0 +1,36 @@
const log = require('loglevel');
const mongoose = require('mongoose');
mongoose.Promise = Promise;
const dbPath = process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/homebrewery';
module.exports = {
connect : ()=>{
return new Promise((resolve, reject)=>{
if(mongoose.connection.readyState == 1){
log.warn('DB already connected');
return resolve();
}
mongoose.connect(dbPath,
(err) => {
if(err){
log.error('Error : Could not connect to a Mongo Database.');
log.error(' If you are running locally, make sure mongodb.exe is running.');
return reject(err);
}
log.info('DB connected.');
return resolve();
}
);
});
},
close : ()=>{
return new Promise((resolve, reject) => {
mongoose.connection.close(()=>{
log.info('DB connection closed.');
return resolve();
});
});
},
instance : mongoose
}

29
server/dev.routes.js Normal file
View File

@@ -0,0 +1,29 @@
const router = require('express').Router();
const jwt = require('jwt-simple');
const auth = require('basic-auth');
const config = require('nconf');
if(process.env.NODE_ENV == 'production') throw 'Loading dev routes in prod. ABORT ABORT';
router.get('/dev/login', (req, res, next) => {
const user = req.query.user;
if(!user){
return res.send(`
<html>
<body>dev login</body>
<script>
var user = prompt('enter username');
if(user) window.location = '/dev/login?user=' + encodeURIComponent(user);
</script></html>
`);
}
res.cookie('nc_session', jwt.encode({username : req.query.user}, config.get('jwt_secret')));
return res.redirect('/');
});
module.exports = router;

25
server/error.js Normal file
View File

@@ -0,0 +1,25 @@
const Error = require('egads').extend('Server Error', 500, 'Generic Server Error');
Error.noBrew = Error.extend('Can not find a brew with that id', 404, 'No Brew Found');
Error.noAuth = Error.extend('You can not access this route', 401, 'Unauthorized');
Error.noAdmin = Error.extend('You need admin credentials to access this route', 401, 'Unauthorized');
Error.expressHandler = (err, req, res, next) => {
if(err instanceof Error){
return res.status(err.status).send({
type : err.name,
message : err.message
});
}
//If server error, print the whole stack for debugging
return res.status(500).send({
message : err.message,
stack : err.stack
});
};
module.exports = Error;

View File

@@ -36,6 +36,8 @@ const getGoodBrewTitle = (text) => {
router.post('/api', (req, res)=>{ router.post('/api', (req, res)=>{
let authors = []; let authors = [];

View File

@@ -0,0 +1,88 @@
const _ = require('lodash');
const config = require('nconf');
const utils = require('./utils.js');
const BrewData = require('./brew.data.js');
const router = require('express').Router();
const mw = require('./middleware.js');
const docs = {
welcomeBrew : require('fs').readFileSync('./welcome.brew.md', 'utf8'),
changelog : require('fs').readFileSync('./changelog.md', 'utf8'),
};
const vitreumRender = require('vitreum/steps/render');
const templateFn = require('../client/template.js');
//TODO: Catch errors here?
const renderPage = (req, res, next) => {
return vitreumRender('homebrew', templateFn, {
url : req.originalUrl,
version : require('../package.json').version,
loginPath : config.get('login_path'),
user : req.account && req.account.username,
brews : req.brews,
brew : req.brew
})
.then((page) => {
return res.send(page)
})
.catch(next);
};
//Share Page
router.get('/share/:shareId', mw.viewBrew, renderPage);
//Edit Page
router.get('/edit/:editId', mw.loadBrew, renderPage);
//Print Page
router.get('/print/:shareId', mw.viewBrew, renderPage);
//Source page
router.get('/source/:sharedId', mw.viewBrew, (req, res, next)=>{
const text = utils.replaceByMap(req.brew.text, { '<' : '&lt;', '>' : '&gt;' });
return res.send(`<code><pre>${text}</pre></code>`);
});
//User Page
router.get('/user/:username', (req, res, next) => {
BrewData.search({ user : req.params.username })
.then((brews) => {
req.brews = brews;
return next();
})
.catch(next);
}, renderPage);
//Search Page
router.get('/search', (req, res, next) => {
BrewData.search()
.then((brews) => {
req.brews = brews;
return next();
})
.catch(next);
}, renderPage);
//Changelog Page
router.get('/changelog', (req, res, next) => {
req.brew = {
text : docs.changelog,
title : 'Changelog'
};
return next();
}, renderPage);
//New Page
router.get('/new', renderPage);
//Home Page
router.get('/', (req, res, next) => {
req.brew = { text : docs.welcomeBrew };
return next();
}, renderPage);
module.exports = router;

69
server/middleware.js Normal file
View File

@@ -0,0 +1,69 @@
const _ = require('lodash');
const jwt = require('jwt-simple');
const auth = require('basic-auth');
const config = require('nconf');
const Error = require('./error.js');
const BrewData = require('./brew.data.js');
const Middleware = {
account : (req, res, next) => {
if(req.cookies && req.cookies.nc_session){
try{
req.account = jwt.decode(req.cookies.nc_session, config.get('jwt_secret'));
}catch(e){}
}
return next();
},
admin : (req, res, next) => {
req.admin = false;
if(req.headers['x-homebrew-admin'] === config.get('admin:key')){
req.admin = true;
}
return next();
},
//Filters
devOnly : (req, res, next) => {
const env = process.env.NODE_ENV;
if(env !== 'staging' && env !== 'production') return next();
return res.sendStatus(404);
},
adminOnly : (req, res, next) => {
if(req.admin) return next();
return next(Error.noAuth());
},
adminLogin : (req, res, next) => {
const creds = auth(req);
if(!creds
|| creds.name !== config.get('admin:user')
|| creds.pass !== config.get('admin:pass')){
res.setHeader('WWW-Authenticate', 'Basic realm="example"');
return next(Error.noAdmin());
}
return next();
},
//TODO: REMOVE
//Loaders
loadBrew : (req, res, next) => {
BrewData.getByEdit(req.params.editId)
.then((brew) => {
req.brew = brew;
return next()
})
.catch(next);
},
viewBrew : (req, res, next) => {
BrewData.getByShare(req.params.shareId)
.then((brew) => {
req.brew = brew;
return next()
})
.catch(next);
},
};
module.exports = Middleware;

23
server/utils.js Normal file
View File

@@ -0,0 +1,23 @@
const _ = require('lodash');
module.exports = {
getGoodBrewTitle : (text = '') => {
const titlePos = text.indexOf('# ');
if(titlePos !== -1){
let ending = text.indexOf('\n', titlePos);
ending = (ending == -1 ? undefined : ending);
return text.substring(titlePos + 2, ending).trim();
}else{
return (_.find(text.split('\n'), (line)=>{
return line;
}) || '').trim();
}
},
replaceByMap : (text, mapping) => {
return _.reduce(mapping, (r, search, replace) => {
return r.split(search).join(replace)
}, text)
}
}

View File

@@ -0,0 +1,18 @@
const Store = require('./account.store.js');
const Actions = {
init : (initState) => {
Store.init(initState);
},
login : ()=>{
window.location = Store.getLoginPath();
},
logout : ()=>{
document.cookie = 'nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;domain=.naturalcrit.com';
//Remove local dev cookies too
document.cookie = 'nc_session=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/;';
window.location ='/';
}
};
module.exports = Actions;

View File

@@ -0,0 +1,27 @@
const _ = require('lodash');
const flux = require('pico-flux');
let State = {
loginPath : '',
user : undefined,
};
const Store = {}; //Maybe Flux it later?
Store.init = (state)=>{
State = _.merge({}, State, state);
};
Store.getLoginPath = ()=>{
let path = State.loginPath;
if(typeof window !== 'undefined'){
path = `${path}?redirect=${encodeURIComponent(window.location.href)}`;
}
return path;
};
Store.getUser = ()=>{
return State.user;
};
module.exports = Store;

View File

@@ -0,0 +1,78 @@
const _ = require('lodash');
const dispatch = require('pico-flux').dispatch;
const request = require('superagent');
const Store = require('./brew.store.js');
let pendingTimer;
const PENDING_TIMEOUT = 3000;
const APIActions = {
save : () => {
clearTimeout(pendingTimer);
const brew = Store.getBrew();
dispatch('SET_STATUS', 'saving');
request
.put('/api/brew/' + brew.editId)
.send(brew)
.end((err, res) => {
if(err) return dispatch('SET_STATUS', 'error', err);
dispatch('SET_BREW', res.body);
dispatch('SET_STATUS', 'ready');
});
},
create : () => {
dispatch('SET_STATUS', 'saving');
request.post('/api/brew')
.send(Store.getBrew())
.end((err, res)=>{
if(err) return dispatch('SET_STATUS', 'error', err);
localStorage.setItem('homebrewery-new', null);
const brew = res.body;
window.location = '/edit/' + brew.editId;
});
},
delete : (editId) => {
dispatch('SET_STATUS', 'deleting');
request.delete('/api/brew/' + editId)
.send()
.end((err, res)=>{
if(err) return dispatch('SET_STATUS', 'error', err);
window.location = '/';
});
}
}
const Actions = {
init : (initState) => {
const filteredState = _.reduce(initState, (r, val, key) => {
if(typeof val !== 'undefined') r[key] = val;
return r;
}, {});
Store.init(filteredState);
},
setBrew : (brew) => {
dispatch('SET_BREW', brew);
},
updateBrewText : (brewText) => {
dispatch('UPDATE_BREW_TEXT', brewText)
},
updateMetaData : (meta) => {
dispatch('UPDATE_META', meta);
},
pendingSave : () => {
clearTimeout(pendingTimer);
pendingTimer = setTimeout(APIActions.save, PENDING_TIMEOUT);
dispatch('SET_STATUS', 'pending');
},
localPrint : ()=>{
localStorage.setItem('print', Store.getBrewText());
window.open('/print?dialog=true&local=print','_blank');
},
print : ()=>{
window.open(`/print/${Store.getBrew().shareId}?dialog=true`, '_blank').focus();
}
};
module.exports = _.merge(Actions, APIActions);

View File

@@ -0,0 +1,69 @@
const _ = require('lodash');
const flux = require('pico-flux');
const Markdown = require('homebrewery/markdown.new.js');
let State = {
version : '0.0.0',
brew : {
text : '',
shareId : undefined,
editId : undefined,
createdAt : undefined,
updatedAt : undefined,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
},
errors : [],
status : 'ready', //ready, pending, saving, error
};
const Store = flux.createStore({
SET_BREW : (brew) => {
State.brew = brew;
},
UPDATE_BREW_TEXT : (brewText) => {
State.brew.text = brewText;
State.errors = Markdown.validate(brewText);
},
UPDATE_META : (meta) => {
State.brew = _.merge({}, State.brew, meta);
},
SET_STATUS : (status, error) => {
if(status == State.status) return false;
if(error) State.errors = error;
State.status = status;
}
});
Store.init = (state)=>{
State = _.merge({}, State, state);
};
Store.getBrew = ()=>{
return State.brew;
};
Store.getBrewText = ()=>{
return State.brew.text;
};
Store.getMetaData = ()=>{
return _.omit(State.brew, ['text']);
};
Store.getErrors = ()=>{
return State.errors;
};
Store.getVersion = ()=>{
return State.version;
};
Store.getStatus = ()=>{
return State.status;
};
module.exports = Store;

View File

@@ -6,14 +6,13 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx'); const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx'); const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const splice = function(str, index, inject){ const splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index); return str.slice(0, index) + inject + str.slice(index);
}; };
const SNIPPETBAR_HEIGHT = 25; const SNIPPETBAR_HEIGHT = 25;
const Editor = React.createClass({ const BrewEditor = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
value : '', value : '',
@@ -67,14 +66,17 @@ const Editor = React.createClass({
}) })
}, },
getCurrentPage : function(){ brewJump : function(){
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1); const currentPage = this.getCurrentPage();
return _.reduce(lines, (r, line) => { window.location.hash = 'p' + currentPage;
if(line.indexOf('\\page') !== -1) r++;
return r;
}, 1);
}, },
//Called when there are changes to the editor's dimensions
update : function(){
this.refs.codeEditor.updateSize();
},
//TODO: convert this into a generic function for columns and blocks
highlightPageLines : function(){ highlightPageLines : function(){
if(!this.refs.codeEditor) return; if(!this.refs.codeEditor) return;
const codeMirror = this.refs.codeEditor.codeMirror; const codeMirror = this.refs.codeEditor.codeMirror;
@@ -89,17 +91,6 @@ const Editor = React.createClass({
return lineNumbers return lineNumbers
}, },
brewJump : function(){
const currentPage = this.getCurrentPage();
window.location.hash = 'p' + currentPage;
},
//Called when there are changes to the editor's dimensions
update : function(){
this.refs.codeEditor.updateSize();
},
renderMetadataEditor : function(){ renderMetadataEditor : function(){
if(!this.state.showMetadataEditor) return; if(!this.state.showMetadataEditor) return;
return <MetadataEditor return <MetadataEditor
@@ -109,37 +100,32 @@ const Editor = React.createClass({
}, },
render : function(){ render : function(){
this.highlightPageLines();
return(
<div className='editor' ref='main'>
<SnippetBar
brew={this.props.value}
onInject={this.handleInject}
onToggle={this.handgleToggle}
showmeta={this.state.showMetadataEditor} />
{this.renderMetadataEditor()}
<CodeEditor
ref='codeEditor'
wrap={true}
language='gfm'
value={this.props.value}
onChange={this.handleTextChange}
onCursorActivity={this.handleCursorActivty} />
{/* this.highlightPageLines();
<div className='brewJump' onClick={this.brewJump}>
<i className='fa fa-arrow-right' /> return<div className='brewEditor' ref='main'>
</div> <SnippetBar
*/} brew={this.props.value}
onInject={this.handleInject}
onToggle={this.handgleToggle}
showmeta={this.state.showMetadataEditor} />
{this.renderMetadataEditor()}
<CodeEditor
ref='codeEditor'
wrap={true}
language='gfm'
value={this.props.value}
onChange={this.handleTextChange}
onCursorActivity={this.handleCursorActivty} />
</div>
/*
<div className='brewJump' onClick={this.brewJump}>
<i className='fa fa-arrow-right' />
</div> </div>
); */
} }
}); });
module.exports = Editor; module.exports = BrewEditor;

View File

@@ -1,8 +1,7 @@
.editor{ .brewEditor{
position : relative; position : relative;
width : 100%; width : 100%;
.codeEditor{ .codeEditor{
height : 100%; height : 100%;
.pageLine{ .pageLine{
@@ -25,5 +24,4 @@
justify-content:center; justify-content:center;
.tooltipLeft("Jump to brew page"); .tooltipLeft("Jump to brew page");
} }
} }

View File

@@ -0,0 +1,13 @@
const Actions = require('homebrewery/brew.actions.js');
const Store = require('homebrewery/brew.store.js');
const BrewEditor = require('./brewEditor.jsx')
module.exports = Store.createSmartComponent(BrewEditor, ()=>{
return {
value : Store.getBrewText(),
onChange : Actions.updateBrewText,
metadata : Store.getMetaData(),
onMetadataChange : Actions.updateMetaData,
};
});

View File

@@ -74,6 +74,7 @@ const MetadataEditor = React.createClass({
}, },
renderPublish : function(){ renderPublish : function(){
//TODO: Move the publish element into here
if(this.props.metadata.published){ if(this.props.metadata.published){
return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}> return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}>
<i className='fa fa-ban' /> unpublish <i className='fa fa-ban' /> unpublish
@@ -139,13 +140,12 @@ const MetadataEditor = React.createClass({
<textarea value={this.props.metadata.description} className='value' <textarea value={this.props.metadata.description} className='value'
onChange={this.handleFieldChange.bind(null, 'description')} /> onChange={this.handleFieldChange.bind(null, 'description')} />
</div> </div>
{/*} <div className='field thumbnail'>
<div className='field tags'> <label>thumbnail</label>
<label>tags</label> <input type='text' className='value'
<textarea value={this.props.metadata.tags} value={this.props.metadata.thumbnail}
onChange={this.handleFieldChange.bind(null, 'tags')} /> onChange={this.handleFieldChange.bind(null, 'thumbnail')} />
</div> </div>
*/}
<div className='field systems'> <div className='field systems'>
<label>systems</label> <label>systems</label>

View File

@@ -76,4 +76,7 @@
font-size: 0.8em; font-size: 0.8em;
line-height : 1.5em; line-height : 1.5em;
} }
.thumbnail.field{
}
} }

View File

@@ -13,7 +13,6 @@
cursor : pointer; cursor : pointer;
line-height : @height; line-height : @height;
text-align : center; text-align : center;
.tooltipLeft("Edit Brew Metadata");
&:hover, &.selected{ &:hover, &.selected{
background-color : #999; background-color : #999;
} }

View File

@@ -2,7 +2,7 @@ var _ = require('lodash');
module.exports = function(classname){ module.exports = function(classname){
classname = _.sample(['archivist', 'fancyman', 'linguist', 'fletcher', classname = classname || _.sample(['archivist', 'fancyman', 'linguist', 'fletcher',
'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge']) 'notary', 'berserker-typist', 'fishmongerer', 'manicurist', 'haberdasher', 'concierge'])
classname = classname.toLowerCase(); classname = classname.toLowerCase();

View File

@@ -0,0 +1,22 @@
const React = require('react');
const _ = require('lodash');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../brewEditor/brewEditor.smart.jsx');
const BrewRenderer = require('../brewRenderer/brewRenderer.smart.jsx');
const BrewInterface = React.createClass({
handleSplitMove : function(){
console.log('split move!');
},
render: function(){
return <SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor ref='editor'/>
<BrewRenderer />
</SplitPane>
}
});
module.exports = BrewInterface;

View File

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

View File

@@ -2,11 +2,12 @@ const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const Markdown = require('naturalcrit/markdown.js'); const Markdown = require('homebrewery/markdown.new.js');
const ErrorBar = require('./errorBar/errorBar.jsx'); const ErrorBar = require('./errorBar/errorBar.jsx');
//TODO: move to the brew renderer
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx') const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx')
const Store = require('homebrewery/brew.store.js');
const PAGE_HEIGHT = 1056; const PAGE_HEIGHT = 1056;
const PPR_THRESHOLD = 50; const PPR_THRESHOLD = 50;
@@ -14,24 +15,19 @@ const PPR_THRESHOLD = 50;
const BrewRenderer = React.createClass({ const BrewRenderer = React.createClass({
getDefaultProps: function() { getDefaultProps: function() {
return { return {
text : '', brewText : '',
errors : [] errors : []
}; };
}, },
getInitialState: function() { getInitialState: function() {
const pages = this.props.text.split('\\page'); const pages = this.props.brewText.split('\\page');
return { return {
viewablePageNumber: 0, viewablePageNumber: 0,
height : 0, height : 0,
isMounted : false, isMounted : false,
usePPR : true,
pages : pages, pages : pages,
usePPR : pages.length >= PPR_THRESHOLD, usePPR : pages.length >= PPR_THRESHOLD
errors : []
}; };
}, },
height : 0, height : 0,
@@ -49,7 +45,7 @@ const BrewRenderer = React.createClass({
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight; if(this.refs.pages && this.refs.pages.firstChild) this.pageHeight = this.refs.pages.firstChild.clientHeight;
const pages = nextProps.text.split('\\page'); const pages = nextProps.brewText.split('\\page');
this.setState({ this.setState({
pages : pages, pages : pages,
usePPR : pages.length >= PPR_THRESHOLD usePPR : pages.length >= PPR_THRESHOLD
@@ -77,13 +73,9 @@ const BrewRenderer = React.createClass({
if(!this.state.isMounted) return false; if(!this.state.isMounted) return false;
var viewIndex = this.state.viewablePageNumber; var viewIndex = this.state.viewablePageNumber;
if(index == viewIndex - 3) return true;
if(index == viewIndex - 2) return true;
if(index == viewIndex - 1) return true; if(index == viewIndex - 1) return true;
if(index == viewIndex) return true; if(index == viewIndex) return true;
if(index == viewIndex + 1) return true; if(index == viewIndex + 1) return true;
if(index == viewIndex + 2) return true;
if(index == viewIndex + 3) return true;
//Check for style tages //Check for style tages
if(pageText.indexOf('<style>') !== -1) return true; if(pageText.indexOf('<style>') !== -1) return true;

View File

@@ -0,0 +1,10 @@
const Store = require('homebrewery/brew.store.js');
const BrewRenderer = require('./brewRenderer.jsx');
module.exports = Store.createSmartComponent(BrewRenderer, () => {
return {
brewText : Store.getBrewText(),
errors : Store.getErrors()
}
});

View File

@@ -0,0 +1,123 @@
const _ = require('lodash');
const Markdown = require('marked');
/*
//Processes the markdown within an HTML block if it's just a class-wrapper
renderer.html = function (html) {
console.log(html);
if(_.startsWith(_.trim(html), '<div') && _.endsWith(_.trim(html), '</div>')){
var openTag = html.substring(0, html.indexOf('>')+1);
html = html.substring(html.indexOf('>')+1);
html = html.substring(0, html.lastIndexOf('</div>'));
return `${openTag} ${Markdown(html)} </div>`;
}
return html;
};
*/
module.exports = {
marked : Markdown,
render : (rawBrewText)=>{
//Adds in the new div block syntax
let count = 0;
let blockReg = /{{[\w|,]+|}}/g;
const renderer = new Markdown.Renderer();
renderer.paragraph = function (text) {
const matches = text.match(blockReg);
if(!matches) return `<p>${text}</p>\n`;
let matchIndex = 0;
const res = _.reduce(text.split(blockReg), (r, text) => {
if(text) r.push(`<p>${text}</p>\n`);
const block = matches[matchIndex];
if(block && _.startsWith(block, '{{')){
r.push(`<div class="${block.substring(2).split(',').join(' ')}">`);
count++;
}
if(block == '}}' && count !== 0){
r.push('</div>');
count--;
}
matchIndex++;
return r;
}, []).join('\n');
return res;
};
let html = Markdown(rawBrewText, {renderer : renderer, sanitize: true});
html += _.times(count, ()=>{return '</div>'}).join('\n');
return html;
},
validate : (rawBrewText) => {
return [];
/*
var errors = [];
var leftovers = _.reduce(rawBrewText.split('\n'), (acc, line, _lineNumber) => {
var lineNumber = _lineNumber + 1;
var matches = line.match(tagRegex);
if(!matches || !matches.length) return acc;
_.each(matches, (match)=>{
_.each(tagTypes, (type)=>{
if(match == `<${type}`){
acc.push({
type : type,
line : lineNumber
});
}
if(match === `</${type}>`){
if(!acc.length){
errors.push({
line : lineNumber,
type : type,
text : 'Unmatched closing tag',
id : 'CLOSE'
});
}else if(_.last(acc).type == type){
acc.pop();
}else{
errors.push({
line : _.last(acc).line + ' to ' + lineNumber,
type : type,
text : 'Type mismatch on closing tag',
id : 'MISMATCH'
});
acc.pop();
}
}
});
});
return acc;
}, []);
_.each(leftovers, (unmatched)=>{
errors.push({
line : unmatched.line,
type : unmatched.type,
text : "Unmatched opening tag",
id : 'OPEN'
})
});
return errors;
*/
},
};

View File

@@ -3,8 +3,6 @@ const React = require('react');
const _ = require('lodash'); const _ = require('lodash');
const cx = require('classnames'); const cx = require('classnames');
const DISMISS_KEY = 'dismiss_render_warning';
const RenderWarnings = React.createClass({ const RenderWarnings = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
@@ -42,9 +40,6 @@ const RenderWarnings = React.createClass({
} }
}, },
checkWarnings : function(){ checkWarnings : function(){
const hideDismiss = localStorage.getItem(DISMISS_KEY);
if(hideDismiss) return this.setState({warnings : {}});
this.setState({ this.setState({
warnings : _.reduce(this.warnings, (r, fn, type) => { warnings : _.reduce(this.warnings, (r, fn, type) => {
const element = fn(); const element = fn();
@@ -53,16 +48,11 @@ const RenderWarnings = React.createClass({
}, {}) }, {})
}) })
}, },
dismiss : function(){
localStorage.setItem(DISMISS_KEY, true);
this.checkWarnings();
},
render: function(){ render: function(){
if(_.isEmpty(this.state.warnings)) return null; if(_.isEmpty(this.state.warnings)) return null;
return <div className='renderWarnings'> return <div className='renderWarnings'>
<i className='fa fa-times dismiss' onClick={this.dismiss}/> <i className='fa fa-exclamation-triangle' />
<i className='fa fa-exclamation-triangle ohno' />
<h3>Render Warnings</h3> <h3>Render Warnings</h3>
<small>If this homebrew is rendering badly if might be because of the following:</small> <small>If this homebrew is rendering badly if might be because of the following:</small>
<ul>{_.values(this.state.warnings)}</ul> <ul>{_.values(this.state.warnings)}</ul>

View File

@@ -11,22 +11,12 @@
padding-left : 85px; padding-left : 85px;
background-color : @yellow; background-color : @yellow;
color : white; color : white;
i.ohno{ i{
position : absolute; position: absolute;
top : 24px; left: 24px;
left : 24px; opacity: 0.8;
opacity : 0.8; font-size: 2.5em;
font-size : 2.5em; top: 24px;
}
i.dismiss{
position : absolute;
top : 10px;
right : 10px;
cursor : pointer;
opacity : 0.6;
&:hover{
opacity : 1;
}
} }
small{ small{
opacity : 0.7; opacity : 0.7;
@@ -42,10 +32,10 @@
list-style-position : outside; list-style-position : outside;
list-style-type : disc; list-style-type : disc;
li{ li{
font-size : 0.8em;
line-height : 1.6em; line-height : 1.6em;
font-size: 0.8em;
em{ em{
font-weight : 800; font-weight: 800;
} }
} }
} }

View File

@@ -0,0 +1,17 @@
const _ = require('lodash');
const Utils = {
controlKeys : (mapping) => {
return (e) => {
if(!(e.ctrlKey || e.metaKey)) return;
if(typeof mapping[e.key] === 'function'){
mapping[e.key]();
e.stopPropagation();
e.preventDefault();
}
}
},
};
module.exports = Utils;

View File

@@ -29,7 +29,7 @@ var CodeEditor = React.createClass({
value : this.props.value, value : this.props.value,
lineNumbers: true, lineNumbers: true,
lineWrapping : this.props.wrap, lineWrapping : this.props.wrap,
mode : this.props.language mode : this.props.language,
}); });
this.codeMirror.on('change', this.handleChange); this.codeMirror.on('change', this.handleChange);

90
test/admin.test.js Normal file
View File

@@ -0,0 +1,90 @@
const testing = require('./test.init.js');
const request = require('supertest-as-promised');
const config = require('nconf');
const app = require('app.js');
const DB = require('db.js');
const BrewData = require('brew.data.js');
const Error = require('error.js');
let brewA = {
title : 'good title',
text : 'original text',
authors : ['your_dm']
};
describe('Admin API', ()=>{
before('Connect DB', DB.connect);
describe('Brew Lookup', ()=>{
before('Clear DB', BrewData.removeAll);
before('Create brew', ()=>{
return BrewData.create(brewA)
.then((brew)=>{ brewA = brew; });
});
it('throws an error if not admin', ()=>{
return request(app)
.get(`/admin/lookup/${brewA.editId}`)
.expect(401);
});
it('looks up a brew based on the share id', () => {
return request(app)
.get(`/admin/lookup/${brewA.shareId}`)
.set('x-homebrew-admin', config.get('admin:key'))
.expect(200)
.then((res) => {
const brew = res.body;
brew.should.have.property('editId').equal(brewA.editId);
brew.should.have.property('shareId').equal(brewA.shareId);
brew.should.have.property('text').equal(brewA.text);
});
});
it('looks up a brew based on the edit id', () => {
return request(app)
.get(`/admin/lookup/${brewA.editId}`)
.set('x-homebrew-admin', config.get('admin:key'))
.expect(200)
.then((res) => {
const brew = res.body;
brew.should.have.property('editId').equal(brewA.editId);
brew.should.have.property('shareId').equal(brewA.shareId);
brew.should.have.property('text').equal(brewA.text);
});
});
it('looks up a brew based on a partial id', () => {
const query = brewA.editId.substring(0, brewA.editId.length -2);
return request(app)
.get(`/admin/lookup/${query}`)
.set('x-homebrew-admin', config.get('admin:key'))
.expect(200)
.then((res) => {
const brew = res.body;
brew.should.have.property('editId').equal(brewA.editId);
brew.should.have.property('shareId').equal(brewA.shareId);
brew.should.have.property('text').equal(brewA.text);
});
});
it('throws an error if it can not find a brew', ()=>{
return request(app)
.get(`/admin/lookup/BADID`)
.set('x-homebrew-admin', config.get('admin:key'))
.expect(404);
});
});
describe('Invalid Brew', ()=>{
before('Clear DB', BrewData.removeAll);
before('Create brew', ()=>{
return BrewData.create(brewA)
.then((brew)=>{ brewA = brew; });
});
});
});

248
test/api.test.js Normal file
View File

@@ -0,0 +1,248 @@
const Test = require('./test.init.js');
const _ = require('lodash');
const request = require('supertest-as-promised');
const config = require('nconf');
const app = require('app.js');
const DB = require('db.js');
const BrewData = require('brew.data.js');
const BrewGen = require('./brew.gen.js');
const Error = require('error.js');
const UserX = { username : 'userX' };
const UserA = { username : 'userA' };
let UserXToken, UserAToken;
describe('Brew API', () => {
before('Create session token', () => {
UserXToken = Test.getSessionToken(UserX);
UserAToken = Test.getSessionToken(UserA);
});
describe('CRUD', ()=>{
before('Connect DB', DB.connect);
before('Clear DB', BrewData.removeAll);
before('Populate brews', ()=>{
return BrewGen.populateDB(BrewGen.static());
});
describe('Create', () => {
it('creates a new brew', () => {
return request(app)
.post(`/api/brew`)
.send({ text : 'Brew Text' })
.expect(200)
.then((res) => {
const brew = res.body;
brew.should.have.property('editId').that.is.a('string');
brew.should.have.property('shareId').that.is.a('string');
brew.should.have.property('text').equal('Brew Text');
brew.should.not.have.property('_id');
});
});
it('creates a new brew with a session author', () => {
return request(app)
.post(`/api/brew`)
.set('Cookie', `nc_session=${UserXToken}`)
.send({ text : 'Brew Text' })
.expect(200)
.then((res) => {
const brew = res.body;
brew.should.have.property('authors').include(UserX.username);
});
});
});
describe('Update', () => {
it('updates an existing brew', () => {
const storedBrew = BrewGen.get('BrewA');
return request(app)
.put(`/api/brew/${storedBrew.editId}`)
.send({ text : 'New Text' })
.expect(200)
.then((res) => {
const brew = res.body;
brew.should.have.property('editId').equal(storedBrew.editId);
brew.should.have.property('text').equal('New Text');
brew.should.have.property('authors').include(storedBrew.authors[0]);
brew.should.not.have.property('_id');
});
});
it('adds the user as author', () => {
const storedBrew = BrewGen.get('BrewA');
return request(app)
.put(`/api/brew/${storedBrew.editId}`)
.set('Cookie', `nc_session=${UserXToken}`)
.send({ text : 'New Text' })
.expect(200)
.then((res) => {
const brew = res.body;
brew.should.have.property('authors').include(UserX.username);
brew.should.have.property('authors').include(storedBrew.authors[0]);
});
});
it('should throw error on bad edit id', ()=>{
const storedBrew = BrewGen.get('BrewA');
return request(app)
.put(`/api/brew/BADEDITID`)
.send({ text : 'New Text' })
.expect(404)
});
});
describe('Remove', () => {
it('should removes a brew', ()=>{
const storedBrew = BrewGen.get('BrewA');
return request(app)
.del(`/api/brew/${storedBrew.editId}`)
.send()
.expect(200)
.then(() => {
BrewData.getByEdit(storedBrew.editId)
.then(() => { throw 'Brew found when one should not have been'; })
.catch((err) => {
err.should.be.instanceof(Error.noBrew);
})
});
});
});
})
describe('Search', () => {
before('Connect DB', DB.connect);
before('Clear DB', BrewData.removeAll);
before('Populate brews', ()=>{
return BrewGen.populateDB(BrewGen.static());
});
it('should be able to search for all published brews', ()=>{
return request(app)
.get(`/api/brew`)
.query({})
.send()
.expect(200)
.then((res) => {
const result = res.body;
result.total.should.be.equal(2);
result.brews.should.have.brews('BrewB','BrewD');
result.brews[0].should.not.have.property('editId');
});
});
it('should be able to search for brews with given terms', ()=>{
return request(app)
.get(`/api/brew`)
.query({
terms : '5e ranger'
})
.send()
.expect(200)
.then((res) => {
const result = res.body;
result.total.should.be.equal(1);
result.brews.should.have.brews('BrewD');
});
});
it('should be able to sort the search', ()=>{
return request(app)
.get(`/api/brew`)
.query({
sort : { views : 1}
})
.send()
.expect(200)
.then((res) => {
const result = res.body;
result.total.should.be.equal(2);
result.brews[0].should.be.brew('BrewD');
result.brews[1].should.be.brew('BrewB');
});
});
it('should use pagniation on the search', ()=>{
return request(app)
.get(`/api/brew`)
.query({
limit : 1,
page : 1,
sort : { views : -1}
})
.send()
.expect(200)
.then((res) => {
const result = res.body;
result.total.should.be.equal(2);
result.brews[0].should.be.brew('BrewD');
})
});
it('should return all brews and editIds if admin', ()=>{
return request(app)
.get(`/api/brew`)
.query({})
.set('x-homebrew-admin', config.get('admin:key'))
.send()
.expect(200)
.then((res) => {
const result = res.body;
const brewCount = _.size(BrewGen.static());
result.total.should.be.equal(brewCount);
result.brews.length.should.be.equal(brewCount);
result.brews[0].should.have.property('editId');
});
});
});
describe('User', () => {
before('Connect DB', DB.connect);
before('Clear DB', BrewData.removeAll);
before('Populate brews', ()=>{
return BrewGen.populateDB(BrewGen.static());
});
it('should be able to query brews for a specific user', ()=>{
return request(app)
.get(`/api/user/userA`)
.send()
.expect(200)
.then((res) => {
const result = res.body;
result.total.should.be.equal(1);
result.brews.length.should.be.equal(1);
result.brews.should.have.brews('BrewB');
result.brews[0].should.not.have.property('editId');
});
});
it('should have full access if loggedin user is queried user', ()=>{
return request(app)
.get(`/api/user/userA`)
.set('Cookie', `nc_session=${UserAToken}`)
.send()
.expect(200)
.then((res) => {
const result = res.body;
result.total.should.be.equal(3);
result.brews.length.should.be.equal(3);
result.brews.should.have.brews('BrewA', 'BrewB', 'BrewC');
result.brews[0].should.have.property('editId');
});
});
it('should have full access if admin', ()=>{
return request(app)
.get(`/api/user/userA`)
.set('x-homebrew-admin', config.get('admin:key'))
.send()
.expect(200)
.then((res) => {
const result = res.body;
result.total.should.be.equal(3);
result.brews.length.should.be.equal(3);
result.brews.should.have.brews('BrewA', 'BrewB', 'BrewC');
result.brews[0].should.have.property('editId');
});
});
});
});

111
test/brew.gen.js Normal file
View File

@@ -0,0 +1,111 @@
const _ = require('lodash');
const BrewData = require('../server/brew.data.js');
let PopulatedBrews = {};
module.exports = {
//TODO: Add in a generator for old brews to test the old rendering code
random : (num = 20)=>{
return _.times(num, ()=>{
//TODO: Build better generator
return {
title : 'BrewA',
description : '',
text : '',
authors : _.sampleSize(['userA','userB','userC','userD'], _.random(0, 3)),
systems : _.sampleSize(['5e', '4e', '3.5e', 'Pathfinder'], _.random(0,2)),
views : _.random(0,1000),
published : !!_.random(0,1)
};
});
},
static : () => {
return {
BrewA : {
title : 'Brew-Alpha',
description : 'fancy',
authors : ['userA'],
systems : [],
views : 12,
published : false
},
BrewB : {
title : 'Brew-Beta',
description : 'very fancy',
authors : ['userA'],
systems : [],
views : 7,
published : true
},
BrewC : {
title : 'Brew-Charlie',
description : 'test',
authors : ['userA', 'userB'],
systems : [],
views : 0,
published : false
},
BrewD : {
title : 'Brew-Delta',
description : 'test super amazing brew for 5e. Geared for Rangers.',
authors : ['userC'],
systems : [],
views : 1,
published : true
}
};
},
populateDB : (brewCollection)=>{
PopulatedBrews = {};
return Promise.all(_.map(brewCollection, (brewData, id) => {
return BrewData.create(brewData)
.then((brew)=>{
PopulatedBrews[id] = brew;
});
})
);
},
get : (brewId) => {
return PopulatedBrews[brewId]
},
chaiPlugin : (chai, utils) => {
chai.Assertion.addMethod('brews', function(...brewIds){
new chai.Assertion(this._obj).to.be.instanceof(Array);
const valid = _.every(brewIds, (brewId) => {
const storedBrew = PopulatedBrews[brewId];
if(!storedBrew) return false;
return _.some(this._obj, (brew)=>{
return brew.shareId == storedBrew.shareId &&
brew.title == storedBrew.title &&
brew.views == storedBrew.views;
});
});
this.assert(
valid,
`expect #{this} to have brews ${brewIds.join(', ')}`,
`expect #{this} to not have brews ${brewIds.join(', ')}`
)
});
chai.Assertion.addMethod('brew', function(brewId){
new chai.Assertion(this._obj).to.be.instanceof(Object);
const brew = this._obj;
const storedBrew = PopulatedBrews[brewId];
const valid = storedBrew &&
brew.shareId == storedBrew.shareId &&
brew.title == storedBrew.title &&
brew.views == storedBrew.views;
this.assert(
valid,
`expect #{this} to be brew ${brewId}`,
`expect #{this} to not be brew ${brewId}`
)
});
}
};

108
test/brew.test.js Normal file
View File

@@ -0,0 +1,108 @@
const testing = require('./test.init.js');
const DB = require('db.js');
const BrewData = require('brew.data.js');
const Error = require('error.js');
let storedBrew = {
title : 'good title',
text : 'original text'
};
describe('Brew Data', () => {
before('Connect DB', DB.connect);
before('Clear DB', BrewData.removeAll);
before('Create brew', ()=>{
return BrewData.create(storedBrew)
.then((brew)=>{ storedBrew = brew; });
});
it('generates edit/share ID on create', () => {
return BrewData.create({
text : 'Brew Text'
}).then((brew) => {
brew.should.have.property('editId').that.is.a('string');
brew.should.have.property('shareId').that.is.a('string');
brew.should.have.property('text').that.is.a('string');
brew.should.have.property('views').equal(0);
});
});
it('generates edit/share ID on create even if given one', () => {
return BrewData.create({
editId : 'NOPE',
shareId : 'NOTTA'
}).then((brew) => {
brew.should.have.property('editId').not.equal('NOPE');
brew.should.have.property('shareId').not.equal('NOTTA');
});
});
it('can update an existing brew', () => {
return BrewData.update(storedBrew.editId,{
text : 'New Text'
}).then((brew) => {
brew.should.have.property('editId').equal(storedBrew.editId);
brew.should.have.property('text').equal('New Text');
brew.should.have.property('title').equal(storedBrew.title);
})
});
it('properly returns a brew if retrieved by just share', () => {
return BrewData.getByShare(storedBrew.shareId)
.then((brew) => {
brew.should.not.have.property('editId');
brew.should.have.property('shareId').equal(storedBrew.shareId);
brew.should.have.property('views').equal(1);
})
});
it('can properly remove a brew', () => {
return BrewData.remove(storedBrew.editId)
.then(() => {
return BrewData.getByEdit(storedBrew.editId)
})
.then(() => { throw 'Brew found when one should not have been'; })
.catch((err) => {
err.should.be.an.instanceof(Error.noBrew);
});
});
it('throws the right error if can not find brew', () => {
return BrewData.getByEdit('NOT A REAL ID')
.then(() => { throw 'Brew found when one should not have been'; })
.catch((err) => {
err.should.be.an.instanceof(Error.noBrew);
});
});
describe('Title Generation', () => {
it('should use the title if given one', () => {
return BrewData.create({
title : 'Actual Title',
text : '# Not this'
}).then((brew) => {
brew.should.have.property('title').equal('Actual Title');
});
});
it('should use the first header found if no title provided', () => {
return BrewData.create({
text : 'Not this \n # But This'
}).then((brew) => {
brew.should.have.property('title').equal('But This');
})
});
it('should use the first line if no headers are found', () => {
return BrewData.create({
text : 'First line \n second line'
}).then((brew) => {
brew.should.have.property('title').equal('First line');
});
});
});
});

3
test/config.json Normal file
View File

@@ -0,0 +1,3 @@
{
"log_level" : "silent"
}

20
test/markdown.test.js Normal file
View File

@@ -0,0 +1,20 @@
const testing = require('./test.init.js');
const Markdown = require('../shared/homebrewery/markdown.new.js');
describe('Markdown', ()=>{
it('should do a thing', ()=>{
const res = Markdown.render(`
test
<div> cool stuff </div>
test2
`)
console.log(res);
});
});

144
test/middleware.test.js Normal file
View File

@@ -0,0 +1,144 @@
const _ = require('lodash');
const testing = require('./test.init.js');
const request = require('supertest-as-promised');
const jwt = require('jwt-simple');
const DB = require('db.js');
const BrewData = require('brew.data.js');
const Error = require('error.js');
const config = require('nconf');
const mw = require('middleware.js');
const requestHandler = (req, res) => {
return res.status(200).json(_.pick(req, ['brew', 'account', 'admin', 'params', 'query', 'body']));
};
const test_user = {
username : 'cool guy'
};
describe('Middleware', () => {
let app = undefined;
let session_token = '';
before('create session token', () => {
session_token = jwt.encode(test_user, config.get('jwt_secret'));
});
beforeEach('setup test server', ()=>{
app = require('express')();
app.use(require('cookie-parser')());
});
describe('Account', ()=>{
it('should get the account for a session', () => {
app.use(mw.account);
app.use(requestHandler)
return request(app).get('/')
.set('Cookie', `nc_session=${session_token}`)
.send()
.expect(200)
.then((res) => {
const req = res.body;
req.should.have.property('account').is.a('object');
req.account.should.have.property('username').equal(test_user.username);
});
});
it('should not have an account for an invalid session', () => {
app.use(mw.account);
app.use(requestHandler)
return request(app).get('/')
.set('Cookie', `nc_session=BADSESSION`)
.send()
.expect(200)
.then((res) => {
const req = res.body;
req.should.not.have.property('account');
});
});
});
describe('Brew', ()=>{
let storedBrew = {
text : 'brew brew',
authors : [test_user.username]
};
before('Connect DB', DB.connect);
before('Clear DB', BrewData.removeAll);
before('Create brew', ()=>{
return BrewData.create(storedBrew)
.then((brew)=>{ storedBrew = brew; });
});
it('should load brew with editId params', ()=>{
app.get('/:editId', mw.loadBrew, requestHandler);
return request(app).get('/' + storedBrew.editId)
.send()
.expect(200)
.then((res) => {
const req = res.body;
req.should.have.property('brew').is.a('object');
req.brew.should.have.property('editId').equal(storedBrew.editId);
});
});
it('should view brew with shareId params', ()=>{
app.get('/:shareId', mw.viewBrew, requestHandler);
return request(app).get('/' + storedBrew.shareId)
.send()
.expect(200)
.then((res) => {
const req = res.body;
req.should.have.property('brew').is.a('object');
req.brew.should.not.have.property('editId');
req.brew.should.have.property('shareId').equal(storedBrew.shareId);
req.brew.should.have.property('views').equal(1);
});
});
});
describe('Admin', ()=>{
it('should detect when you use the admin key', () => {
app.use(mw.admin);
app.use(requestHandler)
return request(app).get('/')
.set('x-homebrew-admin', config.get('admin:key'))
.expect(200)
.then((res) => {
const req = res.body;
req.should.have.property('admin').equal(true);
});
});
it('should block you if you are not an admin', ()=>{
app.use(mw.admin);
app.use(mw.adminOnly);
app.get(requestHandler);
app.use(Error.expressHandler);
return request(app).get('/')
.set('x-homebrew-admin', 'BADADMIN')
.send()
.expect(401);
});
it('should let your through witch basic auth', () => {
app.use(mw.adminLogin);
app.use(requestHandler);
return request(app).get('/')
.auth(config.get('admin:user'), config.get('admin:pass'))
.send()
.expect(200);
});
it('should block you if basic auth is wrong', () => {
app.use(mw.adminLogin);
app.use(requestHandler);
app.use(Error.expressHandler);
return request(app).get('/')
.auth('baduser', 'badpassword')
.send()
.expect(401);
});
});
});

186
test/search.test.js Normal file
View File

@@ -0,0 +1,186 @@
const Test = require('./test.init.js');
const _ = require('lodash');
const DB = require('db.js');
const BrewData = require('brew.data.js');
const BrewGen = require('./brew.gen.js');
//const Error = require('error.js');
describe('Brew Search', () => {
before('Connect DB', DB.connect);
before('Clear DB', BrewData.removeAll);
before('Populate brews', ()=>{
return BrewGen.populateDB(BrewGen.static());
});
describe('Searching', ()=>{
it('should return a total and a brew array', ()=>{
return BrewData.search()
.then((result) => {
result.total.should.be.a('number');
result.brews.should.be.an('array');
})
});
it('should be able to search for all brews', ()=>{
return BrewData.search()
.then((result) => {
const brewCount = _.size(BrewGen.static());
result.total.should.be.equal(brewCount);
result.brews.length.should.be.equal(brewCount);
})
});
});
describe('Pagniation', () => {
it('should return the exact number of brews based on limit', () => {
return BrewData.search({}, {
limit : 2
})
.then((result) => {
result.total.should.be.equal(_.size(BrewGen.static()));
result.brews.length.should.be.equal(2);
})
});
it('should return the correct pages when specified', () => {
return BrewData.search({}, {
limit : 2,
page : 1,
sort : { views : 1 }
})
.then((result) => {
result.brews.should.have.brews('BrewA', 'BrewB');
})
});
it('should return a partial list if on the last page', () => {
return BrewData.search({}, {
limit : 3,
page : 1
})
.then((result) => {
result.brews.length.should.be.equal(1);
});
});
});
describe('Sorting', ()=>{
it('should sort ASC', () => {
return BrewData.search({}, {
sort : { views : 1 }
})
.then((result) => {
result.brews[0].should.be.brew('BrewC');
result.brews[1].should.be.brew('BrewD');
result.brews[2].should.be.brew('BrewB');
result.brews[3].should.be.brew('BrewA');
})
});
it('should sort DESC', () => {
return BrewData.search({}, {
sort : { views : -1 }
})
.then((result) => {
result.brews[0].should.be.brew('BrewA');
result.brews[1].should.be.brew('BrewB');
result.brews[2].should.be.brew('BrewD');
result.brews[3].should.be.brew('BrewC');
})
});
});
describe('Permissions', () => {
it('should only fetch published brews', () => {
return BrewData.search({}, {}, false)
.then((result) => {
result.total.should.be.equal(2);
result.brews.should.have.brews('BrewB', 'BrewD');
})
});
it('fetched brews should not have text or editId', () => {
return BrewData.search({}, {}, false)
.then((result) => {
result.brews[0].should.not.have.property('text');
result.brews[0].should.not.have.property('editId');
})
});
it('if full access, brews should have editid, but no text', () => {
return BrewData.search({}, {}, true)
.then((result) => {
result.brews[0].should.not.have.property('text');
result.brews[0].should.have.property('editId');
})
});
});
describe('Term Search', ()=>{
it('should search brews based on title', () => {
return BrewData.termSearch('Charlie')
.then((result) => {
result.total.should.be.equal(1);
result.brews.should.have.brews('BrewC');
})
});
it('should search brews based on description', () => {
return BrewData.termSearch('fancy')
.then((result) => {
result.total.should.be.equal(2);
result.brews.should.have.brews('BrewA', 'BrewB');
})
});
it('should search brews based on multiple terms', () => {
return BrewData.termSearch('ranger 5e')
.then((result) => {
result.total.should.be.equal(1);
result.brews.should.have.brews('BrewD');
})
});
it('should perform an AND operation on the provided terms', () => {
return BrewData.termSearch('Brew Delta GARBAGE')
.then((result) => {
result.total.should.be.equal(0);
});
});
it('should search brews based on a combination of both', () => {
return BrewData.termSearch('Brew Beta fancy')
.then((result) => {
result.total.should.be.equal(1);
result.brews.should.have.brews('BrewB');
});
});
it('should not worry about the case of the terms', () => {
return BrewData.termSearch('FANCY')
.then((result) => {
result.total.should.be.equal(2);
result.brews.should.have.brews('BrewA', 'BrewB');
});
});
});
describe('User Search', ()=>{
it('should return brews just for a single user', () => {
return BrewData.userSearch('userA')
.then((result) => {
result.total.should.be.equal(3);
result.brews.should.have.brews('BrewA', 'BrewB', 'BrewC');
});
});
it('should return nothing if provided a non-exsistent user', () => {
return BrewData.userSearch('userXYZ')
.then((result) => {
result.total.should.be.equal(0);
});
});
});
});

24
test/test.init.js Normal file
View File

@@ -0,0 +1,24 @@
require('app-module-path').addPath('./server');
const config = require('nconf')
.argv()
.env({ lowerCase: true })
.file('testing', { file: `test/config.json` })
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
.file('defaults', { file: 'config/default.json' });
const Chai = require('chai')
.use(require('chai-as-promised'))
.use(require('chai-subset'))
.use(require('./brew.gen.js').chaiPlugin);
const log = require('loglevel');
log.setLevel(config.get('log_level'));
const jwt = require('jwt-simple');
module.exports = {
should: Chai.should(),
getSessionToken : (userInfo) => {
return jwt.encode(userInfo, config.get('jwt_secret'));
}
};