mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2026-01-23 09:53:06 +00:00
Compare commits
54 Commits
92487d2f52
...
noHtml
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbab4f4959 | ||
|
|
22d9982888 | ||
|
|
76ced9ca49 | ||
|
|
b1db8040a4 | ||
|
|
c8b089f7fb | ||
|
|
97c0443c76 | ||
|
|
c470bed591 | ||
|
|
4593099914 | ||
|
|
6030134de2 | ||
|
|
7e6f42f062 | ||
|
|
75111acefb | ||
|
|
26bcb3395a | ||
|
|
a826aaffd9 | ||
|
|
8018442f25 | ||
|
|
8e58e5aca9 | ||
|
|
2f82d3875e | ||
|
|
efee8ff05c | ||
|
|
a405c7cfb2 | ||
|
|
dfcb04fd09 | ||
|
|
728277f861 | ||
|
|
0878439750 | ||
|
|
e77532acef | ||
|
|
cd2eb5fdce | ||
|
|
37de888f03 | ||
|
|
1aa79b32d9 | ||
|
|
894e345a44 | ||
|
|
07f249b23e | ||
|
|
baaa82ed34 | ||
|
|
0d0f0d8eb0 | ||
|
|
d77fa0a3dc | ||
|
|
a26c4e2092 | ||
|
|
ca40ec5a2d | ||
|
|
987363ed41 | ||
|
|
7b38bccec1 | ||
|
|
174c2973f7 | ||
|
|
66ca09b36d | ||
|
|
5820564894 | ||
|
|
3dc4c13178 | ||
|
|
537a75b2ab | ||
|
|
a0bc4fddf8 | ||
|
|
25e0a1607a | ||
|
|
68ecf749ea | ||
|
|
10f4759471 | ||
|
|
5ba3f98696 | ||
|
|
95c09ba7ad | ||
|
|
1173af5803 | ||
|
|
924b398768 | ||
|
|
41303e6918 | ||
|
|
f75f60aa1e | ||
|
|
f4cf288f27 | ||
|
|
8abf6abf99 | ||
|
|
95aa803c61 | ||
|
|
47396e5c7e | ||
|
|
7581d155a6 |
@@ -26,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)
|
||||
- Removed a lot of unused files in shared
|
||||
- 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
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
var cx = require('classnames');
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
|
||||
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() {
|
||||
return {
|
||||
url : "",
|
||||
admin_key : "",
|
||||
homebrews : [],
|
||||
admin_key : '',
|
||||
};
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Nav.base>
|
||||
<Nav.section>
|
||||
<Nav.item icon='fa-magic' className='homebreweryLogo'>
|
||||
Homebrewery Admin
|
||||
</Nav.item>
|
||||
</Nav.section>
|
||||
</Nav.base>
|
||||
},
|
||||
|
||||
render : function(){
|
||||
var self = this;
|
||||
return(
|
||||
<div className='admin'>
|
||||
return <div className='admin'>
|
||||
{this.renderNavbar()}
|
||||
<main className='content'>
|
||||
<BrewLookup adminKey={this.props.admin_key} />
|
||||
<AdminSearch adminKey={this.props.admin_key} />
|
||||
|
||||
<header>
|
||||
<div className='container'>
|
||||
<i className='fa fa-rocket' />
|
||||
naturalcrit admin
|
||||
</div>
|
||||
</header>
|
||||
<div className='dangerZone'>Danger Zone</div>
|
||||
|
||||
<div className='container'>
|
||||
|
||||
<HomebrewAdmin homebrews={this.props.homebrews} admin_key={this.props.admin_key} />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
<InvalidBrew adminKey={this.props.admin_key} />
|
||||
</main>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
html,body, #reactContainer, .naturalCrit{
|
||||
@import 'naturalcrit/styles/core.less';
|
||||
html,body, #reactRoot{
|
||||
min-height : 100%;
|
||||
}
|
||||
|
||||
@sidebarWidth : 250px;
|
||||
|
||||
body{
|
||||
background-color : #eee;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
color : #4b5055;
|
||||
font-weight : 100;
|
||||
text-rendering : optimizeLegibility;
|
||||
height : 100%;
|
||||
margin : 0;
|
||||
padding : 0;
|
||||
height : 100%;
|
||||
background-color : #ddd;
|
||||
font-family : 'Open Sans', sans-serif;
|
||||
font-weight : 100;
|
||||
color : #4b5055;
|
||||
text-rendering : optimizeLegibility;
|
||||
}
|
||||
|
||||
.admin{
|
||||
|
||||
header{
|
||||
.admin {
|
||||
nav {
|
||||
background-color : @red;
|
||||
font-size: 2em;
|
||||
padding : 20px 0px;
|
||||
color : white;
|
||||
margin-bottom: 30px;
|
||||
i{
|
||||
margin-right: 30px;
|
||||
.navItem{
|
||||
background-color : @red;
|
||||
}
|
||||
.homebreweryLogo{
|
||||
font-family : CodeBold;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
86
client/admin/adminSearch/adminSearch.jsx
Normal file
86
client/admin/adminSearch/adminSearch.jsx
Normal 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;
|
||||
17
client/admin/adminSearch/adminSearch.less
Normal file
17
client/admin/adminSearch/adminSearch.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,7 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const request = require('superagent');
|
||||
const Moment = require('moment');
|
||||
|
||||
const BrewTable = require('../brewTable/brewTable.jsx');
|
||||
|
||||
const BrewLookup = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
@@ -16,7 +15,8 @@ const BrewLookup = React.createClass({
|
||||
return {
|
||||
query:'',
|
||||
resultBrew : null,
|
||||
searching : false
|
||||
searching : false,
|
||||
error : null
|
||||
};
|
||||
},
|
||||
|
||||
@@ -26,13 +26,14 @@ const BrewLookup = React.createClass({
|
||||
})
|
||||
},
|
||||
lookup : function(){
|
||||
this.setState({ searching : true });
|
||||
this.setState({ searching : true, error : null });
|
||||
|
||||
request.get(`/admin/lookup/${this.state.query}`)
|
||||
.query({ admin_key : this.props.adminKey })
|
||||
.set('x-homebrew-admin', this.props.adminKey)
|
||||
.end((err, res) => {
|
||||
this.setState({
|
||||
searching : false,
|
||||
error : err && err.toString(),
|
||||
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.resultBrew) return <div className='noBrew'>No brew found.</div>;
|
||||
|
||||
return <BrewTable brews={[this.state.resultBrew ]} />
|
||||
|
||||
/*
|
||||
const brew = this.state.resultBrew;
|
||||
return <div className='brewRow'>
|
||||
<div>{brew.title}</div>
|
||||
<div>{brew.authors.join(', ')}</div>
|
||||
<div><a href={'/edit/' + brew.editId} target='_blank'>/edit/{brew.editId}</a></div>
|
||||
<div><a href={'/share/' + brew.shareId} target='_blank'>/share/{brew.shareId}</a></div>
|
||||
<div><a href={'/edit/' + brew.editId} target='_blank'>{brew.editId}</a></div>
|
||||
<div><a href={'/share/' + brew.shareId} target='_blank'>{brew.shareId}</a></div>
|
||||
<div>{Moment(brew.updatedAt).fromNow()}</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>
|
||||
},
|
||||
|
||||
@@ -60,6 +78,7 @@ const BrewLookup = React.createClass({
|
||||
<button onClick={this.lookup}><i className='fa fa-search'/></button>
|
||||
|
||||
{this.renderFoundBrew()}
|
||||
{this.renderError()}
|
||||
</div>
|
||||
}
|
||||
});
|
||||
13
client/admin/brewLookup/brewLookup.less
Normal file
13
client/admin/brewLookup/brewLookup.less
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
.brewLookup{
|
||||
height : 200px;
|
||||
input{
|
||||
height : 33px;
|
||||
margin-bottom : 20px;
|
||||
padding : 0px 10px;
|
||||
}
|
||||
.error{
|
||||
font-weight : 800;
|
||||
color : @red;
|
||||
}
|
||||
}
|
||||
54
client/admin/brewTable/brewTable.jsx
Normal file
54
client/admin/brewTable/brewTable.jsx
Normal 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;
|
||||
44
client/admin/brewTable/brewTable.less
Normal file
44
client/admin/brewTable/brewTable.less
Normal 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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.brewLookup{
|
||||
height : 200px;
|
||||
input{
|
||||
height : 33px;
|
||||
padding : 0px 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
54
client/admin/invalidBrew/invalidBrew.jsx
Normal file
54
client/admin/invalidBrew/invalidBrew.jsx
Normal 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;
|
||||
5
client/admin/invalidBrew/invalidBrew.less
Normal file
5
client/admin/invalidBrew/invalidBrew.less
Normal file
@@ -0,0 +1,5 @@
|
||||
.invalidBrew{
|
||||
button{
|
||||
margin: 10px 4px;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,15 @@ const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
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 EditPage = require('./pages/editPage/editPage.jsx');
|
||||
const UserPage = require('./pages/userPage/userPage.jsx');
|
||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||
//const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||
const PrintPage = require('./pages/printPage/printPage.jsx');
|
||||
|
||||
let Router;
|
||||
@@ -17,45 +19,27 @@ const Homebrew = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
url : '',
|
||||
welcomeText : '',
|
||||
changelog : '',
|
||||
version : '0.0.0',
|
||||
account : null,
|
||||
brew : {
|
||||
title : '',
|
||||
text : '',
|
||||
shareId : null,
|
||||
editId : null,
|
||||
createdAt : null,
|
||||
updatedAt : null,
|
||||
}
|
||||
loginPath : '',
|
||||
|
||||
user : undefined,
|
||||
brew : undefined,
|
||||
brews : []
|
||||
};
|
||||
},
|
||||
componentWillMount: function() {
|
||||
global.account = this.props.account;
|
||||
global.version = this.props.version;
|
||||
|
||||
BrewActions.init({
|
||||
version : this.props.version,
|
||||
brew : this.props.brew
|
||||
});
|
||||
AccountActions.init({
|
||||
user : this.props.user,
|
||||
loginPath : this.props.loginPath
|
||||
});
|
||||
|
||||
Router = CreateRouter({
|
||||
'/edit/:id' : (args) => {
|
||||
if(!this.props.brew.editId){
|
||||
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} />
|
||||
},
|
||||
'/edit/:id' : <EditPage />,
|
||||
'/share/:id' : <SharePage />,
|
||||
'/user/:username' : (args) => {
|
||||
return <UserPage
|
||||
username={args.username}
|
||||
@@ -68,15 +52,9 @@ const Homebrew = React.createClass({
|
||||
'/print' : (args, query) => {
|
||||
return <PrintPage query={query}/>;
|
||||
},
|
||||
'/new' : (args) => {
|
||||
return <NewPage />
|
||||
},
|
||||
'/changelog' : (args) => {
|
||||
return <SharePage
|
||||
brew={{title : 'Changelog', text : this.props.changelog}} />
|
||||
},
|
||||
'*' : <HomePage
|
||||
welcomeText={this.props.welcomeText} />,
|
||||
'/new' : <NewPage />,
|
||||
'/changelog' : <SharePage />,
|
||||
'*' : <HomePage />,
|
||||
});
|
||||
},
|
||||
render : function(){
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
const React = require('react');
|
||||
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){
|
||||
if(global.account){
|
||||
return <Nav.item href={`/user/${global.account.username}`} color='yellow' icon='fa-user'>
|
||||
{global.account.username}
|
||||
const user = Store.getUser();
|
||||
if(user && user == props.userPage){
|
||||
return <Nav.item onClick={Actions.logout} color='yellow' icon='fa-user-times'>
|
||||
logout
|
||||
</Nav.item>
|
||||
}
|
||||
let url = '';
|
||||
if(typeof window !== 'undefined'){
|
||||
url = window.location.href
|
||||
if(user){
|
||||
return <Nav.item href={`/user/${user}`} color='yellow' icon='fa-user'>
|
||||
{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
|
||||
</Nav.item>
|
||||
};
|
||||
9
client/homebrew/navbar/brewTitle.navitem.jsx
Normal file
9
client/homebrew/navbar/brewTitle.navitem.jsx
Normal 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};
|
||||
})
|
||||
@@ -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);
|
||||
76
client/homebrew/navbar/continousSave.navitem.jsx
Normal file
76
client/homebrew/navbar/continousSave.navitem.jsx
Normal 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);
|
||||
@@ -2,34 +2,9 @@ const React = require('react');
|
||||
const _ = require('lodash');
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
|
||||
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(){
|
||||
return <Nav.base>
|
||||
<Nav.section>
|
||||
@@ -37,9 +12,7 @@ const Navbar = React.createClass({
|
||||
<Nav.item href='/' className='homebrewLogo'>
|
||||
<div>The Homebrewery</div>
|
||||
</Nav.item>
|
||||
<Nav.item>{`v${this.state.ver}`}</Nav.item>
|
||||
|
||||
{/*this.renderChromeWarning()*/}
|
||||
<Nav.item>{`v${Store.getVersion()}`}</Nav.item>
|
||||
</Nav.section>
|
||||
{this.props.children}
|
||||
</Nav.base>
|
||||
|
||||
@@ -13,32 +13,6 @@
|
||||
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{
|
||||
font-size : 12px;
|
||||
font-weight : 800;
|
||||
@@ -125,4 +99,34 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
client/homebrew/navbar/navitems.js
Normal file
10
client/homebrew/navbar/navitems.js
Normal 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'),
|
||||
};
|
||||
@@ -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;
|
||||
37
client/homebrew/navbar/staticSave.navitem.jsx
Normal file
37
client/homebrew/navbar/staticSave.navitem.jsx
Normal 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()
|
||||
}
|
||||
});
|
||||
@@ -1,230 +1,63 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require("superagent");
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||
const Navbar = require('../../navbar/navbar.jsx');
|
||||
const Items = require('../../navbar/navitems.js');
|
||||
|
||||
const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
const PrintLink = require('../../navbar/print.navitem.jsx');
|
||||
const Account = require('../../navbar/account.navitem.jsx');
|
||||
//const RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
|
||||
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
|
||||
const Utils = require('homebrewery/utils.js');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
|
||||
const SAVE_TIMEOUT = 3000;
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
const Actions = require('homebrewery/brew.actions.js');
|
||||
|
||||
|
||||
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(){
|
||||
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);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
window.onbeforeunload = function(){};
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
|
||||
|
||||
handleControlKeys : function(e){
|
||||
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>
|
||||
},
|
||||
|
||||
handleControlKeys : Utils.controlKeys({
|
||||
s : Actions.save,
|
||||
p : Actions.print
|
||||
}),
|
||||
render : function(){
|
||||
return <div className='editPage page'>
|
||||
{this.renderNavbar()}
|
||||
|
||||
<SmartNav />
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<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>
|
||||
<BrewInterface />
|
||||
</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;
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const request = require("superagent");
|
||||
|
||||
const Nav = require('naturalcrit/nav/nav.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 SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
|
||||
|
||||
|
||||
const Actions = require('homebrewery/brew.actions.js');
|
||||
//
|
||||
|
||||
const HomePage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
welcomeText : '',
|
||||
ver : '0.0.0'
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
return {
|
||||
text: this.props.welcomeText
|
||||
};
|
||||
},
|
||||
handleSave : function(){
|
||||
request.post('/api')
|
||||
.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
|
||||
});
|
||||
Actions.saveNew();
|
||||
},
|
||||
|
||||
renderNavbar : function(){
|
||||
return <Navbar ver={this.props.ver}>
|
||||
return <Navbar>
|
||||
<Nav.section>
|
||||
<PatreonNavItem />
|
||||
<IssueNavItem />
|
||||
@@ -70,15 +43,13 @@ const HomePage = React.createClass({
|
||||
render : function(){
|
||||
return <div className='homePage page'>
|
||||
{this.renderNavbar()}
|
||||
|
||||
<div className='content'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
|
||||
<BrewRenderer text={this.state.text} />
|
||||
</SplitPane>
|
||||
<BrewInterface />
|
||||
</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' />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,158 +1,59 @@
|
||||
const React = require('react');
|
||||
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 Navbar = require('../../navbar/navbar.jsx');
|
||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||
const IssueNavItem = require('../../navbar/issue.navitem.jsx');
|
||||
const Items = require('../../navbar/navitems.js');
|
||||
|
||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||
const Editor = require('../../editor/editor.jsx');
|
||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
const Actions = require('homebrewery/brew.actions.js');
|
||||
|
||||
const BrewInterface = require('homebrewery/brewInterface/brewInterface.jsx');
|
||||
const Utils = require('homebrewery/utils.js');
|
||||
|
||||
const KEY = 'homebrewery-new';
|
||||
|
||||
const NewPage = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
metadata : {
|
||||
title : '',
|
||||
description : '',
|
||||
tags : '',
|
||||
published : false,
|
||||
authors : [],
|
||||
systems : []
|
||||
},
|
||||
|
||||
text: '',
|
||||
isSaving : false,
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
componentDidMount: function() {
|
||||
const storage = localStorage.getItem(KEY);
|
||||
if(storage){
|
||||
this.setState({
|
||||
text : storage
|
||||
})
|
||||
}
|
||||
try{
|
||||
const storedBrew = JSON.parse(localStorage.getItem(KEY));
|
||||
if(storedBrew && storedBrew.text) Actions.setBrew(storedBrew);
|
||||
}catch(e){}
|
||||
Store.updateEmitter.on('change', this.saveToLocal);
|
||||
document.addEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
componentWillUnmount: function() {
|
||||
Store.updateEmitter.removeListener('change', this.saveToLocal);
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
|
||||
handleControlKeys : function(e){
|
||||
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) 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>
|
||||
saveToLocal : function(){
|
||||
localStorage.setItem(KEY, JSON.stringify(Store.getBrew()));
|
||||
},
|
||||
handleControlKeys : Utils.controlKeys({
|
||||
s : Actions.saveNew,
|
||||
p : Actions.localPrint
|
||||
}),
|
||||
|
||||
render : function(){
|
||||
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'>
|
||||
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
|
||||
<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>
|
||||
<BrewInterface />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
.newPage{
|
||||
|
||||
.saveButton{
|
||||
background-color: @orange;
|
||||
&:hover{
|
||||
background-color: @green;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
const Markdown = require('homebrewery/markdown.js');
|
||||
|
||||
const PrintPage = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
@@ -12,21 +12,17 @@ const PrintPage = React.createClass({
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
brewText: this.props.brew.text
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
if(this.props.query.local){
|
||||
this.setState({ brewText : localStorage.getItem(this.props.query.local)});
|
||||
}
|
||||
|
||||
if(this.props.query.dialog) window.print();
|
||||
},
|
||||
|
||||
renderPages : function(){
|
||||
return _.map(this.state.brewText.split('\\page'), (page, index) => {
|
||||
return <div
|
||||
|
||||
@@ -9,9 +9,13 @@ const ReportIssue = require('../../navbar/issue.navitem.jsx');
|
||||
//const RecentlyViewed = require('../../navbar/recent.navitem.jsx').viewed;
|
||||
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({
|
||||
getDefaultProps: function() {
|
||||
@@ -33,36 +37,48 @@ const SharePage = React.createClass({
|
||||
componentWillUnmount: function() {
|
||||
document.removeEventListener('keydown', this.handleControlKeys);
|
||||
},
|
||||
handleControlKeys : function(e){
|
||||
if(!(e.ctrlKey || e.metaKey)) return;
|
||||
const P_KEY = 80;
|
||||
if(e.keyCode == P_KEY){
|
||||
window.open(`/print/${this.props.brew.shareId}?dialog=true`, '_blank').focus();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleControlKeys : Utils.controlKeys({
|
||||
p : Actions.print
|
||||
}),
|
||||
|
||||
renderMetatags : function(brew){
|
||||
let metatags = [
|
||||
<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(){
|
||||
const brew = Store.getBrew();
|
||||
return <div className='sharePage page'>
|
||||
{this.renderMetatags(brew)}
|
||||
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||
<Nav.item className='brewTitle'>{brew.title}</Nav.item>
|
||||
</Nav.section>
|
||||
|
||||
<Nav.section>
|
||||
<ReportIssue />
|
||||
{/*<RecentlyViewed brew={this.props.brew} />*/}
|
||||
<PrintLink shareId={this.props.brew.shareId} />
|
||||
<Nav.item href={'/source/' + this.props.brew.shareId} color='teal' icon='fa-code'>
|
||||
<PrintLink shareId={brew.shareId} />
|
||||
<Nav.item href={'/source/' + brew.shareId} color='teal' icon='fa-code'>
|
||||
source
|
||||
</Nav.item>
|
||||
<Account />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
<div className='content'>
|
||||
<BrewRenderer text={this.props.brew.text} />
|
||||
<BrewRenderer brewText={brew.text} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -52,12 +52,13 @@ const UserPage = React.createClass({
|
||||
|
||||
render : function(){
|
||||
const brews = this.getSortedBrews();
|
||||
console.log('user brews', brews);
|
||||
|
||||
return <div className='userPage page'>
|
||||
<Navbar>
|
||||
<Nav.section>
|
||||
<RecentNavItem.both />
|
||||
<Account />
|
||||
<Account userPage={this.props.username} />
|
||||
</Nav.section>
|
||||
</Navbar>
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
{
|
||||
"host" : "homebrewery.local.naturalcrit.com:8000",
|
||||
"naturalcrit_url" : "local.naturalcrit.com:8010",
|
||||
"secret" : "secret"
|
||||
"log_level" : "info",
|
||||
"login_path" : "/dev/login",
|
||||
"jwt_secret" : "secretsecret",
|
||||
"admin" : {
|
||||
"user" : "admin",
|
||||
"pass" : "password",
|
||||
"key" : "adminadminadmin"
|
||||
}
|
||||
}
|
||||
4
config/production.json
Normal file
4
config/production.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"login_path" : "http://naturalcrit.com/login",
|
||||
"log_level" : "warn"
|
||||
}
|
||||
4
config/staging.json
Normal file
4
config/staging.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"login_path" : "http://staging.naturalcrit.com/login",
|
||||
"log_level" : "trace"
|
||||
}
|
||||
28
package.json
28
package.json
@@ -1,39 +1,53 @@
|
||||
{
|
||||
"name": "homebrewery",
|
||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||
"version": "2.7.1",
|
||||
"version": "3.0.0",
|
||||
"scripts": {
|
||||
"dev": "node scripts/dev.js",
|
||||
"quick": "node scripts/quick.js",
|
||||
"build": "node scripts/build.js",
|
||||
"phb": "node scripts/phb.js",
|
||||
"populate": "node scripts/populate.js",
|
||||
"prod": "set NODE_ENV=production&& 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",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"babel-preset-env": "^1.1.8",
|
||||
"basic-auth": "^1.0.3",
|
||||
"body-parser": "^1.14.2",
|
||||
"classnames": "^2.2.0",
|
||||
"codemirror": "^5.22.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"egads": "^1.0.1",
|
||||
"express": "^4.13.3",
|
||||
"jwt-simple": "^0.5.1",
|
||||
"lodash": "^4.11.2",
|
||||
"lodash": "^4.17.3",
|
||||
"loglevel": "^1.4.1",
|
||||
"marked": "^0.3.5",
|
||||
"moment": "^2.11.0",
|
||||
"mongoose": "^4.3.3",
|
||||
"nconf": "^0.8.4",
|
||||
"pico-flux": "^1.1.0",
|
||||
"pico-flux": "^2.1.2",
|
||||
"pico-router": "^1.1.0",
|
||||
"react": "^15.0.2",
|
||||
"react-dom": "^15.0.2",
|
||||
"react": "^15.4.1",
|
||||
"react-dom": "^15.4.1",
|
||||
"shortid": "^2.2.4",
|
||||
"striptags": "^2.1.1",
|
||||
"superagent": "^1.6.1",
|
||||
"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
@@ -12,10 +12,8 @@ const Proj = require('./project.json');
|
||||
Promise.resolve()
|
||||
.then(jsx('homebrew', './client/homebrew/homebrew.jsx', Proj.libs, './shared'))
|
||||
.then(less('homebrew', './shared'))
|
||||
|
||||
.then(jsx('admin', './client/admin/admin.jsx', Proj.libs, './shared'))
|
||||
.then(less('admin', './shared'))
|
||||
|
||||
.then(assets(Proj.assets, ['./shared', './client']))
|
||||
.then(livereload())
|
||||
.then(server('./server.js', ['server']))
|
||||
|
||||
22
scripts/populate.js
Normal file
22
scripts/populate.js
Normal 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);
|
||||
148
server.js
148
server.js
@@ -1,141 +1,35 @@
|
||||
const _ = require('lodash');
|
||||
const jwt = require('jwt-simple');
|
||||
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')());
|
||||
|
||||
const config = require('nconf')
|
||||
.argv()
|
||||
.env({ lowerCase: true })
|
||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||
.file('defaults', { file: 'config/default.json' });
|
||||
|
||||
const log = require('loglevel');
|
||||
log.setLevel(config.get('log_level'));
|
||||
|
||||
//DB
|
||||
require('mongoose')
|
||||
.connect(process.env.MONGODB_URI || process.env.MONGOLAB_URI || 'mongodb://localhost/naturalcrit')
|
||||
.connection.on('error', () => {
|
||||
console.log('Error : Could not connect to a Mongo Database.');
|
||||
console.log(' If you are running locally, make sure mongodb.exe is running.');
|
||||
});
|
||||
require('./server/db.js').connect();
|
||||
|
||||
//Server
|
||||
const app = require('./server/app.js');
|
||||
|
||||
//Account MIddleware
|
||||
/*
|
||||
app.use((req, res, next) => {
|
||||
if(req.cookies && req.cookies.nc_session){
|
||||
try{
|
||||
req.account = jwt.decode(req.cookies.nc_session, config.get('secret'));
|
||||
}catch(e){}
|
||||
log.debug('---------------------------');
|
||||
log.debug(req.method, req.path);
|
||||
|
||||
if (req.params) {
|
||||
log.debug('req params', req.params);
|
||||
}
|
||||
return next();
|
||||
if (req.query) {
|
||||
log.debug('req query', req.query);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
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('<', '<').replaceAll('>', '>');
|
||||
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}`);
|
||||
const httpServer = app.listen(PORT, () => {
|
||||
log.info(`server on port:${PORT}`);
|
||||
});
|
||||
@@ -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
60
server/admin.routes.js
Normal 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
28
server/app.js
Normal 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
68
server/brew.api.js
Normal 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
101
server/brew.data.js
Normal 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
66
server/brew.search.js
Normal 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
36
server/db.js
Normal 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
29
server/dev.routes.js
Normal 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
25
server/error.js
Normal 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;
|
||||
@@ -36,6 +36,8 @@ const getGoodBrewTitle = (text) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
router.post('/api', (req, res)=>{
|
||||
|
||||
let authors = [];
|
||||
|
||||
88
server/interface.routes.js
Normal file
88
server/interface.routes.js
Normal 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, { '<' : '<', '>' : '>' });
|
||||
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
69
server/middleware.js
Normal 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
23
server/utils.js
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
18
shared/homebrewery/account.actions.js
Normal file
18
shared/homebrewery/account.actions.js
Normal 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;
|
||||
27
shared/homebrewery/account.store.js
Normal file
27
shared/homebrewery/account.store.js
Normal 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;
|
||||
78
shared/homebrewery/brew.actions.js
Normal file
78
shared/homebrewery/brew.actions.js
Normal 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);
|
||||
69
shared/homebrewery/brew.store.js
Normal file
69
shared/homebrewery/brew.store.js
Normal 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;
|
||||
@@ -6,14 +6,13 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
|
||||
|
||||
|
||||
const splice = function(str, index, inject){
|
||||
return str.slice(0, index) + inject + str.slice(index);
|
||||
};
|
||||
|
||||
const SNIPPETBAR_HEIGHT = 25;
|
||||
|
||||
const Editor = React.createClass({
|
||||
const BrewEditor = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
value : '',
|
||||
@@ -67,14 +66,17 @@ const Editor = React.createClass({
|
||||
})
|
||||
},
|
||||
|
||||
getCurrentPage : function(){
|
||||
const lines = this.props.value.split('\n').slice(0, this.cursorPosition.line + 1);
|
||||
return _.reduce(lines, (r, line) => {
|
||||
if(line.indexOf('\\page') !== -1) r++;
|
||||
return r;
|
||||
}, 1);
|
||||
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();
|
||||
},
|
||||
|
||||
//TODO: convert this into a generic function for columns and blocks
|
||||
highlightPageLines : function(){
|
||||
if(!this.refs.codeEditor) return;
|
||||
const codeMirror = this.refs.codeEditor.codeMirror;
|
||||
@@ -89,17 +91,6 @@ const Editor = React.createClass({
|
||||
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(){
|
||||
if(!this.state.showMetadataEditor) return;
|
||||
return <MetadataEditor
|
||||
@@ -109,37 +100,32 @@ const Editor = React.createClass({
|
||||
},
|
||||
|
||||
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} />
|
||||
|
||||
{/*
|
||||
<div className='brewJump' onClick={this.brewJump}>
|
||||
<i className='fa fa-arrow-right' />
|
||||
</div>
|
||||
*/}
|
||||
this.highlightPageLines();
|
||||
|
||||
return<div className='brewEditor' 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} />
|
||||
</div>
|
||||
|
||||
/*
|
||||
<div className='brewJump' onClick={this.brewJump}>
|
||||
<i className='fa fa-arrow-right' />
|
||||
</div>
|
||||
);
|
||||
*/
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Editor;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = BrewEditor;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
.editor{
|
||||
.brewEditor{
|
||||
position : relative;
|
||||
width : 100%;
|
||||
|
||||
.codeEditor{
|
||||
height : 100%;
|
||||
.pageLine{
|
||||
@@ -25,5 +24,4 @@
|
||||
justify-content:center;
|
||||
.tooltipLeft("Jump to brew page");
|
||||
}
|
||||
|
||||
}
|
||||
13
shared/homebrewery/brewEditor/brewEditor.smart.jsx
Normal file
13
shared/homebrewery/brewEditor/brewEditor.smart.jsx
Normal 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,
|
||||
};
|
||||
});
|
||||
@@ -74,6 +74,7 @@ const MetadataEditor = React.createClass({
|
||||
},
|
||||
|
||||
renderPublish : function(){
|
||||
//TODO: Move the publish element into here
|
||||
if(this.props.metadata.published){
|
||||
return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}>
|
||||
<i className='fa fa-ban' /> unpublish
|
||||
@@ -139,13 +140,12 @@ const MetadataEditor = React.createClass({
|
||||
<textarea value={this.props.metadata.description} className='value'
|
||||
onChange={this.handleFieldChange.bind(null, 'description')} />
|
||||
</div>
|
||||
{/*}
|
||||
<div className='field tags'>
|
||||
<label>tags</label>
|
||||
<textarea value={this.props.metadata.tags}
|
||||
onChange={this.handleFieldChange.bind(null, 'tags')} />
|
||||
<div className='field thumbnail'>
|
||||
<label>thumbnail</label>
|
||||
<input type='text' className='value'
|
||||
value={this.props.metadata.thumbnail}
|
||||
onChange={this.handleFieldChange.bind(null, 'thumbnail')} />
|
||||
</div>
|
||||
*/}
|
||||
|
||||
<div className='field systems'>
|
||||
<label>systems</label>
|
||||
@@ -76,4 +76,7 @@
|
||||
font-size: 0.8em;
|
||||
line-height : 1.5em;
|
||||
}
|
||||
.thumbnail.field{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
cursor : pointer;
|
||||
line-height : @height;
|
||||
text-align : center;
|
||||
.tooltipLeft("Edit Brew Metadata");
|
||||
&:hover, &.selected{
|
||||
background-color : #999;
|
||||
}
|
||||
22
shared/homebrewery/brewInterface/brewInterface.jsx
Normal file
22
shared/homebrewery/brewInterface/brewInterface.jsx
Normal 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;
|
||||
3
shared/homebrewery/brewInterface/brewInterface.less
Normal file
3
shared/homebrewery/brewInterface/brewInterface.less
Normal file
@@ -0,0 +1,3 @@
|
||||
.brewInterface{
|
||||
|
||||
}
|
||||
@@ -2,11 +2,12 @@ const React = require('react');
|
||||
const _ = require('lodash');
|
||||
const cx = require('classnames');
|
||||
|
||||
const Markdown = require('naturalcrit/markdown.js');
|
||||
const Markdown = require('homebrewery/markdown.new.js');
|
||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||
|
||||
//TODO: move to the brew renderer
|
||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx')
|
||||
const Store = require('homebrewery/brew.store.js');
|
||||
|
||||
|
||||
const PAGE_HEIGHT = 1056;
|
||||
const PPR_THRESHOLD = 50;
|
||||
@@ -14,24 +15,19 @@ const PPR_THRESHOLD = 50;
|
||||
const BrewRenderer = React.createClass({
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
text : '',
|
||||
brewText : '',
|
||||
errors : []
|
||||
};
|
||||
},
|
||||
getInitialState: function() {
|
||||
const pages = this.props.text.split('\\page');
|
||||
const pages = this.props.brewText.split('\\page');
|
||||
|
||||
return {
|
||||
viewablePageNumber: 0,
|
||||
height : 0,
|
||||
isMounted : false,
|
||||
|
||||
usePPR : true,
|
||||
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD,
|
||||
|
||||
errors : []
|
||||
usePPR : pages.length >= PPR_THRESHOLD
|
||||
};
|
||||
},
|
||||
height : 0,
|
||||
@@ -49,7 +45,7 @@ const BrewRenderer = React.createClass({
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
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({
|
||||
pages : pages,
|
||||
usePPR : pages.length >= PPR_THRESHOLD
|
||||
10
shared/homebrewery/brewRenderer/brewRenderer.smart.jsx
Normal file
10
shared/homebrewery/brewRenderer/brewRenderer.smart.jsx
Normal 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()
|
||||
}
|
||||
});
|
||||
123
shared/homebrewery/markdown.new.js
Normal file
123
shared/homebrewery/markdown.new.js
Normal 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;
|
||||
*/
|
||||
},
|
||||
};
|
||||
|
||||
17
shared/homebrewery/utils.js
Normal file
17
shared/homebrewery/utils.js
Normal 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;
|
||||
@@ -29,7 +29,7 @@ var CodeEditor = React.createClass({
|
||||
value : this.props.value,
|
||||
lineNumbers: true,
|
||||
lineWrapping : this.props.wrap,
|
||||
mode : this.props.language
|
||||
mode : this.props.language,
|
||||
});
|
||||
|
||||
this.codeMirror.on('change', this.handleChange);
|
||||
|
||||
90
test/admin.test.js
Normal file
90
test/admin.test.js
Normal 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
248
test/api.test.js
Normal 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
111
test/brew.gen.js
Normal 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
108
test/brew.test.js
Normal 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
3
test/config.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"log_level" : "silent"
|
||||
}
|
||||
20
test/markdown.test.js
Normal file
20
test/markdown.test.js
Normal 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
144
test/middleware.test.js
Normal 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
186
test/search.test.js
Normal 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
24
test/test.init.js
Normal 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'));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user