Add preset feature (#595)

* Add preset feature without create

* fix lint errors

* Add presets to Editor state

* add remove, update -> apply, omit presets

* replace name with index, add undo functionality

* fix reduce function

* Tweaks:

- Make remove filter setState atomic
- Remove broken sCU in BackgroundSelect
- Touch up style of arrow functions a little
- Remove titleBar from default settings
- Don't expose SETTINGS_KEYS
- Use hasOwnProperty instead of includes()

* refactor preset state into Settings

* move format code into editor and make it work again

* omit custom in applyPreset

* move presets array state into Settings

* keep custom sCU in BackgroundSelect

* pull out inline objects

* revert pages/index

* increase Presets font-size, remove margin-top

* Add ability to create presets

* also enable passing exportSize as prop

* move selectedPreset back into Settings (my bad Sean)

* replace splice with filter, getSavedX -> getX

* Revert "move selectedPreset back into Settings (my bad Sean)"

This reverts commit ae5da4700ea36ad7c31e697e83a2724be4b448f4.

* make sure background updates remove selected preset

* selectedPreset -> preset

* use onChange instead of selectPreset

* use preset id's instead of indexes

* bug fixes

* use disabled instead of pointer-events

* make .settings-presets-applied flex 💪

* make .settings-presets-arrow flex 💪

* move getPresets outside of `setState`

* move inline styles to style tag

* refactor using omitBy and isFunction

* remove lodash.isfunction

* fix applyPreset to disclude preset field

* move omit to getSettingsFromProps

* replace lodash.omit with omitBy solution

* .includes -> .indexOf

* add default preset and presetApplied state

* fix lint error

* remove presetApplied

* add more default presets

* fix default preset functionality

* tweaks

* preserve preset list scrollLeft b/w updates with a hack

* Use ref for preset content

* remove forwardRef
main
Sean 6 years ago committed by Michael Fix
parent bf761d7d5b
commit f6f0adee6b

@ -22,10 +22,11 @@ class BackgroundSelect extends React.Component {
} }
shouldComponentUpdate(prevProps, prevState) { shouldComponentUpdate(prevProps, prevState) {
return ( return [
prevState.isVisible !== this.state.isVisible || prevState.isVisible !== this.state.isVisible,
(prevState.isVisible && shallowCompare(this, prevProps, prevState)) prevProps.color !== this.props.color,
) prevState.isVisible && shallowCompare(this, prevProps, prevState)
].some(Boolean)
} }
toggle() { toggle() {

@ -7,7 +7,6 @@ import domtoimage from 'dom-to-image'
import ReadFileDropContainer, { DATA_URL, TEXT } from 'dropperx' import ReadFileDropContainer, { DATA_URL, TEXT } from 'dropperx'
import Spinner from 'react-spinner' import Spinner from 'react-spinner'
import shallowCompare from 'react-addons-shallow-compare' import shallowCompare from 'react-addons-shallow-compare'
import omit from 'lodash.omit'
// Ours // Ours
import Button from './Button' import Button from './Button'
@ -31,13 +30,19 @@ import {
EXPORT_SIZES_HASH, EXPORT_SIZES_HASH,
DEFAULT_CODE, DEFAULT_CODE,
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
DEFAULT_LANGUAGE DEFAULT_LANGUAGE,
DEFAULT_PRESET_ID
} from '../lib/constants' } from '../lib/constants'
import { serializeState, getQueryStringState } from '../lib/routing' import { serializeState, getQueryStringState } from '../lib/routing'
import { getState, escapeHtml, unescapeHtml, formatCode } from '../lib/util' import { getSettings, escapeHtml, unescapeHtml, formatCode, omit } from '../lib/util'
import LanguageIcon from './svg/Language' import LanguageIcon from './svg/Language'
import ThemeIcon from './svg/Theme' import ThemeIcon from './svg/Theme'
const themeIcon = <ThemeIcon />
const languageIcon = <LanguageIcon />
const tweetButtonStyle = { marginRight: '8px' }
class Editor extends React.Component { class Editor extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
@ -46,15 +51,13 @@ class Editor extends React.Component {
loading: true, loading: true,
uploading: false, uploading: false,
code: props.content, code: props.content,
online: true online: true,
preset: DEFAULT_PRESET_ID
} }
this.export = this.export.bind(this) this.export = this.export.bind(this)
this.upload = this.upload.bind(this) this.upload = this.upload.bind(this)
this.updateSetting = this.updateSetting.bind(this) this.updateSetting = this.updateSetting.bind(this)
this.updateCode = this.updateSetting.bind(this, 'code')
this.updateAspectRatio = this.updateSetting.bind(this, 'aspectRatio')
this.updateTitleBar = this.updateSetting.bind(this, 'titleBar')
this.updateTheme = this.updateTheme.bind(this) this.updateTheme = this.updateTheme.bind(this)
this.updateLanguage = this.updateLanguage.bind(this) this.updateLanguage = this.updateLanguage.bind(this)
this.updateBackground = this.updateBackground.bind(this) this.updateBackground = this.updateBackground.bind(this)
@ -62,8 +65,6 @@ class Editor extends React.Component {
this.getCarbonImage = this.getCarbonImage.bind(this) this.getCarbonImage = this.getCarbonImage.bind(this)
this.onDrop = this.onDrop.bind(this) this.onDrop = this.onDrop.bind(this)
this.setOffline = () => this.setState({ online: false })
this.setOnline = () => this.setState({ online: true })
this.innerRef = node => (this.carbonNode = node) this.innerRef = node => (this.carbonNode = node)
} }
@ -89,7 +90,7 @@ class Editor extends React.Component {
const newState = { const newState = {
// Load from localStorage // Load from localStorage
...getState(localStorage), ...getSettings(localStorage),
// and then URL params // and then URL params
...initialState, ...initialState,
loading: false, loading: false,
@ -119,7 +120,20 @@ class Editor extends React.Component {
} }
} }
async getCarbonImage({ format, type } = { format: 'png' }) { updateCode = code => this.setState({ code })
updateAspectRatio = aspectRatio => this.setState({ aspectRatio })
updateTitleBar = titleBar => this.setState({ titleBar })
setOffline = () => this.setState({ online: false })
setOnline = () => this.setState({ online: true })
async getCarbonImage(
{
format,
type,
squared = this.state.squaredImage,
exportSize = (EXPORT_SIZES_HASH[this.state.exportSize] || DEFAULT_EXPORT_SIZE).value
} = { format: 'png' }
) {
// if safari, get image from api // if safari, get image from api
const isPNG = format !== 'svg' const isPNG = format !== 'svg'
if ( if (
@ -134,8 +148,6 @@ class Editor extends React.Component {
const node = this.carbonNode const node = this.carbonNode
const exportSize = (EXPORT_SIZES_HASH[this.state.exportSize] || DEFAULT_EXPORT_SIZE).value
const map = new Map() const map = new Map()
const undoMap = value => { const undoMap = value => {
map.forEach((value, node) => (node.innerText = value)) map.forEach((value, node) => (node.innerText = value))
@ -152,15 +164,13 @@ class Editor extends React.Component {
} }
const width = node.offsetWidth * exportSize const width = node.offsetWidth * exportSize
const height = this.state.squaredImage const height = squared ? node.offsetWidth * exportSize : node.offsetHeight * exportSize
? node.offsetWidth * exportSize
: node.offsetHeight * exportSize
const config = { const config = {
style: { style: {
transform: `scale(${exportSize})`, transform: `scale(${exportSize})`,
'transform-origin': 'center', 'transform-origin': 'center',
background: this.state.squaredImage ? this.state.backgroundColor : 'none' background: squared ? this.state.backgroundColor : 'none'
}, },
filter: n => { filter: n => {
if (n.className) { if (n.className) {
@ -192,8 +202,6 @@ class Editor extends React.Component {
// Twitter needs regular dataurls // Twitter needs regular dataurls
return await domtoimage.toPng(node, config) return await domtoimage.toPng(node, config)
} catch (error) {
throw error
} finally { } finally {
undoMap() undoMap()
} }
@ -201,6 +209,9 @@ class Editor extends React.Component {
updateSetting(key, value) { updateSetting(key, value) {
this.setState({ [key]: value }) this.setState({ [key]: value })
if (Object.prototype.hasOwnProperty.call(DEFAULT_SETTINGS, key)) {
this.setState({ preset: null })
}
} }
export(format = 'png') { export(format = 'png') {
@ -220,7 +231,7 @@ class Editor extends React.Component {
} }
resetDefaultSettings() { resetDefaultSettings() {
this.setState(DEFAULT_SETTINGS) this.setState({ ...DEFAULT_SETTINGS, preset: DEFAULT_PRESET_ID })
this.props.onReset() this.props.onReset()
} }
@ -238,7 +249,8 @@ class Editor extends React.Component {
this.setState({ this.setState({
backgroundImage: file.content, backgroundImage: file.content,
backgroundImageSelection: null, backgroundImageSelection: null,
backgroundMode: 'image' backgroundMode: 'image',
preset: null
}) })
} else { } else {
this.setState({ code: file.content, language: 'auto' }) this.setState({ code: file.content, language: 'auto' })
@ -257,10 +269,11 @@ class Editor extends React.Component {
if (photographer) { if (photographer) {
this.setState(({ code = DEFAULT_CODE }) => ({ this.setState(({ code = DEFAULT_CODE }) => ({
...changes, ...changes,
code: code + `\n\n// Photo by ${photographer.name} on Unsplash` code: code + `\n\n// Photo by ${photographer.name} on Unsplash`,
preset: null
})) }))
} else { } else {
this.setState(changes) this.setState({ ...changes, preset: null })
} }
} }
@ -271,6 +284,8 @@ class Editor extends React.Component {
// create toast here in the future // create toast here in the future
}) })
applyPreset = ({ id: preset, ...settings }) => this.setState({ preset, ...settings })
render() { render() {
const { const {
loading, loading,
@ -302,20 +317,20 @@ class Editor extends React.Component {
) )
} }
const config = omit(this.state, ['code', 'aspectRatio']) const config = omit(this.state, ['code', 'aspectRatio', 'titleBar'])
return ( return (
<React.Fragment> <React.Fragment>
<div className="editor"> <div className="editor">
<Toolbar> <Toolbar>
<Dropdown <Dropdown
icon={<ThemeIcon />} icon={themeIcon}
selected={THEMES_HASH[theme] || DEFAULT_THEME} selected={THEMES_HASH[theme] || DEFAULT_THEME}
list={THEMES} list={THEMES}
onChange={this.updateTheme} onChange={this.updateTheme}
/> />
<Dropdown <Dropdown
icon={<LanguageIcon />} icon={languageIcon}
selected={ selected={
LANGUAGE_NAME_HASH[language] || LANGUAGE_NAME_HASH[language] ||
LANGUAGE_MIME_HASH[language] || LANGUAGE_MIME_HASH[language] ||
@ -337,6 +352,8 @@ class Editor extends React.Component {
onChange={this.updateSetting} onChange={this.updateSetting}
resetDefaultSettings={this.resetDefaultSettings} resetDefaultSettings={this.resetDefaultSettings}
format={this.format} format={this.format}
applyPreset={this.applyPreset}
getCarbonImage={this.getCarbonImage}
/> />
<div className="buttons"> <div className="buttons">
{this.props.api.tweet && {this.props.api.tweet &&
@ -346,7 +363,7 @@ class Editor extends React.Component {
onClick={this.upload} onClick={this.upload}
title={uploading ? 'Loading...' : 'Tweet'} title={uploading ? 'Loading...' : 'Tweet'}
color="#57b5f9" color="#57b5f9"
style={{ marginRight: '8px' }} style={tweetButtonStyle}
/> />
)} )}
<ExportMenu <ExportMenu

@ -1,16 +1,18 @@
import React from 'react' import React from 'react'
import enhanceWithClickOutside from 'react-click-outside' import enhanceWithClickOutside from 'react-click-outside'
import omitBy from 'lodash.omitby'
import ThemeSelect from './ThemeSelect' import ThemeSelect from './ThemeSelect'
import FontSelect from './FontSelect' import FontSelect from './FontSelect'
import Slider from './Slider' import Slider from './Slider'
import Toggle from './Toggle' import Toggle from './Toggle'
import WindowPointer from './WindowPointer' import WindowPointer from './WindowPointer'
import { COLORS } from '../lib/constants' import { COLORS, DEFAULT_PRESETS } from '../lib/constants'
import { getPresets, savePresets } from '../lib/util'
import { toggle } from '../lib/util' import { toggle } from '../lib/util'
import SettingsIcon from './svg/Settings' import SettingsIcon from './svg/Settings'
import * as Arrows from './svg/Arrows' import * as Arrows from './svg/Arrows'
//import Remove from './svg/Remove' import Remove from './svg/Remove'
const WindowSettings = React.memo( const WindowSettings = React.memo(
({ ({
@ -202,77 +204,51 @@ const MenuButton = React.memo(({ name, select, selected }) => {
) )
}) })
/*const Presets = React.memo(({ show, presets, toggle, create, remove }) => { const Preset = React.memo(({ remove, apply, selected, preset }) => (
return ( <div className="preset-container">
<div className="settings-presets"> <button
<div className="settings-presets-header"> className="preset-tile"
<span>Presets</span> onClick={() => apply(preset)}
{show && <button className="settings-presets-create" onClick={create}>create +</button>} disabled={preset.id === selected}
<button className="settings-presets-arrow" onClick={toggle}> />
{show ? <Arrows.Up /> : <Arrows.Down />} {preset.custom ? (
</button> <button className="preset-remove" onClick={() => remove(preset.id)}>
</div> <Remove />
{show && ( </button>
<div className="settings-presets-content"> ) : null}
{presets.map(({ id, backgroundColor, userCreated }) => ( <style jsx>
<div key={id} className="settings-presets-preset" style={{
backgroundColor
}}>
{
userCreated ? <button className="settings-presets-remove" onClick={() => remove(id)}><Remove /></button> : null
}
</div>
))}
</div>
)}
<style jsx>
{` {`
.settings-presets { button {
border-bottom: 1px solid ${COLORS.SECONDARY};
}
.settings-presets-header {
display: flex;
padding: 10px 8px;
position: relative;
color: ${COLORS.SECONDARY};
width: 100%;
align-items: center;
}
.settings-presets-arrow, .settings-presets-create, .settings-presets-remove {
cursor: pointer;
background: transparent;
outline: none; outline: none;
border: none; border: none;
font-size: 12px; background: transparent;
} cursor: pointer;
padding: 0;
.settings-presets-create {
color: ${COLORS.GRAY};
padding: 0 8px;
}
.settings-presets-arrow {
position: absolute;
right: 16px;
} }
.settings-presets-content { .preset-container {
display: flex; display: flex;
overflow-x: scroll; position: relative;
margin: 12px 8px; flex: 0 0 96px;
height: 96px;
margin-right: 8px;
} }
.settings-presets-preset { .preset-tile {
flex: 1;
border-radius: 3px; border-radius: 3px;
height: 96px; background-repeat: no-repeat;
margin-right: 8px; background-position: center center;
flex: 0 0 96px; background-size: contain;
position: relative; cursor: ${preset.id === selected ? 'initial' : 'pointer'};
background-image: ${`url('${preset.icon}')`};
background-color: ${preset.icon ? 'initial' : preset.backgroundColor};
box-shadow: ${preset.id === selected
? `inset 0px 0px 0px 2px ${COLORS.SECONDARY}`
: 'initial'};
} }
.settings-presets-remove { .preset-remove {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -286,16 +262,165 @@ const MenuButton = React.memo(({ name, select, selected }) => {
background-color: ${COLORS.SECONDARY}; background-color: ${COLORS.SECONDARY};
} }
`} `}
</style> </style>
</div> </div>
) ))
})*/
const Presets = React.memo(
({ show, create, toggle, undo, presets, selected, remove, apply, applied, contentRef }) => {
const customPresetsLength = presets.length - DEFAULT_PRESETS.length
const disabledCreate = selected != null
return (
<div className="settings-presets">
<div className="settings-presets-header">
<span>Presets</span>
{show && (
<button className="settings-presets-create" onClick={create} disabled={disabledCreate}>
create +
</button>
)}
<button className="settings-presets-arrow" onClick={toggle}>
{show ? <Arrows.Up /> : <Arrows.Down />}
</button>
</div>
{show ? (
<div className="settings-presets-content" ref={contentRef}>
{presets.filter(p => p.custom).map(preset => (
<Preset
key={preset.id}
remove={remove}
apply={apply}
preset={preset}
selected={selected}
/>
))}
{customPresetsLength > 0 ? <div className="settings-presets-divider" /> : null}
{presets.filter(p => !p.custom).map(preset => (
<Preset key={preset.id} apply={apply} preset={preset} selected={selected} />
))}
</div>
) : null}
{show && applied ? (
<div className="settings-presets-applied">
<span>Preset applied!</span>
<button onClick={undo}>
undo <span>&#x21A9;</span>
</button>
</div>
) : null}
<style jsx>
{`
button {
outline: none;
border: none;
background: transparent;
cursor: pointer;
padding: 0;
}
.settings-presets {
border-bottom: 1px solid ${COLORS.SECONDARY};
}
.settings-presets-header {
display: flex;
padding: 10px 8px;
position: relative;
color: ${COLORS.SECONDARY};
width: 100%;
align-items: center;
}
.settings-presets-header > span {
font-size: 14px;
}
.settings-presets-arrow,
.settings-presets-create,
.settings-presets-remove {
cursor: pointer;
background: transparent;
outline: none;
border: none;
font-size: 12px;
}
.settings-presets-create {
color: ${COLORS.GRAY};
padding: 0 8px;
cursor: ${disabledCreate ? 'not-allowed' : 'pointer'};
}
.settings-presets-create:enabled:hover {
color: ${COLORS.SECONDARY};
}
.settings-presets-arrow {
display: flex;
position: absolute;
right: 16px;
}
.settings-presets-content {
display: flex;
overflow-x: scroll;
margin: 0 8px 12px 8px;
align-items: center;
/* https://iamsteve.me/blog/entry/using-flexbox-for-horizontal-scrolling-navigation */
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
}
.settings-presets-divider {
height: 72px;
padding: 1px;
border-radius: 3px;
margin-right: 8px;
background-color: ${COLORS.DARK_GRAY};
}
.settings-presets-applied {
display: flex;
justify-content: space-between;
background-color: ${COLORS.SECONDARY};
width: 100%;
color: ${COLORS.BLACK};
padding: 4px 8px;
}
.settings-presets-applied button {
float: right;
}
.settings-presets-applied button span {
float: right;
margin: 1px 0 0 2px;
}
`}
</style>
</div>
)
}
)
class Settings extends React.PureComponent { class Settings extends React.PureComponent {
state = { state = {
presets: DEFAULT_PRESETS,
isVisible: false, isVisible: false,
selectedMenu: 'Window', selectedMenu: 'Window',
showPresets: false showPresets: false,
previousSettings: null
}
presetContentRef = React.createRef()
componentDidMount() {
const storedPresets = getPresets(localStorage) || []
this.setState(({ presets }) => ({
presets: [...storedPresets, ...presets]
}))
} }
toggleVisible = () => this.setState(toggle('isVisible')) toggleVisible = () => this.setState(toggle('isVisible'))
@ -311,7 +436,7 @@ class Settings extends React.PureComponent {
case 'Window': case 'Window':
return ( return (
<WindowSettings <WindowSettings
onChange={this.props.onChange} onChange={this.handleChange}
windowTheme={this.props.windowTheme} windowTheme={this.props.windowTheme}
paddingHorizontal={this.props.paddingHorizontal} paddingHorizontal={this.props.paddingHorizontal}
paddingVertical={this.props.paddingVertical} paddingVertical={this.props.paddingVertical}
@ -327,21 +452,90 @@ class Settings extends React.PureComponent {
case 'Type': case 'Type':
return ( return (
<TypeSettings <TypeSettings
onChange={this.props.onChange} onChange={this.handleChange}
font={this.props.fontFamily} font={this.props.fontFamily}
size={this.props.fontSize} size={this.props.fontSize}
lineHeight={this.props.lineHeight} lineHeight={this.props.lineHeight}
/> />
) )
case 'Misc': case 'Misc':
return <MiscSettings format={this.props.format} reset={this.props.resetDefaultSettings} /> return <MiscSettings format={this.props.format} reset={this.handleReset} />
default: default:
return null return null
} }
} }
handleChange = (key, value) => {
this.props.onChange(key, value)
this.setState({ previousSettings: null })
}
handleReset = () => {
this.props.resetDefaultSettings()
this.setState({ previousSettings: null })
}
getSettingsFromProps = () =>
omitBy(this.props, (v, k) => typeof v === 'function' || k === 'preset')
applyPreset = preset => {
const previousSettings = this.getSettingsFromProps()
this.props.applyPreset(preset)
// TODO: this is a hack to prevent the scrollLeft position from changing when preset is applied
const { scrollLeft: previousScrollLeft } = this.presetContentRef.current
this.setState({ previousSettings }, () => {
this.presetContentRef.current.scrollLeft = previousScrollLeft
})
}
undoPreset = () => {
this.props.applyPreset({ ...this.state.previousSettings, id: null })
this.setState({ previousSettings: null })
}
removePreset = id => {
if (this.props.preset === id) {
this.props.onChange('preset', null)
this.setState({ previousSettings: null })
}
this.setState(
({ presets }) => ({ presets: presets.filter(p => p.id !== id) }),
this.savePresets
)
}
createPreset = async () => {
const newPreset = this.getSettingsFromProps()
newPreset.id = `preset:${Math.random()
.toString(36)
.slice(2)}`
newPreset.custom = true
newPreset.icon = await this.props.getCarbonImage({
format: 'png',
squared: true,
exportSize: 1
})
this.props.onChange('preset', newPreset.id)
this.setState(
({ presets }) => ({
previousSettings: null,
presets: [newPreset, ...presets]
}),
this.savePresets
)
}
savePresets = () => savePresets(localStorage, this.state.presets.filter(p => p.custom))
render() { render() {
const { isVisible, selectedMenu } = this.state const { isVisible, selectedMenu, showPresets, presets, previousSettings } = this.state
const { preset } = this.props
return ( return (
<div className="settings-container"> <div className="settings-container">
@ -353,6 +547,18 @@ class Settings extends React.PureComponent {
</div> </div>
<div className="settings-settings"> <div className="settings-settings">
<WindowPointer fromLeft="15px" /> <WindowPointer fromLeft="15px" />
<Presets
show={showPresets}
presets={presets}
selected={preset}
toggle={this.togglePresets}
apply={this.applyPreset}
undo={this.undoPreset}
remove={this.removePreset}
create={this.createPreset}
applied={!!previousSettings}
contentRef={this.presetContentRef}
/>
<div className="settings-bottom"> <div className="settings-bottom">
<div className="settings-menu"> <div className="settings-menu">
<MenuButton name="Window" select={this.selectMenu} selected={selectedMenu} /> <MenuButton name="Window" select={this.selectMenu} selected={selectedMenu} />
@ -408,7 +614,7 @@ class Settings extends React.PureComponent {
top: 52px; top: 52px;
left: 0; left: 0;
border: 2px solid ${COLORS.SECONDARY}; border: 2px solid ${COLORS.SECONDARY};
width: 320px; width: 324px;
border-radius: 3px; border-radius: 3px;
background: ${COLORS.BLACK}; background: ${COLORS.BLACK};
} }

@ -40,7 +40,7 @@ export default ({ titleBar, theme, handleTitleBarChange, copyable, code }) => (
<div className="window-title-container"> <div className="window-title-container">
<input <input
aria-label="Image Title" aria-label="Image Title"
value={titleBar} value={titleBar || ''}
type="text" type="text"
spellCheck="false" spellCheck="false"
onChange={e => handleTitleBarChange(e.target.value)} onChange={e => handleTitleBarChange(e.target.value)}

@ -457,9 +457,9 @@ export const LANGUAGES = [
] ]
export const EXPORT_SIZES = [ export const EXPORT_SIZES = [
{ id: '1x', name: '1x', value: '1' }, { id: '1x', name: '1x', value: 1 },
{ id: '2x', name: '2x', value: '2' }, { id: '2x', name: '2x', value: 2 },
{ id: '4x', name: '4x', value: '4' } { id: '4x', name: '4x', value: 4 }
] ]
export const EXPORT_SIZES_HASH = toHash(EXPORT_SIZES) export const EXPORT_SIZES_HASH = toHash(EXPORT_SIZES)
@ -512,8 +512,8 @@ if (typeof window !== 'undefined' && typeof window.navigator !== 'undefined') {
} }
export const DEFAULT_SETTINGS = { export const DEFAULT_SETTINGS = {
paddingVertical: '48px', paddingVertical: '56px',
paddingHorizontal: '32px', paddingHorizontal: '56px',
marginVertical: '45px', marginVertical: '45px',
marginHorizontal: '45px', marginHorizontal: '45px',
backgroundImage: null, backgroundImage: null,
@ -533,7 +533,103 @@ export const DEFAULT_SETTINGS = {
widthAdjustment: true, widthAdjustment: true,
lineNumbers: false, lineNumbers: false,
exportSize: '2x', exportSize: '2x',
titleBar: '',
watermark: false, watermark: false,
squaredImage: false squaredImage: false
} }
export const DEFAULT_PRESET_ID = 'preset:4'
export const DEFAULT_PRESETS = [
{
...DEFAULT_SETTINGS,
icon: '/static/presets/4.png',
id: DEFAULT_PRESET_ID
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(74,144,226,1)',
dropShadow: false,
theme: 'material',
fontFamily: 'Fira Code',
lineHeight: '152%',
icon: '/static/presets/7.png',
id: 'preset:7'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(248,231,28,1)',
dropShadow: false,
theme: 'blackboard',
fontFamily: 'Fira Code',
lineHeight: '152%',
icon: '/static/presets/8.png',
id: 'preset:8'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(182,162,145,1)',
dropShadow: false,
theme: 'zenburn',
windowTheme: 'bw',
lineHeight: '152%',
icon: '/static/presets/9.png',
id: 'preset:9'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(121,72,185,1)',
dropShadow: false,
theme: 'verminal',
windowTheme: 'bw',
fontFamily: 'Fira Code',
fontSize: '14px',
lineHeight: '143%',
icon: '/static/presets/0.png',
id: 'preset:0'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(239,40,44,1)',
theme: 'one-light',
lineHeight: '143%',
icon: '/static/presets/1.png',
id: 'preset:1'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(31,129,109,1)',
dropShadow: false,
theme: 'night-owl',
lineHeight: '143%',
windowControls: false,
icon: '/static/presets/2.png',
id: 'preset:2'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(249,237,212,1)',
theme: 'twilight',
fontFamily: 'IBM Plex Mono',
lineHeight: '143%',
icon: '/static/presets/3.png',
id: 'preset:3'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(222,171,99,1)',
theme: 'duotone-dark',
icon: '/static/presets/5.png',
id: 'preset:5'
},
{
...DEFAULT_SETTINGS,
backgroundColor: 'rgba(187,187,187,1)',
dropShadowOffsetY: '3px',
dropShadowBlurRadius: '13px',
theme: 'solarized light',
windowTheme: 'sharp',
icon: '/static/presets/6.png',
id: 'preset:6'
}
]

@ -1,8 +1,11 @@
import morph from 'morphmorph' import morph from 'morphmorph'
import omitBy from 'lodash.omitby'
const KEY = 'CARBON_STATE' const SETTINGS_KEY = 'CARBON_SETTINGS'
const PRESETS_KEY = 'CARBON_PRESETS'
const assign = morph.assign(KEY) const assignSettings = morph.assign(SETTINGS_KEY)
const assignPresets = morph.assign(PRESETS_KEY)
const parse = v => { const parse = v => {
try { try {
@ -36,12 +39,22 @@ export const unescapeHtml = s => {
export const parseRGBA = obj => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})` export const parseRGBA = obj => `rgba(${obj.r},${obj.g},${obj.b},${obj.a})`
export const getState = morph.compose( export const getSettings = morph.compose(
parse, parse,
escapeHtml, escapeHtml,
morph.get(KEY) morph.get(SETTINGS_KEY)
) )
export const saveState = (window, v) => assign(window, JSON.stringify(v))
export const getPresets = morph.compose(
parse,
morph.get(PRESETS_KEY)
)
export const saveSettings = (window, v) => assignSettings(window, JSON.stringify(v))
export const savePresets = (window, v) => assignPresets(window, JSON.stringify(v))
export const clearSettings = () => localStorage.removeItem(SETTINGS_KEY)
export const capitalizeFirstLetter = s => s.charAt(0).toUpperCase() + s.slice(1) export const capitalizeFirstLetter = s => s.charAt(0).toUpperCase() + s.slice(1)
@ -65,3 +78,5 @@ export const formatCode = async code => {
singleQuote: true singleQuote: true
}) })
} }
export const omit = (object, keys) => omitBy(object, (_, k) => keys.indexOf(k) > -1)

@ -28,7 +28,7 @@
"graphql": "^14.0.2", "graphql": "^14.0.2",
"highlight.js": "^9.13.1", "highlight.js": "^9.13.1",
"lodash.debounce": "^4.0.8", "lodash.debounce": "^4.0.8",
"lodash.omit": "^4.5.0", "lodash.omitby": "^4.6.0",
"match-sorter": "^2.3.0", "match-sorter": "^2.3.0",
"morphmorph": "^0.1.0", "morphmorph": "^0.1.0",
"ms": "^2.0.0", "ms": "^2.0.0",

@ -1,19 +1,18 @@
// Theirs // Theirs
import React from 'react' import React from 'react'
import { withRouter } from 'next/router' import { withRouter } from 'next/router'
import omit from 'lodash.omit'
// Ours // Ours
import Editor from '../components/Editor' import Editor from '../components/Editor'
import Page from '../components/Page' import Page from '../components/Page'
import api from '../lib/api' import api from '../lib/api'
import { updateQueryString } from '../lib/routing' import { updateQueryString } from '../lib/routing'
import { saveState } from '../lib/util' import { saveSettings, clearSettings, omit } from '../lib/util'
class Index extends React.Component { class Index extends React.Component {
onEditorUpdate = state => { onEditorUpdate = state => {
updateQueryString(this.props.router, state) updateQueryString(this.props.router, state)
saveState( saveSettings(
localStorage, localStorage,
omit(state, ['code', 'backgroundImage', 'backgroundImageSelection', 'filename']) omit(state, ['code', 'backgroundImage', 'backgroundImageSelection', 'filename'])
) )
@ -34,7 +33,7 @@ class Index extends React.Component {
} }
function onReset() { function onReset() {
localStorage.clear() clearSettings()
if (window.navigator && navigator.serviceWorker) { if (window.navigator && navigator.serviceWorker) {
navigator.serviceWorker.getRegistrations().then(registrations => { navigator.serviceWorker.getRegistrations().then(registrations => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

@ -4310,10 +4310,10 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
lodash.omit@^4.5.0: lodash.omitby@^4.6.0:
version "4.5.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.omit/-/lodash.omit-4.5.0.tgz#6eb19ae5a1ee1dd9df0b969e66ce0b7fa30b5e60" resolved "https://registry.yarnpkg.com/lodash.omitby/-/lodash.omitby-4.6.0.tgz#5c15ff4754ad555016b53c041311e8f079204791"
integrity sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA= integrity sha1-XBX/R1StVVAWtTwEExHo8HkgR5E=
lodash.once@^4.1.1: lodash.once@^4.1.1:
version "4.1.1" version "4.1.1"

Loading…
Cancel
Save