mirror of
https://github.com/naturalcrit/homebrewery.git
synced 2025-12-30 19:42:43 +00:00
@@ -1,9 +1,11 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
require('./metadataEditor.less');
|
require('./metadataEditor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
const request = require('superagent');
|
const request = require('superagent');
|
||||||
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
const SYSTEMS = ['5e', '4e', '3.5e', 'Pathfinder'];
|
||||||
|
|
||||||
@@ -17,7 +19,7 @@ const MetadataEditor = createClass({
|
|||||||
editId : null,
|
editId : null,
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
tags : '',
|
tags : [],
|
||||||
published : false,
|
published : false,
|
||||||
authors : [],
|
authors : [],
|
||||||
systems : [],
|
systems : [],
|
||||||
@@ -45,9 +47,10 @@ const MetadataEditor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleFieldChange : function(name, e){
|
handleFieldChange : function(name, e){
|
||||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
this.props.onChange({
|
||||||
|
...this.props.metadata,
|
||||||
[name] : e.target.value
|
[name] : e.target.value
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
handleSystem : function(system, e){
|
handleSystem : function(system, e){
|
||||||
if(e.target.checked){
|
if(e.target.checked){
|
||||||
@@ -64,9 +67,10 @@ const MetadataEditor = createClass({
|
|||||||
this.props.onChange(this.props.metadata);
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
handlePublish : function(val){
|
handlePublish : function(val){
|
||||||
this.props.onChange(_.merge({}, this.props.metadata, {
|
this.props.onChange({
|
||||||
|
...this.props.metadata,
|
||||||
published : val
|
published : val
|
||||||
}));
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDelete : function(){
|
handleDelete : function(){
|
||||||
@@ -193,13 +197,11 @@ const MetadataEditor = createClass({
|
|||||||
</button>
|
</button>
|
||||||
{this.renderThumbnail()}
|
{this.renderThumbnail()}
|
||||||
</div>
|
</div>
|
||||||
{/*}
|
|
||||||
<div className='field tags'>
|
<StringArrayEditor label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
||||||
<label>tags</label>
|
placeholder='add tag' unique={true}
|
||||||
<textarea value={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)} />
|
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||||
</div>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderAuthors()}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
.metadataEditor{
|
.metadataEditor{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
@@ -108,4 +109,86 @@
|
|||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
line-height : 1.5em;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
142
client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx
Normal file
142
client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx
Normal file
@@ -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
|
||||||
|
? <React.Fragment key={i}>
|
||||||
|
<div className='input-group'>
|
||||||
|
<input type='text' className={`value ${this.valueIsValid(this.state.updateValue, i) ? '' : 'invalid'}`} autoFocus placeholder={this.props.placeholder}
|
||||||
|
value={this.state.updateValue}
|
||||||
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e, i)}
|
||||||
|
onChange={(e)=>this.setState({ updateValue: e.target.value })}/>
|
||||||
|
{<div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.closeEditInput(i); }}><i className='fa fa-undo fa-fw'/></div>}
|
||||||
|
{this.valueIsValid(this.state.updateValue, i) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
: <div className='badge' key={i} onClick={()=>this.editValue(i)}>{context.value}
|
||||||
|
{!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null : <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.removeValue(i); }}><i className='fa fa-times fa-fw'/></div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <div className='field values'>
|
||||||
|
<label>{this.props.label}</label>
|
||||||
|
<div className='list'>
|
||||||
|
{valueElements}
|
||||||
|
<div className='input-group'>
|
||||||
|
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||||
|
value={this.state.temporaryValue}
|
||||||
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||||
|
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||||
|
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = StringArrayEditor;
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
&:nth-of-type(2){ background-color: darken(@purple, 30%); }
|
||||||
}
|
}
|
||||||
.item{
|
.item{
|
||||||
#backgroundColors;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
position : relative;
|
position : relative;
|
||||||
display : block;
|
display : block;
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ const EditPage = createClass({
|
|||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { text: text }),
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
isPending : true,
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors
|
||||||
}), ()=>this.trySave());
|
}), ()=>this.trySave());
|
||||||
@@ -122,14 +122,17 @@ const EditPage = createClass({
|
|||||||
|
|
||||||
handleStyleChange : function(style){
|
handleStyleChange : function(style){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { style: style }),
|
brew : { ...prevState.brew, style: style },
|
||||||
isPending : true
|
isPending : true
|
||||||
}), ()=>this.trySave());
|
}), ()=>this.trySave());
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata){
|
handleMetaChange : function(metadata){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, metadata),
|
brew : {
|
||||||
|
...prevState.brew,
|
||||||
|
...metadata
|
||||||
|
},
|
||||||
isPending : true,
|
isPending : true,
|
||||||
}), ()=>this.trySave());
|
}), ()=>this.trySave());
|
||||||
|
|
||||||
@@ -213,11 +216,11 @@ const EditPage = createClass({
|
|||||||
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, {
|
brew : { ...prevState.brew,
|
||||||
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
googleId : this.savedBrew.googleId ? this.savedBrew.googleId : null,
|
||||||
editId : this.savedBrew.editId,
|
editId : this.savedBrew.editId,
|
||||||
shareId : this.savedBrew.shareId
|
shareId : this.savedBrew.shareId
|
||||||
}),
|
},
|
||||||
isPending : false,
|
isPending : false,
|
||||||
isSaving : false,
|
isSaving : false,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ const HomePage = createClass({
|
|||||||
},
|
},
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { text: text })
|
brew : { ...prevState.brew, text: text }
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ const NewPage = createClass({
|
|||||||
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { text: text }),
|
brew : { ...prevState.brew, text: text },
|
||||||
htmlErrors : htmlErrors
|
htmlErrors : htmlErrors
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(BREWKEY, text);
|
localStorage.setItem(BREWKEY, text);
|
||||||
@@ -120,14 +120,14 @@ const NewPage = createClass({
|
|||||||
|
|
||||||
handleStyleChange : function(style){
|
handleStyleChange : function(style){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, { style: style }),
|
brew : { ...prevState.brew, style: style },
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(STYLEKEY, style);
|
localStorage.setItem(STYLEKEY, style);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata){
|
handleMetaChange : function(metadata){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : _.merge({}, prevState.brew, metadata),
|
brew : { ...prevState.brew, ...metadata },
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({
|
localStorage.setItem(METAKEY, JSON.stringify({
|
||||||
// 'title' : this.state.brew.title,
|
// 'title' : this.state.brew.title,
|
||||||
|
|||||||
@@ -176,7 +176,8 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
'editId',
|
'editId',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'lastViewed'
|
'lastViewed',
|
||||||
|
'tags'
|
||||||
];
|
];
|
||||||
|
|
||||||
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
|
let brews = await HomebrewModel.getByUser(req.params.username, ownAccount, fields)
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ const GoogleActions = {
|
|||||||
title : file.properties.title,
|
title : file.properties.title,
|
||||||
description : file.description,
|
description : file.description,
|
||||||
views : parseInt(file.properties.views),
|
views : parseInt(file.properties.views),
|
||||||
tags : '',
|
|
||||||
published : file.properties.published ? file.properties.published == 'true' : false,
|
published : file.properties.published ? file.properties.published == 'true' : false,
|
||||||
systems : [],
|
systems : [],
|
||||||
thumbnail : file.properties.thumbnail
|
thumbnail : file.properties.thumbnail
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ const getBrew = (accessType)=>{
|
|||||||
throw 'Brew not found in Homebrewery database or Google Drive';
|
throw 'Brew not found in Homebrewery database or Google Drive';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(typeof stub.tags === 'string') {
|
||||||
|
stub.tags = [];
|
||||||
|
}
|
||||||
req.brew = stub;
|
req.brew = stub;
|
||||||
|
|
||||||
next();
|
next();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const HomebrewSchema = mongoose.Schema({
|
|||||||
pageCount : { type: Number, default: 1 },
|
pageCount : { type: Number, default: 1 },
|
||||||
|
|
||||||
description : { type: String, default: '' },
|
description : { type: String, default: '' },
|
||||||
tags : { type: String, default: '' },
|
tags : [String],
|
||||||
systems : [String],
|
systems : [String],
|
||||||
renderer : { type: String, default: '' },
|
renderer : { type: String, default: '' },
|
||||||
authors : [String],
|
authors : [String],
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ nav{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navItem{
|
.navItem{
|
||||||
#backgroundColors;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
padding : 8px 12px;
|
padding : 8px 12px;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
|||||||
@@ -23,6 +23,29 @@
|
|||||||
@grey : #7F8C8D;
|
@grey : #7F8C8D;
|
||||||
|
|
||||||
#backgroundColors {
|
#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 };
|
&.tealLight:hover{ background-color : @tealLight };
|
||||||
&.teal:hover{ background-color : @teal };
|
&.teal:hover{ background-color : @teal };
|
||||||
&.greenLight:hover{ background-color : @greenLight };
|
&.greenLight:hover{ background-color : @greenLight };
|
||||||
|
|||||||
Reference in New Issue
Block a user