From e70a8c249596014c55e96ed58c17e400e4aa94f0 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 29 Oct 2017 23:22:46 -0700 Subject: [PATCH 1/5] color editing --- app/components/Input/Input.js | 4 +- app/components/Input/index.js | 3 +- .../Layout/components/SidebarCollections.js | 2 +- app/models/Collection.js | 15 +- app/models/Document.js | 2 +- app/scenes/CollectionEdit/CollectionEdit.js | 12 +- app/scenes/CollectionNew/CollectionNew.js | 9 +- .../components/ColorPicker/ColorPicker.js | 184 ++++++++++++++++++ frontend/components/ColorPicker/index.js | 3 + server/api/collections.js | 10 +- server/api/middlewares/validation.js | 7 + .../20171023064220-collection-color.js | 11 ++ server/models/Collection.js | 1 + server/models/Document.js | 2 +- server/presenters/collection.js | 1 + shared/styles/constants.js | 7 + shared/utils/color.js | 4 + shared/{ => utils}/parseTitle.js | 0 18 files changed, 261 insertions(+), 16 deletions(-) create mode 100644 frontend/components/ColorPicker/ColorPicker.js create mode 100644 frontend/components/ColorPicker/index.js create mode 100644 server/migrations/20171023064220-collection-color.js create mode 100644 shared/utils/color.js rename shared/{ => utils}/parseTitle.js (100%) diff --git a/app/components/Input/Input.js b/app/components/Input/Input.js index 5fbe34106..7897b3b3e 100644 --- a/app/components/Input/Input.js +++ b/app/components/Input/Input.js @@ -32,7 +32,7 @@ const Wrapper = styled.div` `; -const Outline = styled(Flex)` +export const Outline = styled(Flex)` display: flex; flex: 1; margin: 0 0 ${size.large}; @@ -48,7 +48,7 @@ const Outline = styled(Flex)` } `; -const LabelText = styled.div` +export const LabelText = styled.div` font-weight: 500; padding-bottom: 4px; `; diff --git a/app/components/Input/index.js b/app/components/Input/index.js index e005a8af8..e9b21f08b 100644 --- a/app/components/Input/index.js +++ b/app/components/Input/index.js @@ -1,3 +1,4 @@ // @flow -import Input from './Input'; +import Input, { LabelText, Outline } from './Input'; export default Input; +export { LabelText, Outline }; diff --git a/app/components/Layout/components/SidebarCollections.js b/app/components/Layout/components/SidebarCollections.js index 45e9e304e..9f257e2e7 100644 --- a/app/components/Layout/components/SidebarCollections.js +++ b/app/components/Layout/components/SidebarCollections.js @@ -100,7 +100,7 @@ type Props = { } + icon={} > {collection.name} diff --git a/app/models/Collection.js b/app/models/Collection.js index 93923e9c0..77158d866 100644 --- a/app/models/Collection.js +++ b/app/models/Collection.js @@ -19,6 +19,7 @@ class Collection extends BaseModel { description: ?string; id: string; name: string; + color: string; type: 'atlas' | 'journal'; documents: Array; updatedAt: string; @@ -57,19 +58,21 @@ class Collection extends BaseModel { if (this.isSaving) return this; this.isSaving = true; + const params = { + name: this.name, + color: this.color, + description: this.description, + }; + try { let res; if (this.id) { res = await client.post('/collections.update', { id: this.id, - name: this.name, - description: this.description, + ...params, }); } else { - res = await client.post('/collections.create', { - name: this.name, - description: this.description, - }); + res = await client.post('/collections.create', params); } runInAction('Collection#save', () => { invariant(res && res.data, 'Data should be available'); diff --git a/app/models/Document.js b/app/models/Document.js index 4f4c760ef..d882c039d 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -5,7 +5,7 @@ import invariant from 'invariant'; import { client } from 'utils/ApiClient'; import stores from 'stores'; import ErrorsStore from 'stores/ErrorsStore'; -import parseTitle from '../../shared/parseTitle'; +import parseTitle from '../../shared/utils/parseTitle.js'; import type { User } from 'types'; import BaseModel from './BaseModel'; diff --git a/app/scenes/CollectionEdit/CollectionEdit.js b/app/scenes/CollectionEdit/CollectionEdit.js index 17fc570b8..db5b07117 100644 --- a/app/scenes/CollectionEdit/CollectionEdit.js +++ b/app/scenes/CollectionEdit/CollectionEdit.js @@ -7,6 +7,7 @@ import Button from 'components/Button'; import Input from 'components/Input'; import Flex from 'shared/components/Flex'; import HelpText from 'components/HelpText'; +import ColorPicker from 'components/ColorPicker'; import Collection from 'models/Collection'; type Props = { @@ -18,6 +19,7 @@ type Props = { @observer class CollectionEdit extends Component { props: Props; @observable name: string; + @observable color: string = ''; @observable isSaving: boolean; componentWillMount() { @@ -28,7 +30,7 @@ type Props = { ev.preventDefault(); this.isSaving = true; - this.props.collection.updateData({ name: this.name }); + this.props.collection.updateData({ name: this.name, color: this.color }); const success = await this.props.collection.save(); if (success) { @@ -42,6 +44,10 @@ type Props = { this.name = ev.target.value; }; + handleColor = (color: string) => { + this.color = color; + }; + render() { return ( @@ -58,6 +64,10 @@ type Props = { required autoFocus /> + diff --git a/frontend/components/ColorPicker/ColorPicker.js b/frontend/components/ColorPicker/ColorPicker.js new file mode 100644 index 000000000..58df07be6 --- /dev/null +++ b/frontend/components/ColorPicker/ColorPicker.js @@ -0,0 +1,184 @@ +// @flow +import React from 'react'; +import { observable, computed, action } from 'mobx'; +import { observer } from 'mobx-react'; +import styled from 'styled-components'; +import Flex from 'components/Flex'; +import { LabelText, Outline } from 'components/Input'; +import { color, fonts, fontWeight } from 'styles/constants'; +import { validateColorHex } from '../../../shared/utils/color'; + +const colors = [ + '#4E5C6E', + '#19B7FF', + '#7F6BFF', + '#FC7419', + '#FC2D2D', + '#FFE100', + '#14CF9F', + '#EE84F0', + '#2F362F', +]; + +type Props = { + onSelect: string => void, + value?: string, +}; + +@observer class ColorPicker extends React.Component { + props: Props; + + @observable selectedColor: string = colors[0]; + @observable customColorValue: string = ''; + @observable customColorSelected: boolean; + + componentWillMount() { + const { value } = this.props; + if (value && colors.includes(value)) { + this.selectedColor = value; + } else if (value) { + this.customColorSelected = true; + this.customColorValue = value.replace('#', ''); + } + } + + componentDidMount() { + this.fireCallback(); + } + + fireCallback = () => { + this.props.onSelect( + this.customColorSelected ? this.customColor : this.selectedColor + ); + }; + + @computed get customColor(): string { + return this.customColorValue && + validateColorHex(`#${this.customColorValue}`) + ? `#${this.customColorValue}` + : colors[0]; + } + + @action setColor = (color: string) => { + this.selectedColor = color; + this.customColorSelected = false; + this.fireCallback(); + }; + + @action focusOnCustomColor = (event: SyntheticEvent) => { + this.selectedColor = ''; + this.customColorSelected = true; + this.fireCallback(); + }; + + @action setCustomColor = (event: SyntheticEvent) => { + let target = event.target; + if (target instanceof HTMLInputElement) { + const color = target.value; + this.customColorValue = color.replace('#', ''); + this.fireCallback(); + } + }; + + render() { + return ( + + Color + + + {colors.map(color => ( + this.setColor(color)} + /> + ))} + + + Custom color: + # + + + + + + ); + } +} + +type SwatchProps = { + onClick?: Function, + color?: string, + active?: boolean, +}; + +const Swatch = ({ onClick, ...props }: SwatchProps) => ( + + + +); + +const SwatchOutset = styled(Flex)` +width: 24px; +height: 24px; +margin-right: 5px; +border: 2px solid ${({ active, color }) => (active ? color : 'transparent')}; +border-radius: 2px; +background: ${({ color }) => color}; +${({ onClick }) => onClick && `cursor: pointer;`} + +&:last-child { + margin-right: 0; +} +`; + +const SwatchInset = styled(Flex)` + width: 20px; + height: 20px; + border: 1px solid ${({ active, color }) => (active ? 'white' : 'transparent')}; + border-radius: 2px; + background: ${({ color }) => color}; +`; + +const StyledOutline = styled(Outline)` + padding: 5px; +`; + +const HexHash = styled.div` + margin-left: 12px; + padding-bottom: 0; + font-weight: ${fontWeight.medium}; + user-select: none; +`; + +const CustomColorInput = styled.input` + border: 0; + flex: 1; + width: 65px; + margin-right: 12px; + padding-bottom: 0; + outline: none; + background: none; + font-family: ${fonts.monospace}; + font-weight: ${fontWeight.medium}; + + &::placeholder { + color: ${color.slate}; + font-family: ${fonts.monospace}; + font-weight: ${fontWeight.medium}; + } +`; + +export default ColorPicker; diff --git a/frontend/components/ColorPicker/index.js b/frontend/components/ColorPicker/index.js new file mode 100644 index 000000000..84f7ebb8d --- /dev/null +++ b/frontend/components/ColorPicker/index.js @@ -0,0 +1,3 @@ +// @flow +import ColorPicker from './ColorPicker'; +export default ColorPicker; diff --git a/server/api/collections.js b/server/api/collections.js index 2e76179b4..e7964c105 100644 --- a/server/api/collections.js +++ b/server/api/collections.js @@ -11,14 +11,17 @@ import { Collection } from '../models'; const router = new Router(); router.post('collections.create', auth(), async ctx => { - const { name, description, type } = ctx.body; + const { name, color, description, type } = ctx.body; ctx.assertPresent(name, 'name is required'); + if (color) + ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)'); const user = ctx.state.user; const collection = await Collection.create({ name, description, + color, type: type || 'atlas', teamId: user.teamId, creatorId: user.id, @@ -30,11 +33,14 @@ router.post('collections.create', auth(), async ctx => { }); router.post('collections.update', auth(), async ctx => { - const { id, name } = ctx.body; + const { id, name, color } = ctx.body; ctx.assertPresent(name, 'name is required'); + if (color) + ctx.assertHexColor(color, 'Invalid hex value (please use format #FFFFFF)'); const collection = await Collection.findById(id); collection.name = name; + collection.color = color; await collection.save(); ctx.body = { diff --git a/server/api/middlewares/validation.js b/server/api/middlewares/validation.js index 874cbecf7..d7ef52bc3 100644 --- a/server/api/middlewares/validation.js +++ b/server/api/middlewares/validation.js @@ -1,6 +1,7 @@ // @flow import apiError from '../../errors'; import validator from 'validator'; +import { validateColorHex } from '../../../shared/utils/color'; export default function validation() { return function validationMiddleware(ctx: Object, next: Function) { @@ -28,6 +29,12 @@ export default function validation() { } }; + ctx.assertHexColor = (value, message) => { + if (!validateColorHex(value)) { + throw apiError(400, 'validation_error', message); + } + }; + return next(); }; } diff --git a/server/migrations/20171023064220-collection-color.js b/server/migrations/20171023064220-collection-color.js new file mode 100644 index 000000000..b67a8b515 --- /dev/null +++ b/server/migrations/20171023064220-collection-color.js @@ -0,0 +1,11 @@ +module.exports = { + up: function(queryInterface, Sequelize) { + queryInterface.addColumn('collections', 'color', { + type: Sequelize.TEXT, + }); + }, + + down: function(queryInterface, Sequelize) { + queryInterface.removeColumn('collections', 'color'); + }, +}; diff --git a/server/models/Collection.js b/server/models/Collection.js index a05f02894..8cfd45ee3 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -22,6 +22,7 @@ const Collection = sequelize.define( urlId: { type: DataTypes.STRING, unique: true }, name: DataTypes.STRING, description: DataTypes.STRING, + color: DataTypes.STRING, type: { type: DataTypes.STRING, validate: { isIn: allowedCollectionTypes }, diff --git a/server/models/Document.js b/server/models/Document.js index 95e3b1465..61028013b 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -5,7 +5,7 @@ import randomstring from 'randomstring'; import isUUID from 'validator/lib/isUUID'; import { DataTypes, sequelize } from '../sequelize'; -import parseTitle from '../../shared/parseTitle'; +import parseTitle from '../../shared/utils/parseTitle.js'; import Revision from './Revision'; const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/; diff --git a/server/presenters/collection.js b/server/presenters/collection.js index aeae3dcef..d9f3b05e6 100644 --- a/server/presenters/collection.js +++ b/server/presenters/collection.js @@ -11,6 +11,7 @@ async function present(ctx: Object, collection: Collection) { url: collection.getUrl(), name: collection.name, description: collection.description, + color: collection.color || '#4E5C6E', type: collection.type, createdAt: collection.createdAt, updatedAt: collection.updatedAt, diff --git a/shared/styles/constants.js b/shared/styles/constants.js index 9bd4f6f43..d3c72bd39 100644 --- a/shared/styles/constants.js +++ b/shared/styles/constants.js @@ -35,6 +35,13 @@ export const fontWeight = { heavy: 800, }; +export const fonts = { + regular: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, + Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;`, + monospace: `'Atlas Typewriter', 'Source Code Pro', Menlo, Consolas, + 'Liberation Mono', monospace;`, +}; + export const color = { text: '#171B35', diff --git a/shared/utils/color.js b/shared/utils/color.js new file mode 100644 index 000000000..fd34e65df --- /dev/null +++ b/shared/utils/color.js @@ -0,0 +1,4 @@ +// @flow + +export const validateColorHex = (color: string) => + /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(color); diff --git a/shared/parseTitle.js b/shared/utils/parseTitle.js similarity index 100% rename from shared/parseTitle.js rename to shared/utils/parseTitle.js From fe9965093c1da1b8e2caeafee385105c532240a0 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 29 Oct 2017 23:27:06 -0700 Subject: [PATCH 2/5] Moved folders --- {frontend => app}/components/ColorPicker/ColorPicker.js | 4 ++-- {frontend => app}/components/ColorPicker/index.js | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename {frontend => app}/components/ColorPicker/ColorPicker.js (97%) rename {frontend => app}/components/ColorPicker/index.js (100%) diff --git a/frontend/components/ColorPicker/ColorPicker.js b/app/components/ColorPicker/ColorPicker.js similarity index 97% rename from frontend/components/ColorPicker/ColorPicker.js rename to app/components/ColorPicker/ColorPicker.js index 58df07be6..552201a77 100644 --- a/frontend/components/ColorPicker/ColorPicker.js +++ b/app/components/ColorPicker/ColorPicker.js @@ -3,9 +3,9 @@ import React from 'react'; import { observable, computed, action } from 'mobx'; import { observer } from 'mobx-react'; import styled from 'styled-components'; -import Flex from 'components/Flex'; +import Flex from 'shared/components/Flex'; import { LabelText, Outline } from 'components/Input'; -import { color, fonts, fontWeight } from 'styles/constants'; +import { color, fonts, fontWeight } from 'shared/styles/constants'; import { validateColorHex } from '../../../shared/utils/color'; const colors = [ diff --git a/frontend/components/ColorPicker/index.js b/app/components/ColorPicker/index.js similarity index 100% rename from frontend/components/ColorPicker/index.js rename to app/components/ColorPicker/index.js From 148cf8ad339a01343e4918fb612b3df193c79397 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Sun, 29 Oct 2017 23:28:45 -0700 Subject: [PATCH 3/5] lint --- app/components/ColorPicker/ColorPicker.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/components/ColorPicker/ColorPicker.js b/app/components/ColorPicker/ColorPicker.js index 552201a77..f82ee599f 100644 --- a/app/components/ColorPicker/ColorPicker.js +++ b/app/components/ColorPicker/ColorPicker.js @@ -131,17 +131,17 @@ const Swatch = ({ onClick, ...props }: SwatchProps) => ( ); const SwatchOutset = styled(Flex)` -width: 24px; -height: 24px; -margin-right: 5px; -border: 2px solid ${({ active, color }) => (active ? color : 'transparent')}; -border-radius: 2px; -background: ${({ color }) => color}; -${({ onClick }) => onClick && `cursor: pointer;`} + width: 24px; + height: 24px; + margin-right: 5px; + border: 2px solid ${({ active, color }) => (active ? color : 'transparent')}; + border-radius: 2px; + background: ${({ color }) => color}; + ${({ onClick }) => onClick && `cursor: pointer;`} -&:last-child { - margin-right: 0; -} + &:last-child { + margin-right: 0; + } `; const SwatchInset = styled(Flex)` From 980ad792c54316433f887be9f425da9b3dac9ca3 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 31 Oct 2017 21:55:46 -0700 Subject: [PATCH 4/5] addressed comments --- app/components/ColorPicker/ColorPicker.js | 6 +++--- server/models/Document.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/components/ColorPicker/ColorPicker.js b/app/components/ColorPicker/ColorPicker.js index f82ee599f..f95d857ec 100644 --- a/app/components/ColorPicker/ColorPicker.js +++ b/app/components/ColorPicker/ColorPicker.js @@ -6,7 +6,7 @@ import styled from 'styled-components'; import Flex from 'shared/components/Flex'; import { LabelText, Outline } from 'components/Input'; import { color, fonts, fontWeight } from 'shared/styles/constants'; -import { validateColorHex } from '../../../shared/utils/color'; +import { validateColorHex } from 'shared/utils/color'; const colors = [ '#4E5C6E', @@ -21,7 +21,7 @@ const colors = [ ]; type Props = { - onSelect: string => void, + onSelect: (color: string) => void, value?: string, }; @@ -119,7 +119,7 @@ type Props = { } type SwatchProps = { - onClick?: Function, + onClick?: () => void, color?: string, active?: boolean, }; diff --git a/server/models/Document.js b/server/models/Document.js index 61028013b..3bbf8c9ed 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -5,7 +5,7 @@ import randomstring from 'randomstring'; import isUUID from 'validator/lib/isUUID'; import { DataTypes, sequelize } from '../sequelize'; -import parseTitle from '../../shared/utils/parseTitle.js'; +import parseTitle from '../../shared/utils/parseTitle'; import Revision from './Revision'; const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/; From c991b85c8dc73750aa385169cd904ec782962d98 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Tue, 31 Oct 2017 21:59:14 -0700 Subject: [PATCH 5/5] removed additional .js --- app/models/Document.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/Document.js b/app/models/Document.js index d882c039d..5df71a8f8 100644 --- a/app/models/Document.js +++ b/app/models/Document.js @@ -5,7 +5,7 @@ import invariant from 'invariant'; import { client } from 'utils/ApiClient'; import stores from 'stores'; import ErrorsStore from 'stores/ErrorsStore'; -import parseTitle from '../../shared/utils/parseTitle.js'; +import parseTitle from '../../shared/utils/parseTitle'; import type { User } from 'types'; import BaseModel from './BaseModel';