diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
index d692fe262..6f162de95 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx
@@ -1,9 +1,11 @@
+/* eslint-disable max-lines */
require('./metadataEditor.less');
const React = require('react');
const createClass = require('create-react-class');
const _ = require('lodash');
const cx = require('classnames');
const request = require('superagent');
+const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
@@ -17,7 +19,7 @@ const MetadataEditor = createClass({
editId : null,
title : '',
description : '',
- tags : '',
+ tags : [],
published : false,
authors : [],
systems : [],
@@ -45,9 +47,10 @@ const MetadataEditor = createClass({
},
handleFieldChange : function(name, e){
- this.props.onChange(_.merge({}, this.props.metadata, {
+ this.props.onChange({
+ ...this.props.metadata,
[name] : e.target.value
- }));
+ });
},
handleSystem : function(system, e){
if(e.target.checked){
@@ -64,9 +67,10 @@ const MetadataEditor = createClass({
this.props.onChange(this.props.metadata);
},
handlePublish : function(val){
- this.props.onChange(_.merge({}, this.props.metadata, {
+ this.props.onChange({
+ ...this.props.metadata,
published : val
- }));
+ });
},
handleDelete : function(){
@@ -193,13 +197,11 @@ const MetadataEditor = createClass({
{this.renderThumbnail()}
- {/*}
-
-
-
- */}
+
+ this.handleFieldChange('tags', e)}/>
{this.renderAuthors()}
diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.less b/client/homebrew/editor/metadataEditor/metadataEditor.less
index 9dd03b373..fd2c33ac8 100644
--- a/client/homebrew/editor/metadataEditor/metadataEditor.less
+++ b/client/homebrew/editor/metadataEditor/metadataEditor.less
@@ -1,3 +1,4 @@
+@import 'naturalcrit/styles/colors.less';
.metadataEditor{
position : absolute;
@@ -108,4 +109,86 @@
font-size: 0.8em;
line-height : 1.5em;
}
+
+ .field .list {
+ display: flex;
+ flex-wrap: wrap;
+
+ > * {
+ flex: 0 0 auto;
+ }
+
+ #groupedIcon {
+ #backgroundColors;
+ display: inline-block;
+ height: ~"calc(100% + 0.6em)";
+ position: relative;
+ top: -0.3em;
+ right: -0.3em;
+ cursor: pointer;
+ min-width: 20px;
+ text-align: center;
+ color: white;
+
+ i {
+ position: relative;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+
+ &:not(:last-child) {
+ border-right: 1px solid black;
+ }
+
+ &:last-child {
+ border-radius: 0 0.5em 0.5em 0;
+ }
+ }
+
+ .badge {
+ background-color: #dddddd;
+ border-radius: .5em;
+ font-size: .9em;
+ margin: 2px;
+ padding: .3em;
+
+ .icon {
+ #groupedIcon
+ }
+ }
+
+ .input-group {
+ height: ~"calc(.9em + 4px + .6em)";
+
+ input {
+ border-radius: .5em 0 0 .5em;
+ }
+
+ input:last-child {
+ border-radius: .5em;
+ }
+
+ .value {
+ width: 7.5vw;
+ min-width: 75px;
+ height: 100%;
+ }
+
+ .invalid:focus {
+ background-color: pink;
+ }
+
+ .icon {
+ #groupedIcon;
+ height: 97%;
+ font-size: .8em;
+ right: 1px;
+ top: -.54em;
+
+ i {
+ font-size: 1.125em;
+ }
+ }
+ }
+ }
}
diff --git a/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx b/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx
new file mode 100644
index 000000000..790cfba95
--- /dev/null
+++ b/client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx
@@ -0,0 +1,142 @@
+const React = require('react');
+const createClass = require('create-react-class');
+const _ = require('lodash');
+
+const StringArrayEditor = createClass({
+ displayName : 'StringArrayEditor',
+ getDefaultProps : function() {
+ return {
+ label : '',
+ values : [],
+ valuePatterns : null,
+ placeholder : '',
+ unique : false,
+ cannotEdit : [],
+ onChange : ()=>{}
+ };
+ },
+
+ getInitialState : function() {
+ return {
+ valueContext : !!this.props.values ? this.props.values.map((value)=>({
+ value,
+ editing : false
+ })) : [],
+ temporaryValue : '',
+ updateValue : ''
+ };
+ },
+
+ componentDidUpdate : function(prevProps) {
+ if(!_.eq(this.props.values, prevProps.values)) {
+ this.setState({
+ valueContext : this.props.values ? this.props.values.map((newValue)=>({
+ value : newValue,
+ editing : this.state.valueContext.find(({ value })=>value === newValue)?.editing || false
+ })) : []
+ });
+ }
+ },
+
+ handleChange : function(value) {
+ this.props.onChange({
+ target : {
+ value
+ }
+ });
+ },
+
+ addValue : function(value){
+ this.handleChange(_.uniq([...this.props.values, value]));
+ this.setState({
+ temporaryValue : ''
+ });
+ },
+
+ removeValue : function(index){
+ this.handleChange(this.props.values.filter((_, i)=>i !== index));
+ },
+
+ updateValue : function(value, index){
+ const valueContext = this.state.valueContext;
+ valueContext[index].value = value;
+ valueContext[index].editing = false;
+ this.handleChange(valueContext.map((context)=>context.value));
+ this.setState({ valueContext, updateValue: '' });
+ },
+
+ editValue : function(index){
+ if(!!this.props.cannotEdit && this.props.cannotEdit.includes(this.props.values[index])) {
+ return;
+ }
+ const valueContext = this.state.valueContext.map((context, i)=>{
+ context.editing = index === i;
+ return context;
+ });
+ this.setState({ valueContext, updateValue: this.props.values[index] });
+ },
+
+ valueIsValid : function(value, index) {
+ const values = _.clone(this.props.values);
+ if(index !== undefined) {
+ values.splice(index, 1);
+ }
+ const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
+ const uniqueIfSet = !this.props.unique || !values.includes(value);
+ return matchesPatterns && uniqueIfSet;
+ },
+
+ handleValueInputKeyDown : function(event, index) {
+ if(event.key === 'Enter') {
+ if(this.valueIsValid(event.target.value, index)) {
+ if(index !== undefined) {
+ this.updateValue(event.target.value, index);
+ } else {
+ this.addValue(event.target.value);
+ }
+ }
+ } else if(event.key === 'Escape') {
+ this.closeEditInput(index);
+ }
+ },
+
+ closeEditInput : function(index) {
+ const valueContext = this.state.valueContext;
+ valueContext[index].editing = false;
+ this.setState({ valueContext, updateValue: '' });
+ },
+
+ render : function() {
+ const valueElements = Object.values(this.state.valueContext).map((context, i)=>context.editing
+ ?
+
+
this.handleValueInputKeyDown(e, i)}
+ onChange={(e)=>this.setState({ updateValue: e.target.value })}/>
+ {
{ e.stopPropagation(); this.closeEditInput(i); }}>
}
+ {this.valueIsValid(this.state.updateValue, i) ?
{ e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}>
: null}
+
+
+ : this.editValue(i)}>{context.value}
+ {!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null :
{ e.stopPropagation(); this.removeValue(i); }}>
}
+
+ );
+
+ return
+
+
+ {valueElements}
+
+
this.handleValueInputKeyDown(e)}
+ onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
+ {this.valueIsValid(this.state.temporaryValue) ?
{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}>
: null}
+
+
+
;
+ }
+});
+
+module.exports = StringArrayEditor;
\ No newline at end of file
diff --git a/client/homebrew/navbar/navbar.less b/client/homebrew/navbar/navbar.less
index 95cc04c7a..39fbfaf5c 100644
--- a/client/homebrew/navbar/navbar.less
+++ b/client/homebrew/navbar/navbar.less
@@ -91,7 +91,7 @@
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
}
.item{
- #backgroundColors;
+ #backgroundColorsHover;
.animate(background-color);
position : relative;
display : block;
diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx
index f8ac70a5d..d28968df7 100644
--- a/client/homebrew/pages/editPage/editPage.jsx
+++ b/client/homebrew/pages/editPage/editPage.jsx
@@ -114,7 +114,7 @@ const EditPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, { text: text }),
+ brew : { ...prevState.brew, text: text },
isPending : true,
htmlErrors : htmlErrors
}), ()=>this.trySave());
@@ -122,14 +122,17 @@ const EditPage = createClass({
handleStyleChange : function(style){
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, { style: style }),
+ brew : { ...prevState.brew, style: style },
isPending : true
}), ()=>this.trySave());
},
handleMetaChange : function(metadata){
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, metadata),
+ brew : {
+ ...prevState.brew,
+ ...metadata
+ },
isPending : true,
}), ()=>this.trySave());
@@ -213,11 +216,11 @@ const EditPage = createClass({
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, {
+ brew : { ...prevState.brew,
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
editId : this.savedBrew.editId,
shareId : this.savedBrew.shareId
- }),
+ },
isPending : false,
isSaving : false,
}));
diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx
index 6947eb031..c70894093 100644
--- a/client/homebrew/pages/homePage/homePage.jsx
+++ b/client/homebrew/pages/homePage/homePage.jsx
@@ -52,7 +52,7 @@ const HomePage = createClass({
},
handleTextChange : function(text){
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, { text: text })
+ brew : { ...prevState.brew, text: text }
}));
},
renderNavbar : function(){
diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx
index ae57039b5..c47e38dc2 100644
--- a/client/homebrew/pages/newPage/newPage.jsx
+++ b/client/homebrew/pages/newPage/newPage.jsx
@@ -112,7 +112,7 @@ const NewPage = createClass({
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, { text: text }),
+ brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors
}));
localStorage.setItem(BREWKEY, text);
@@ -120,14 +120,14 @@ const NewPage = createClass({
handleStyleChange : function(style){
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, { style: style }),
+ brew : { ...prevState.brew, style: style },
}));
localStorage.setItem(STYLEKEY, style);
},
handleMetaChange : function(metadata){
this.setState((prevState)=>({
- brew : _.merge({}, prevState.brew, metadata),
+ brew : { ...prevState.brew, ...metadata },
}));
localStorage.setItem(METAKEY, JSON.stringify({
// 'title' : this.state.brew.title,
diff --git a/server/app.js b/server/app.js
index 95a861375..45699eb14 100644
--- a/server/app.js
+++ b/server/app.js
@@ -176,7 +176,8 @@ app.get('/user/:username', async (req, res, next)=>{
'editId',
'createdAt',
'updatedAt',
- 'lastViewed'
+ 'lastViewed',
+ 'tags'
];
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
diff --git a/server/googleActions.js b/server/googleActions.js
index 5d9e43b71..7b86a34c1 100644
--- a/server/googleActions.js
+++ b/server/googleActions.js
@@ -124,7 +124,6 @@ const GoogleActions = {
title : file.properties.title,
description : file.description,
views : parseInt(file.properties.views),
- tags : '',
published : file.properties.published ? file.properties.published == 'true' : false,
systems : [],
thumbnail : file.properties.thumbnail
diff --git a/server/homebrew.api.js b/server/homebrew.api.js
index 4af232b4c..924e728db 100644
--- a/server/homebrew.api.js
+++ b/server/homebrew.api.js
@@ -56,6 +56,9 @@ const getBrew = (accessType)=>{
throw 'Brew not found in Homebrewery database or Google Drive';
}
+ if(typeof stub.tags === 'string') {
+ stub.tags = [];
+ }
req.brew = stub;
next();
diff --git a/server/homebrew.model.js b/server/homebrew.model.js
index db0669e42..a514e3fd8 100644
--- a/server/homebrew.model.js
+++ b/server/homebrew.model.js
@@ -13,7 +13,7 @@ const HomebrewSchema = mongoose.Schema({
pageCount : { type: Number, default: 1 },
description : { type: String, default: '' },
- tags : { type: String, default: '' },
+ tags : [String],
systems : [String],
renderer : { type: String, default: '' },
authors : [String],
diff --git a/shared/naturalcrit/nav/nav.less b/shared/naturalcrit/nav/nav.less
index f84d11733..43eaa0819 100644
--- a/shared/naturalcrit/nav/nav.less
+++ b/shared/naturalcrit/nav/nav.less
@@ -50,7 +50,7 @@ nav{
}
}
.navItem{
- #backgroundColors;
+ #backgroundColorsHover;
.animate(background-color);
padding : 8px 12px;
cursor : pointer;
diff --git a/shared/naturalcrit/styles/colors.less b/shared/naturalcrit/styles/colors.less
index 340fb38a7..30a7610a2 100644
--- a/shared/naturalcrit/styles/colors.less
+++ b/shared/naturalcrit/styles/colors.less
@@ -23,6 +23,29 @@
@grey : #7F8C8D;
#backgroundColors {
+ &.tealLight{ background-color : @tealLight };
+ &.teal{ background-color : @teal };
+ &.greenLight{ background-color : @greenLight };
+ &.green{ background-color : @green };
+ &.blueLight{ background-color : @blueLight };
+ &.blue{ background-color : @blue };
+ &.purpleLight{ background-color : @purpleLight };
+ &.purple{ background-color : @purple };
+ &.steelLight{ background-color : @steelLight };
+ &.steel{ background-color : @steel };
+ &.yellowLight{ background-color : @yellowLight };
+ &.yellow{ background-color : @yellow };
+ &.orangeLight{ background-color : @orangeLight };
+ &.orange{ background-color : @orange };
+ &.redLight{ background-color : @redLight };
+ &.red{ background-color : @red };
+ &.silverLight{ background-color : @silverLight };
+ &.silver{ background-color : @silver };
+ &.greyLight{ background-color : @greyLight };
+ &.grey{ background-color : @grey };
+}
+
+#backgroundColorsHover {
&.tealLight:hover{ background-color : @tealLight };
&.teal:hover{ background-color : @teal };
&.greenLight:hover{ background-color : @greenLight };