1
0
mirror of https://github.com/stolksdorf/homebrewery.git synced 2025-12-13 05:25:58 +00:00

Added in metadata editor

This commit is contained in:
Scott Tolksdorf
2016-11-23 14:47:28 -05:00
parent ccdb6a3cbd
commit e61bf22788
19 changed files with 483 additions and 191 deletions

View File

@@ -1,25 +1,31 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
var CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
var Snippets = require('./snippets/snippets.js');
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./MetadataEditor/MetadataEditor.jsx');
var splice = function(str, index, inject){
const splice = function(str, index, inject){
return str.slice(0, index) + inject + str.slice(index);
};
var execute = function(val){
if(_.isFunction(val)) return val();
return val;
}
const SNIPPETBAR_HEIGHT = 25;
var Editor = React.createClass({
const Editor = React.createClass({
getDefaultProps: function() {
return {
value : "",
onChange : function(){}
value : '',
onChange : ()=>{},
metadata : {},
onMetadataChange : ()=>{}
};
},
getInitialState: function() {
return {
showMetadataEditor: false
};
},
cursorPosition : {
@@ -27,7 +33,6 @@ var Editor = React.createClass({
ch : 0
},
componentDidMount: function() {
this.updateEditorSize();
window.addEventListener("resize", this.updateEditorSize);
@@ -37,8 +42,8 @@ var Editor = React.createClass({
},
updateEditorSize : function() {
var paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= this.refs.snippetBar.clientHeight + 1;
let paneHeight = this.refs.main.parentNode.clientHeight;
paneHeight -= SNIPPETBAR_HEIGHT + 1;
this.refs.codeEditor.codeMirror.setSize(null, paneHeight);
},
@@ -48,38 +53,37 @@ var Editor = React.createClass({
handleCursorActivty : function(curpos){
this.cursorPosition = curpos;
},
handleSnippetClick : function(injectText){
var lines = this.props.value.split('\n');
handleInject : function(injectText){
const lines = this.props.value.split('\n');
lines[this.cursorPosition.line] = splice(lines[this.cursorPosition.line], this.cursorPosition.ch, injectText);
this.handleTextChange(lines.join('\n'));
this.refs.codeEditor.setCursorPosition(this.cursorPosition.line, this.cursorPosition.ch + injectText.length);
},
handgleToggle : function(){
this.setState({
showMetadataEditor : !this.state.showMetadataEditor
})
},
//Called when there are changes to the editor's dimensions
update : function(){
this.refs.codeEditor.updateSize();
},
renderSnippetGroups : function(){
return _.map(Snippets, (snippetGroup)=>{
return <SnippetGroup
groupName={snippetGroup.groupName}
icon={snippetGroup.icon}
snippets={snippetGroup.snippets}
key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick}
/>
})
renderMetadataEditor : function(){
if(!this.state.showMetadataEditor) return;
return <MetadataEditor
metadata={this.props.metadata}
onChange={this.props.onMetadataChange}
/>
},
render : function(){
return(
<div className='editor' ref='main'>
<div className='snippetBar' ref='snippetBar'>
{this.renderSnippetGroups()}
</div>
<SnippetBar onInject={this.handleInject} onToggle={this.handgleToggle} showmeta={this.state.showMetadataEditor} />
{this.renderMetadataEditor()}
<CodeEditor
ref='codeEditor'
wrap={true}
@@ -99,40 +103,3 @@ module.exports = Editor;
var SnippetGroup = React.createClass({
getDefaultProps: function() {
return {
groupName : '',
icon : 'fa-rocket',
snippets : [],
onSnippetClick : function(){},
};
},
handleSnippetClick : function(snippet){
this.props.onSnippetClick(execute(snippet.gen));
},
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={this.handleSnippetClick.bind(this, snippet)}>
<i className={'fa fa-fw ' + snippet.icon} />
{snippet.name}
</div>
})
},
render : function(){
return <div className='snippetGroup'>
<div className='text'>
<i className={'fa fa-fw ' + this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets()}
</div>
</div>
},
});

View File

@@ -2,76 +2,10 @@
.editor{
position : relative;
width : 100%;
.snippetBar{
@height : 25px;
position : relative;
height : @height;
background-color : #ddd;
.snippetGroup{
/*
.animate(background-color);
margin : 0px 8px;
padding : 3px;
font-size : 13px;
border-radius : 5px;
&:hover, &.selected{
background-color : #999;
}
*/
display : inline-block;
height : @height;
padding : 0px 5px;
cursor : pointer;
font-size : 0.6em;
font-weight : 800;
line-height : @height;
text-transform : uppercase;
border-right : 1px solid black;
i{
vertical-align : middle;
margin-right : 3px;
font-size : 1.2em;
}
&:hover, &.selected{
background-color : #999;
}
.text{
line-height : @height;
.groupName{
margin-left : 6px;
font-size : 10px;
}
}
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown{
position : absolute;
visibility : hidden;
top : 100%;
z-index : 1000;
padding : 0px;
margin-left: -5px;
background-color : #ddd;
.snippet{
.animate(background-color);
padding : 10px;
cursor : pointer;
font-size : 10px;
i{
margin-right: 8px;
font-size : 13px;
}
&:hover{
background-color : #999;
}
}
}
}
}
.codeEditor{
height : 100%;
}
}

View File

@@ -0,0 +1,106 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder']
const MetadataEditor = React.createClass({
getDefaultProps: function() {
return {
metadata: {
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
},
onChange : ()=>{}
};
},
handleFieldChange : function(name, e){
this.props.onChange(_.merge({}, this.props.metadata, {
[name] : e.target.value
}))
},
handleSystem : function(system, e){
if(e.target.checked){
this.props.metadata.systems.push(system);
}else{
this.props.metadata.systems = _.without(this.props.metadata.systems, system);
}
this.props.onChange(this.props.metadata);
},
handlePublish : function(val){
this.props.onChange(_.merge({}, this.props.metadata, {
published : val
}));
},
renderSystems : function(){
return _.map(SYSTEMS, (val)=>{
return <label key={val}>
<input
type='checkbox'
checked={_.includes(this.props.metadata.systems, val)}
onChange={this.handleSystem.bind(null, val)} />
{val}
</label>
});
},
renderPublish : function(){
if(this.props.metadata.published){
return <button className='unpublish' onClick={this.handlePublish.bind(null, false)}>
<i className='fa fa-ban' /> unpublish
</button>
}else{
return <button className='publish' onClick={this.handlePublish.bind(null, true)}>
<i className='fa fa-globe' /> publish
</button>
}
},
render : function(){
return <div className='metadataEditor'>
<div className='field title'>
<label>title</label>
<input type='text' className='value'
value={this.props.metadata.title}
onChange={this.handleFieldChange.bind(null, 'title')} />
</div>
<div className='field description'>
<label>description</label>
<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>
*/}
<div className='field systems'>
<label>systems</label>
<div className='value'>
{this.renderSystems()}
</div>
</div>
<div className='field publish'>
<label>publish</label>
<div className='value'>
{this.renderPublish()}
<small>Published homebrews will be publicly searchable (eventually...)</small>
</div>
</div>
</div>
}
});
module.exports = MetadataEditor;

View File

@@ -0,0 +1,63 @@
.metadataEditor{
position : absolute;
z-index : 10000;
box-sizing : border-box;
width : 100%;
padding : 25px;
background-color : #999;
.field{
display : flex;
width : 100%;
margin-bottom : 10px;
&>label{
display : inline-block;
vertical-align : top;
width : 80px;
font-size : 0.7em;
font-weight : 800;
line-height : 1.8em;
text-transform : uppercase;
flex-grow : 0;
}
&>.value{
flex-grow : 1;
}
}
.description.field textarea.value{
resize : none;
height : 5em;
font-family : 'Open Sans', sans-serif;
font-size : 0.8em;
}
.systems.field .value{
label{
vertical-align : middle;
margin-right : 15px;
cursor : pointer;
font-size : 0.7em;
font-weight : 800;
user-select : none;
}
input{
vertical-align : middle;
cursor : pointer;
}
}
.publish.field .value{
position : relative;
button.publish{
.button(@blueLight);
}
button.unpublish{
.button(@silver);
}
small{
position : absolute;
bottom : -15px;
left : 0px;
font-size : 0.6em;
font-style : italic;
}
}
}

View File

@@ -0,0 +1,91 @@
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const Snippets = require('./snippets/snippets.js');
const execute = function(val){
if(_.isFunction(val)) return val();
return val;
}
const Snippetbar = React.createClass({
getDefaultProps: function() {
return {
onInject : ()=>{},
onToggle : ()=>{},
showmeta : false
};
},
handleSnippetClick : function(injectedText){
this.props.onInject(injectedText)
},
renderSnippetGroups : function(){
return _.map(Snippets, (snippetGroup)=>{
return <SnippetGroup
groupName={snippetGroup.groupName}
icon={snippetGroup.icon}
snippets={snippetGroup.snippets}
key={snippetGroup.groupName}
onSnippetClick={this.handleSnippetClick}
/>
})
},
render : function(){
return <div className='snippetBar'>
{this.renderSnippetGroups()}
<div className={cx('toggleMeta', {selected: this.props.showmeta})}
onClick={this.props.onToggle}>
<i className='fa fa-database' />
</div>
</div>
}
});
module.exports = Snippetbar;
const SnippetGroup = React.createClass({
getDefaultProps: function() {
return {
groupName : '',
icon : 'fa-rocket',
snippets : [],
onSnippetClick : function(){},
};
},
handleSnippetClick : function(snippet){
this.props.onSnippetClick(execute(snippet.gen));
},
renderSnippets : function(){
return _.map(this.props.snippets, (snippet)=>{
return <div className='snippet' key={snippet.name} onClick={this.handleSnippetClick.bind(this, snippet)}>
<i className={'fa fa-fw ' + snippet.icon} />
{snippet.name}
</div>
})
},
render : function(){
return <div className='snippetGroup'>
<div className='text'>
<i className={'fa fa-fw ' + this.props.icon} />
<span className='groupName'>{this.props.groupName}</span>
</div>
<div className='dropdown'>
{this.renderSnippets()}
</div>
</div>
},
});

View File

@@ -0,0 +1,72 @@
.snippetBar{
@height : 25px;
position : relative;
height : @height;
background-color : #ddd;
.toggleMeta{
position : absolute;
top : 0px;
right : 0px;
height : @height;
width : @height;
cursor : pointer;
line-height : @height;
text-align : center;
&:hover, &.selected{
background-color : #999;
}
}
.snippetGroup{
display : inline-block;
height : @height;
padding : 0px 5px;
cursor : pointer;
font-size : 0.6em;
font-weight : 800;
line-height : @height;
text-transform : uppercase;
border-right : 1px solid black;
i{
vertical-align : middle;
margin-right : 3px;
font-size : 1.2em;
}
&:hover, &.selected{
background-color : #999;
}
.text{
line-height : @height;
.groupName{
font-size : 10px;
}
}
&:hover{
.dropdown{
visibility : visible;
}
}
.dropdown{
position : absolute;
top : 100%;
visibility : hidden;
z-index : 1000;
margin-left : -5px;
padding : 0px;
background-color : #ddd;
.snippet{
.animate(background-color);
padding : 10px;
cursor : pointer;
font-size : 10px;
i{
margin-right : 8px;
font-size : 13px;
}
&:hover{
background-color : #999;
}
}
}
}
}

View File

@@ -13,4 +13,7 @@
flex : auto;
}
}
}

View File

@@ -1,47 +1,52 @@
var React = require('react');
var _ = require('lodash');
var cx = require('classnames');
var request = require("superagent");
const React = require('react');
const _ = require('lodash');
const cx = require('classnames');
const request = require("superagent");
var Nav = require('naturalcrit/nav/nav.jsx');
var Navbar = require('../../navbar/navbar.jsx');
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
var EditTitle = require('../../navbar/editTitle.navitem.jsx');
var ReportIssue = require('../../navbar/issue.navitem.jsx');
var PrintLink = require('../../navbar/print.navitem.jsx');
var RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
const ReportIssue = require('../../navbar/issue.navitem.jsx');
const PrintLink = require('../../navbar/print.navitem.jsx');
const RecentlyEdited = require('../../navbar/recent.navitem.jsx').edited;
var SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
var Editor = require('../../editor/editor.jsx');
var BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
var Markdown = require('naturalcrit/markdown.js');
const Markdown = require('naturalcrit/markdown.js');
const SAVE_TIMEOUT = 3000;
var EditPage = React.createClass({
const EditPage = React.createClass({
getDefaultProps: function() {
return {
id : null,
brew : {
title : '',
text : '',
shareId : null,
editId : null,
createdAt : null,
updatedAt : null,
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
}
};
},
getInitialState: function() {
return {
title : this.props.brew.title,
text: this.props.brew.text,
brew : this.props.brew,
isSaving : false,
isPending : false,
errors : null,
@@ -60,7 +65,7 @@ var EditPage = React.createClass({
};
this.setState({
htmlErrors : Markdown.validate(this.state.text)
htmlErrors : Markdown.validate(this.state.brew.text)
})
document.addEventListener('keydown', this.handleControlKeys);
@@ -85,13 +90,15 @@ var EditPage = React.createClass({
this.refs.editor.update();
},
handleTitleChange : function(title){
handleMetadataChange : function(metadata){
this.setState({
title : title,
isPending : true
brew : _.merge({}, this.state.brew, metadata),
isPending : true,
}, ()=>{
console.log(this.hasChanges());
(this.hasChanges() ? this.debounceSave() : this.debounceSave.cancel());
});
(this.hasChanges() ? this.debounceSave() : this.debounceSave.cancel());
},
handleTextChange : function(text){
@@ -101,7 +108,7 @@ var EditPage = React.createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState({
text : text,
brew : _.merge({}, this.state.brew, {text : text}),
isPending : true,
htmlErrors : htmlErrors
});
@@ -122,11 +129,9 @@ var EditPage = React.createClass({
hasChanges : function(){
if(this.savedBrew){
if(this.state.text !== this.savedBrew.text) return true;
if(this.state.title !== this.savedBrew.title) return true;
return !_.isEqual(this.state.brew, this.savedBrew)
}else{
if(this.state.text !== this.props.brew.text) return true;
if(this.state.title !== this.props.brew.title) return true;
return !_.isEqual(this.state.brew, this.props.brew)
}
return false;
},
@@ -136,15 +141,12 @@ var EditPage = React.createClass({
this.setState({
isSaving : true,
errors : null,
htmlErrors : Markdown.validate(this.state.text)
htmlErrors : Markdown.validate(this.state.brew.text)
});
request
.put('/api/update/' + this.props.brew.editId)
.send({
text : this.state.text,
title : this.state.title
})
.send(this.state.brew)
.end((err, res) => {
if(err){
this.setState({
@@ -184,17 +186,17 @@ var EditPage = React.createClass({
if(this.state.isSaving){
return <Nav.item className='save' icon="fa-spinner fa-spin">saving...</Nav.item>
}
if(!this.state.isPending && !this.state.isSaving){
return <Nav.item className='save saved'>saved.</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>
<EditTitle title={this.state.title} onChange={this.handleTitleChange} />
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderSaveButton()}
@@ -216,8 +218,14 @@ var EditPage = React.createClass({
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<BrewRenderer text={this.state.text} errors={this.state.htmlErrors} />
<Editor
ref='editor'
value={this.state.brew.text}
onChange={this.handleTextChange}
metadata={this.state.brew}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.brew.text} errors={this.state.htmlErrors} />
</SplitPane>
</div>
</div>

View File

@@ -1,7 +1,7 @@
.editPage{
.navItem.save{
width : 75px;
width : 105px;
text-align : center;
&.saved{
cursor : initial;

View File

@@ -20,7 +20,15 @@ const KEY = 'homebrewery-new';
const NewPage = React.createClass({
getInitialState: function() {
return {
title : '',
metadata : {
title : '',
description : '',
tags : '',
published : false,
authors : [],
systems : []
},
text: '',
isSaving : false,
errors : []
@@ -53,9 +61,9 @@ const NewPage = React.createClass({
this.refs.editor.update();
},
handleTitleChange : function(title){
handleMetadataChange : function(metadata){
this.setState({
title : title
metadata : _.merge({}, this.state.metadata, metadata)
});
},
@@ -73,10 +81,9 @@ const NewPage = React.createClass({
});
request.post('/api')
.send({
title : this.state.title,
.send(_.merge({}, this.state.metadata, {
text : this.state.text
})
}))
.end((err, res)=>{
if(err){
this.setState({
@@ -117,7 +124,7 @@ const NewPage = React.createClass({
renderNavbar : function(){
return <Navbar>
<Nav.section>
<EditTitle title={this.state.title} onChange={this.handleTitleChange} />
<Nav.item className='brewTitle'>{this.state.metadata.title}</Nav.item>
</Nav.section>
<Nav.section>
@@ -133,7 +140,13 @@ const NewPage = React.createClass({
{this.renderNavbar()}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove} ref='pane'>
<Editor value={this.state.text} onChange={this.handleTextChange} ref='editor'/>
<Editor
ref='editor'
value={this.state.text}
onChange={this.handleTextChange}
metadata={this.state.metadata}
onMetadataChange={this.handleMetadataChange}
/>
<BrewRenderer text={this.state.text} errors={this.state.errors} />
</SplitPane>
</div>

View File

@@ -7,7 +7,12 @@ var HomebrewSchema = mongoose.Schema({
editId : {type : String, default: shortid.generate, index: { unique: true }},
title : {type : String, default : ""},
text : {type : String, default : ""},
description : {type : String, default : ""},
tags : {type : String, default : ""},
systems : [String],
authors : [String],
published : {type : Boolean, default : false},
createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now},

View File

@@ -1,9 +1,9 @@
@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';
@font-face {
font-family : CodeLight;
src : url('/assets/naturalcrit/styles/CODE Light.otf');
@@ -13,8 +13,38 @@
src : url('/assets/naturalcrit/styles/CODE Bold.otf');
}
html,body, #reactContainer{
min-height: 100vh;
height: 100vh;
margin: 0;
height : 100vh;
min-height : 100vh;
margin : 0;
font-family : 'Open Sans', sans-serif;
}
*{
box-sizing : border-box;
}
button{
.button();
}
.button(@backgroundColor : @green){
.animate(background-color);
display : inline-block;
padding : 0.6em 1.2em;
cursor : pointer;
background-color : @backgroundColor;
font-family : 'Open Sans', sans-serif;
font-size : 0.8em;
font-weight : 800;
color : white;
text-decoration : none;
text-transform : uppercase;
border : none;
outline : none;
&:hover{
background-color : darken(@backgroundColor, 5%);
}
&:active{
background-color : darken(@backgroundColor, 10%);
}
&:disabled{
background-color : @silver !important;
}
}