Refactor state components (#338)

* WIP: Unstated works

* WIP: extract into its own file

* Container instance

* Remove unused variables

* Move toolbar children into Toolbar

* Extract our Coder interface

* Remove top level subscription

* Bug fix

* WIP

* Remove old dependencies

* Add import eslint plugin

* Rename components

- add createRef TODO [ ]

* Bug fixes

* Rename Coder -> CodeWindow

* Address comments

- Rename to variable
- Rename onDrop from Editor containers perspective

* More variable renaming
main
Michael Fix 7 years ago committed by GitHub
parent f44f944d3a
commit 0a59db56c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,25 @@
module.exports = {
parser: 'babel-eslint',
env: {
browser: true,
es6: true,
node: true
},
extends: ['eslint:recommended', 'plugin:react/recommended'],
parserOptions: {
ecmaFeatures: {
experimentalObjectRestSpread: true,
jsx: true
},
sourceType: 'module'
},
plugins: ['react', 'import'],
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/display-name': 'off',
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'import/no-unresolved': 2
}
}

@ -1,279 +1,41 @@
import React, { PureComponent } from 'react'
import * as hljs from 'highlight.js'
import Spinner from 'react-spinner'
import ResizeObserver from 'resize-observer-polyfill'
import debounce from 'lodash.debounce'
import ms from 'ms'
import WindowControls from '../components/WindowControls'
import Watermark from '../components/svg/Watermark'
import CodeMirror from '../lib/react-codemirror'
import { COLORS, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, DEFAULT_SETTINGS } from '../lib/constants'
class Carbon extends PureComponent {
// Theirs
import React from 'react'
import { Provider } from 'unstated'
import HTML5Backend from 'react-dnd-html5-backend'
import { DragDropContext } from 'react-dnd'
// Ours
import EditorContainer from '../containers/Editor'
import Editor from './Editor'
import Toolbar from './Toolbar'
import { COLORS } from '../lib/constants'
class Carbon extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
language: props.config.language
}
this.handleLanguageChange = this.handleLanguageChange.bind(this)
this.handleTitleBarChange = this.handleTitleBarChange.bind(this)
this.codeUpdated = this.codeUpdated.bind(this)
this.inject = [new EditorContainer(props)]
}
componentDidMount() {
this.setState({
loading: false
})
this.handleLanguageChange(this.props.children)
const ro = new ResizeObserver(entries => {
const cr = entries[0].contentRect
this.props.onAspectRatioChange(cr.width / cr.height)
})
ro.observe(this.exportContainerNode)
}
componentWillReceiveProps(newProps) {
// TODO use getDerivedStateFromProps() on React@16.3
this.handleLanguageChange(newProps.children, { customProps: newProps })
}
codeUpdated(newCode) {
this.handleLanguageChange(newCode)
this.props.updateCode(newCode)
}
handleTitleBarChange(newTitle) {
this.props.updateTitleBar(newTitle)
}
handleLanguageChange = debounce(
(newCode, config) => {
const props = (config && config.customProps) || this.props
if (props.config.language === 'auto') {
// try to set the language
const detectedLanguage = hljs.highlightAuto(newCode).language
const languageMode =
LANGUAGE_MODE_HASH[detectedLanguage] || LANGUAGE_NAME_HASH[detectedLanguage]
if (languageMode) {
this.setState({ language: languageMode.mime || languageMode.mode })
}
} else {
this.setState({ language: props.config.language })
}
},
ms('300ms'),
{ trailing: true }
)
render() {
const config = { ...DEFAULT_SETTINGS, ...this.props.config }
const options = {
lineNumbers: config.lineNumbers,
mode: this.state.language || 'plaintext',
theme: config.theme,
scrollBarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
extraKeys: {
'Shift-Tab': 'indentLess'
}
}
const backgroundImage =
(this.props.config.backgroundImage && this.props.config.backgroundImageSelection) ||
this.props.config.backgroundImage
// set content to spinner if loading, else editor
let content = (
<div>
<Spinner />
<style jsx>
{`
div {
height: 352px;
}
`}
</style>
</div>
)
if (this.state.loading === false) {
content = (
<div id="container">
{config.windowControls ? (
<WindowControls
titleBar={this.props.titleBar}
theme={config.windowTheme}
handleTitleBarChange={this.handleTitleBarChange}
/>
) : null}
<CodeMirror
className={`CodeMirror__container window-theme__${config.windowTheme}`}
onBeforeChange={(editor, meta, code) => this.codeUpdated(code)}
value={this.props.children}
options={options}
/>
{config.watermark && <Watermark />}
<div id="container-bg">
<div className="white eliminateOnRender" />
<div className="alpha eliminateOnRender" />
<div className="bg" />
</div>
<style jsx>
{`
#container {
position: relative;
min-width: ${config.widthAdjustment ? '90px' : '680px'};
max-width: 1024px; /* The Fallback */
max-width: 92vw;
padding: ${config.paddingVertical} ${config.paddingHorizontal};
}
#container :global(.watermark) {
fill-opacity: 0.3;
position: absolute;
z-index: 2;
bottom: calc(${config.paddingVertical} + 16px);
right: calc(${config.paddingHorizontal} + 16px);
}
#container #container-bg {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
#container .white {
background: #fff;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
#container .bg {
${this.props.config.backgroundMode === 'image'
? `background: url(${backgroundImage});
background-size: cover;
background-repeat: no-repeat;`
: `background: ${this.props.config.backgroundColor || config.backgroundColor};
background-size: auto;
background-repeat: repeat;`} position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
#container .alpha {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==);
}
#container :global(.cm-s-dracula .CodeMirror-cursor) {
border-left: solid 2px #159588;
}
#container :global(.cm-s-solarized) {
box-shadow: none;
}
#container :global(.cm-s-solarized.cm-s-light) {
text-shadow: #eee8d5 0 1px;
}
#container :global(.CodeMirror-gutters) {
background-color: unset;
border-right: none;
}
#container :global(.CodeMirror__container) {
min-width: inherit;
position: relative;
z-index: 1;
border-radius: 5px;
${config.dropShadow
? `box-shadow: 0 ${config.dropShadowOffsetY} ${
config.dropShadowBlurRadius
} rgba(0, 0, 0, 0.55)`
: ''};
}
#container :global(.CodeMirror__container .CodeMirror) {
height: auto;
min-width: inherit;
padding: 18px 18px;
${config.lineNumbers ? 'padding-left: 12px;' : ''} border-radius: 5px;
font-family: ${config.fontFamily}, monospace !important;
font-size: ${config.fontSize};
font-variant-ligatures: contextual;
font-feature-settings: 'calt' 1;
user-select: none;
}
#container :global(.CodeMirror-scroll) {
overflow: hidden !important;
}
#container :global(.window-theme__sharp > .CodeMirror) {
border-radius: 0px;
}
#container :global(.window-theme__bw > .CodeMirror) {
border: 2px solid ${COLORS.SECONDARY};
}
#container :global(.window-controls + .CodeMirror__container > .CodeMirror) {
padding-top: 48px;
}
`}
</style>
</div>
)
}
return (
<div id="section">
<div id="export-container" ref={ele => (this.exportContainerNode = ele)}>
{content}
<div id="twitter-png-fix" />
<Provider inject={this.inject}>
<div id="carbon">
<Toolbar />
<Editor />
</div>
<style jsx>
{`
#section,
#export-container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
/* forces twitter to save images as png — https://github.com/dawnlabs/carbon/issues/86 */
#twitter-png-fix {
height: 1px;
width: 100%;
background: rgba(0, 0, 0, 0.01);
#carbon {
background: ${COLORS.BLACK};
border: 3px solid ${COLORS.SECONDARY};
border-radius: 8px;
padding: 16px;
}
`}
</style>
</div>
</Provider>
)
}
}
export default Carbon
export default DragDropContext(HTML5Backend)(Carbon)

@ -0,0 +1,280 @@
import React, { PureComponent } from 'react'
import * as hljs from 'highlight.js'
import Spinner from 'react-spinner'
import ResizeObserver from 'resize-observer-polyfill'
import debounce from 'lodash.debounce'
import ms from 'ms'
import WindowControls from '../components/WindowControls'
import Watermark from '../components/svg/Watermark'
import CodeMirror from '../lib/react-codemirror'
import { COLORS, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, DEFAULT_SETTINGS } from '../lib/constants'
class CodeWindow extends PureComponent {
constructor(props) {
super(props)
this.state = {
loading: true,
language: props.config.language
}
this.handleLanguageChange = this.handleLanguageChange.bind(this)
this.handleTitleBarChange = this.handleTitleBarChange.bind(this)
this.codeUpdated = this.codeUpdated.bind(this)
}
componentDidMount() {
this.setState({
loading: false
})
this.handleLanguageChange(this.props.children)
const ro = new ResizeObserver(entries => {
const cr = entries[0].contentRect
this.props.onAspectRatioChange(cr.width / cr.height)
})
ro.observe(this.exportContainerNode)
}
componentWillReceiveProps(newProps) {
// TODO use getDerivedStateFromProps() on React@16.3
this.handleLanguageChange(newProps.children, { customProps: newProps })
}
codeUpdated(newCode) {
this.handleLanguageChange(newCode)
this.props.updateCode(newCode)
}
handleTitleBarChange(newTitle) {
this.props.updateTitleBar(newTitle)
}
handleLanguageChange = debounce(
(newCode, config) => {
const props = (config && config.customProps) || this.props
if (props.config.language === 'auto') {
// try to set the language
const detectedLanguage = hljs.highlightAuto(newCode).language
const languageMode =
LANGUAGE_MODE_HASH[detectedLanguage] || LANGUAGE_NAME_HASH[detectedLanguage]
if (languageMode) {
this.setState({ language: languageMode.mime || languageMode.mode })
}
} else {
this.setState({ language: props.config.language })
}
},
ms('300ms'),
{ trailing: true }
)
render() {
const config = { ...DEFAULT_SETTINGS, ...this.props.config }
const options = {
lineNumbers: config.lineNumbers,
mode: this.state.language || 'plaintext',
theme: config.theme,
scrollBarStyle: null,
viewportMargin: Infinity,
lineWrapping: true,
extraKeys: {
'Shift-Tab': 'indentLess'
}
}
const backgroundImage =
(this.props.config.backgroundImage && this.props.config.backgroundImageSelection) ||
this.props.config.backgroundImage
// set content to spinner if loading, else editor
let content = (
<div>
<Spinner />
<style jsx>
{`
div {
height: 352px;
}
`}
</style>
</div>
)
if (this.state.loading === false) {
content = (
<div id="container">
{config.windowControls ? (
<WindowControls
titleBar={this.props.titleBar}
theme={config.windowTheme}
handleTitleBarChange={this.handleTitleBarChange}
/>
) : null}
<CodeMirror
className={`CodeMirror__container window-theme__${config.windowTheme}`}
onBeforeChange={(editor, meta, code) => this.codeUpdated(code)}
value={this.props.children}
options={options}
/>
{config.watermark && <Watermark />}
<div id="container-bg">
<div className="white eliminateOnRender" />
<div className="alpha eliminateOnRender" />
<div className="bg" />
</div>
<style jsx>
{`
#container {
position: relative;
min-width: ${config.widthAdjustment ? '90px' : '680px'};
max-width: 1024px; /* The Fallback */
max-width: 92vw;
padding: ${config.paddingVertical} ${config.paddingHorizontal};
}
#container :global(.watermark) {
fill-opacity: 0.3;
position: absolute;
z-index: 2;
bottom: calc(${config.paddingVertical} + 16px);
right: calc(${config.paddingHorizontal} + 16px);
}
#container #container-bg {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
#container .white {
background: #fff;
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
#container .bg {
${this.props.config.backgroundMode === 'image'
? `background: url(${backgroundImage});
background-size: cover;
background-repeat: no-repeat;`
: `background: ${this.props.config.backgroundColor || config.backgroundColor};
background-size: auto;
background-repeat: repeat;`} position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
}
#container .alpha {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==);
}
#container :global(.cm-s-dracula .CodeMirror-cursor) {
border-left: solid 2px #159588;
}
#container :global(.cm-s-solarized) {
box-shadow: none;
}
#container :global(.cm-s-solarized.cm-s-light) {
text-shadow: #eee8d5 0 1px;
}
#container :global(.CodeMirror-gutters) {
background-color: unset;
border-right: none;
}
#container :global(.CodeMirror__container) {
min-width: inherit;
position: relative;
z-index: 1;
border-radius: 5px;
${config.dropShadow
? `box-shadow: 0 ${config.dropShadowOffsetY} ${
config.dropShadowBlurRadius
} rgba(0, 0, 0, 0.55)`
: ''};
}
#container :global(.CodeMirror__container .CodeMirror) {
height: auto;
min-width: inherit;
padding: 18px 18px;
${config.lineNumbers ? 'padding-left: 12px;' : ''} border-radius: 5px;
font-family: ${config.fontFamily}, monospace !important;
font-size: ${config.fontSize};
font-variant-ligatures: contextual;
font-feature-settings: 'calt' 1;
user-select: none;
}
#container :global(.CodeMirror-scroll) {
overflow: hidden !important;
}
#container :global(.window-theme__sharp > .CodeMirror) {
border-radius: 0px;
}
#container :global(.window-theme__bw > .CodeMirror) {
border: 2px solid ${COLORS.SECONDARY};
}
#container :global(.window-controls + .CodeMirror__container > .CodeMirror) {
padding-top: 48px;
}
`}
</style>
</div>
)
}
return (
<div id="section">
{/* TODO use createRef */}
<div id="export-container" ref={ele => (this.exportContainerNode = ele)}>
{content}
<div id="twitter-png-fix" />
</div>
<style jsx>
{`
#section,
#export-container {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
overflow: hidden;
}
/* forces twitter to save images as png — https://github.com/dawnlabs/carbon/issues/86 */
#twitter-png-fix {
height: 1px;
width: 100%;
background: rgba(0, 0, 0, 0.01);
}
`}
</style>
</div>
)
}
}
export default CodeWindow

@ -74,11 +74,9 @@ class Dropdown extends PureComponent {
}
}
const renderDropdown = ({ button, color, list, minWidth, selected }) => ({
const renderDropdown = ({ button, color, list, minWidth }) => ({
isOpen,
highlightedIndex,
setHighlightedIndex,
selectHighlightedItem,
selectedItem,
getRootProps,
getButtonProps,

@ -1,276 +1,48 @@
// Theirs
import React from 'react'
import HTML5Backend from 'react-dnd-html5-backend'
import { DragDropContext } from 'react-dnd'
import domtoimage from 'dom-to-image'
import { Subscribe } from 'unstated'
import ReadFileDropContainer, { DATA_URL, TEXT } from 'dropperx'
// Ours
import Button from './Button'
import Dropdown from './Dropdown'
import BackgroundSelect from './BackgroundSelect'
import Settings from './Settings'
import Toolbar from './Toolbar'
import EditorContainer from '../containers/Editor'
import Overlay from './Overlay'
import Carbon from './Carbon'
import api from '../lib/api'
import {
THEMES,
THEMES_HASH,
LANGUAGES,
LANGUAGE_MIME_HASH,
LANGUAGE_MODE_HASH,
LANGUAGE_NAME_HASH,
DEFAULT_THEME,
DEFAULT_EXPORT_SIZE,
COLORS,
EXPORT_SIZES_HASH,
DEFAULT_CODE,
DEFAULT_SETTINGS
} from '../lib/constants'
import { serializeState } from '../lib/routing'
import { getState } from '../lib/util'
import CodeWindow from './CodeWindow'
const saveButtonOptions = {
button: true,
color: '#c198fb',
selected: { id: 'SAVE_IMAGE', name: 'Save Image' },
list: ['png', 'svg'].map(id => ({ id, name: id.toUpperCase() }))
}
import { DEFAULT_CODE } from '../lib/constants'
import { isImage } from '../lib/util'
const editorContainer = [EditorContainer]
class Editor extends React.Component {
constructor(props) {
super(props)
this.state = {
...DEFAULT_SETTINGS,
uploading: false,
code: props.content
}
this.save = this.save.bind(this)
this.upload = this.upload.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.updateLanguage = this.updateLanguage.bind(this)
this.updateBackground = this.updateBackground.bind(this)
this.resetDefaultSettings = this.resetDefaultSettings.bind(this)
this.getCarbonImage = this.getCarbonImage.bind(this)
this.onDrop = this.onDrop.bind(this)
}
componentDidMount() {
// Load from localStorage and then URL params
this.setState({
...getState(localStorage),
...this.props.initialState
})
}
componentDidUpdate() {
this.props.onUpdate(this.state)
}
getCarbonImage({ format } = { format: 'png' }) {
// if safari, get image from api
if (
navigator.userAgent.indexOf('Safari') !== -1 &&
navigator.userAgent.indexOf('Chrome') === -1 &&
format === 'png'
) {
const encodedState = serializeState(this.state)
return api.image(encodedState)
}
const node = document.getElementById('export-container')
const exportSize = (EXPORT_SIZES_HASH[this.state.exportSize] || DEFAULT_EXPORT_SIZE).value
const width = node.offsetWidth * exportSize
const height = this.state.squaredImage
? node.offsetWidth * exportSize
: node.offsetHeight * exportSize
const config = {
style: {
transform: `scale(${exportSize})`,
'transform-origin': 'center',
background: this.state.squaredImage ? this.state.backgroundColor : 'none'
},
filter: n => {
// %[00 -> 19] cause failures
if (n.innerText && n.innerText.match(/%[0-1][0-9]/)) {
return false
}
if (n.className) {
return String(n.className).indexOf('eliminateOnRender') < 0
}
return true
},
width,
height
}
if (format === 'svg') {
return domtoimage
.toSvg(node, config)
.then(dataUrl => dataUrl.split('&nbsp;').join('&#160;'))
.then(uri => uri.slice(uri.indexOf(',') + 1))
.then(data => new Blob([data], { type: 'image/svg+xml' }))
.then(data => window.URL.createObjectURL(data))
}
return domtoimage.toBlob(node, config).then(blob => window.URL.createObjectURL(blob))
}
updateSetting(key, value) {
this.setState({ [key]: value })
}
save({ id: format = 'png' }) {
const link = document.createElement('a')
return this.getCarbonImage({ format }).then(url => {
link.download = `carbon.${format}`
link.href = url
document.body.appendChild(link)
link.click()
link.remove()
})
}
resetDefaultSettings() {
this.setState(DEFAULT_SETTINGS)
localStorage.clear()
}
upload() {
this.setState({ uploading: true })
this.getCarbonImage({ format: 'png' })
.then(this.props.tweet)
.catch(console.error)
.then(() => this.setState({ uploading: false }))
}
onDrop([file]) {
if (isImage(file)) {
this.setState({
backgroundImage: file.content,
backgroundImageSelection: null,
backgroundMode: 'image'
})
} else {
this.setState({ code: file.content, language: 'auto' })
}
}
updateTheme(theme) {
this.updateSetting('theme', theme.id)
}
updateLanguage(language) {
this.updateSetting('language', language.mime || language.mode)
}
updateBackground({ photographer, ...changes } = {}) {
if (photographer) {
this.setState(({ code = DEFAULT_CODE }) => ({
...changes,
code: code + `\n\n// Photo by ${photographer.name} on Unsplash`
}))
} else {
this.setState(changes)
}
this.renderPane = this.renderPane.bind(this)
}
render() {
renderPane(editor) {
return (
<React.Fragment>
<div id="editor">
<Toolbar>
<Dropdown
selected={THEMES_HASH[this.state.theme] || DEFAULT_THEME}
list={THEMES}
onChange={this.updateTheme}
/>
<Dropdown
selected={
LANGUAGE_NAME_HASH[this.state.language] ||
LANGUAGE_MIME_HASH[this.state.language] ||
LANGUAGE_MODE_HASH[this.state.language] ||
'auto'
}
list={LANGUAGES}
onChange={this.updateLanguage}
/>
<BackgroundSelect
onChange={this.updateBackground}
mode={this.state.backgroundMode}
color={this.state.backgroundColor}
image={this.state.backgroundImage}
aspectRatio={this.state.aspectRatio}
/>
<Settings
{...this.state}
onChange={this.updateSetting}
resetDefaultSettings={this.resetDefaultSettings}
/>
<div className="buttons">
{this.props.tweet && (
<Button
className="tweetButton"
onClick={this.upload}
title={this.state.uploading ? 'Loading...' : 'Tweet Image'}
color="#57b5f9"
style={{ marginRight: '8px' }}
/>
)}
<Dropdown {...saveButtonOptions} onChange={this.save} />
</div>
</Toolbar>
<ReadFileDropContainer readAs={readAs} onDrop={this.onDrop}>
<ReadFileDropContainer readAs={readAs} onDrop={editor.handleDroppedFile}>
{({ isOver, canDrop }) => (
<Overlay
isOver={isOver || canDrop}
title={`Drop your file here to import ${isOver ? '✋' : '✊'}`}
>
<Carbon
config={this.state}
updateCode={this.updateCode}
onAspectRatioChange={this.updateAspectRatio}
titleBar={this.state.titleBar}
updateTitleBar={this.updateTitleBar}
<CodeWindow
config={editor.state}
updateCode={editor.updateCode}
onAspectRatioChange={editor.updateAspectRatio}
titleBar={editor.state.titleBar}
updateTitleBar={editor.updateTitleBar}
>
{this.state.code != null ? this.state.code : DEFAULT_CODE}
</Carbon>
{editor.state.code != null ? editor.state.code : DEFAULT_CODE}
</CodeWindow>
</Overlay>
)}
</ReadFileDropContainer>
</div>
<style jsx>
{`
#editor {
background: ${COLORS.BLACK};
border: 3px solid ${COLORS.SECONDARY};
border-radius: 8px;
padding: 16px;
}
.buttons {
display: flex;
margin-left: auto;
}
`}
</style>
</React.Fragment>
)
}
}
function isImage(file) {
return file.type.split('/')[0] === 'image'
render() {
return <Subscribe to={editorContainer}>{this.renderPane}</Subscribe>
}
}
function readAs(file) {
@ -280,8 +52,4 @@ function readAs(file) {
return TEXT
}
Editor.defaultProps = {
onUpdate: () => {}
}
export default DragDropContext(HTML5Backend)(Editor)
export default Editor

@ -1,3 +1,4 @@
import React from 'react'
import Head from 'next/head'
import { THEMES } from '../lib/constants'
import Reset from './style/Reset'

@ -1,8 +1,76 @@
import React from 'react'
import { Subscribe } from 'unstated'
const Toolbar = props => (
import EditorContainer from '../containers/Editor'
import Button from './Button'
import Dropdown from './Dropdown'
import BackgroundSelect from './BackgroundSelect'
import Settings from './Settings'
import {
THEMES,
THEMES_HASH,
LANGUAGES,
LANGUAGE_MIME_HASH,
LANGUAGE_MODE_HASH,
LANGUAGE_NAME_HASH,
DEFAULT_THEME
} from '../lib/constants'
const editorContainer = [EditorContainer]
const saveButtonOptions = {
button: true,
color: '#c198fb',
selected: { id: 'SAVE_IMAGE', name: 'Save Image' },
list: ['png', 'svg'].map(id => ({ id, name: id.toUpperCase() }))
}
function Toolbar() {
return <Subscribe to={editorContainer}>{render}</Subscribe>
}
function render(editor) {
return (
<div id="toolbar">
{props.children}
<Dropdown
selected={THEMES_HASH[editor.state.theme] || DEFAULT_THEME}
list={THEMES}
onChange={editor.updateTheme}
/>
<Dropdown
selected={
LANGUAGE_NAME_HASH[editor.state.language] ||
LANGUAGE_MIME_HASH[editor.state.language] ||
LANGUAGE_MODE_HASH[editor.state.language] ||
'auto'
}
list={LANGUAGES}
onChange={editor.updateLanguage}
/>
<BackgroundSelect
onChange={editor.updateBackground}
mode={editor.state.backgroundMode}
color={editor.state.backgroundColor}
image={editor.state.backgroundImage}
aspectRatio={editor.state.aspectRatio}
/>
<Settings
{...editor.state}
onChange={editor.updateSetting}
resetDefaultSettings={editor.resetDefaultSettings}
/>
<div className="buttons">
{/* TODO don't set container function if no prop */}
{editor.upload && (
<Button
className="tweetButton"
onClick={editor.upload}
title={editor.state.uploading ? 'Loading...' : 'Tweet Image'}
color="#57b5f9"
style={{ marginRight: '8px' }}
/>
)}
<Dropdown {...saveButtonOptions} onChange={editor.save} />
</div>
<style jsx>
{`
#toolbar {
@ -23,9 +91,15 @@ const Toolbar = props => (
#toolbar > :global(div):last-child {
margin-right: 0px;
}
.buttons {
display: flex;
margin-left: auto;
}
`}
</style>
</div>
)
}
export default Toolbar

@ -0,0 +1,168 @@
// Theirs
import { Container } from 'unstated'
import domtoimage from 'dom-to-image'
// Ours
import api from '../lib/api'
import {
DEFAULT_EXPORT_SIZE,
EXPORT_SIZES_HASH,
DEFAULT_CODE,
DEFAULT_SETTINGS
} from '../lib/constants'
import { serializeState } from '../lib/routing'
import { getState, isImage } from '../lib/util'
class EditorContainer extends Container {
constructor(props = {}) {
super(props)
this.state = {
...DEFAULT_SETTINGS,
uploading: false,
code: props.content
}
this.save = this.save.bind(this)
this.upload = this.upload.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.updateLanguage = this.updateLanguage.bind(this)
this.updateBackground = this.updateBackground.bind(this)
this.resetDefaultSettings = this.resetDefaultSettings.bind(this)
this.getCarbonImage = this.getCarbonImage.bind(this)
this.handleDroppedFile = this.handleDroppedFile.bind(this)
}
componentDidMount() {
// Load from localStorage and then URL params
this.setState({
...getState(localStorage),
...this.props.initialState
})
}
componentDidUpdate() {
this.props.onUpdate(this.state)
}
getCarbonImage({ format } = { format: 'png' }) {
// if safari, get image from api
if (
navigator.userAgent.indexOf('Safari') !== -1 &&
navigator.userAgent.indexOf('Chrome') === -1 &&
format === 'png'
) {
const encodedState = serializeState(this.state)
return api.image(encodedState)
}
const node = document.getElementById('export-container')
const exportSize = (EXPORT_SIZES_HASH[this.state.exportSize] || DEFAULT_EXPORT_SIZE).value
const width = node.offsetWidth * exportSize
const height = this.state.squaredImage
? node.offsetWidth * exportSize
: node.offsetHeight * exportSize
const config = {
style: {
transform: `scale(${exportSize})`,
'transform-origin': 'center',
background: this.state.squaredImage ? this.state.backgroundColor : 'none'
},
filter: n => {
// %[00 -> 19] cause failures
if (n.innerText && n.innerText.match(/%[0-1][0-9]/)) {
return false
}
if (n.className) {
return String(n.className).indexOf('eliminateOnRender') < 0
}
return true
},
width,
height
}
if (format === 'svg') {
return domtoimage
.toSvg(node, config)
.then(dataUrl => dataUrl.split('&nbsp;').join('&#160;'))
.then(uri => uri.slice(uri.indexOf(',') + 1))
.then(data => new Blob([data], { type: 'image/svg+xml' }))
.then(data => window.URL.createObjectURL(data))
}
return domtoimage.toBlob(node, config).then(blob => window.URL.createObjectURL(blob))
}
updateSetting(key, value) {
this.setState({ [key]: value })
}
save({ id: format = 'png' }) {
const link = document.createElement('a')
return this.getCarbonImage({ format }).then(url => {
link.download = `carbon.${format}`
link.href = url
document.body.appendChild(link)
link.click()
link.remove()
})
}
resetDefaultSettings() {
this.setState(DEFAULT_SETTINGS)
localStorage.clear()
}
upload() {
this.setState({ uploading: true })
this.getCarbonImage({ format: 'png' })
.then(this.props.tweet)
// eslint-disable-next-line
.catch(console.error)
.then(() => this.setState({ uploading: false }))
}
handleDroppedFile([file]) {
if (isImage(file)) {
this.setState({
backgroundImage: file.content,
backgroundImageSelection: null,
backgroundMode: 'image'
})
} else {
this.setState({ code: file.content, language: 'auto' })
}
}
updateTheme(theme) {
this.updateSetting('theme', theme.id)
}
updateLanguage(language) {
this.updateSetting('language', language.mime || language.mode)
}
updateBackground({ photographer, ...changes } = {}) {
if (photographer) {
this.setState(({ code = DEFAULT_CODE }) => ({
...changes,
code: code + `\n\n// Photo by ${photographer.name} on Unsplash`
}))
} else {
this.setState(changes)
}
}
}
EditorContainer.defaultProps = {
onUpdate: () => {}
}
export default EditorContainer

@ -56,7 +56,7 @@ describe('background color', () => {
const pink = 'ff00ff'
openPicker()
.find(`input[value="FF0000"]`)
.find('input[value="FF0000"]')
.clear()
.type(`${pink}{enter}`)
closePicker()

@ -11,7 +11,7 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = (on, config) => {
module.exports = (/* on, config */) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
}

@ -33,6 +33,7 @@ module.exports = browser => async (req, res) => {
res.status(200).json({ dataUrl })
} catch (e) {
// eslint-disable-next-line
console.error(e)
res.status(500).send()
} finally {

@ -29,6 +29,7 @@ const respondFail = (res, err) => {
return res.status(420).send()
}
// eslint-disable-next-line
console.error(`Error: ${err.message || JSON.stringify(err, null, 2)}`)
return res.status(500).send()
}

@ -1,3 +1,4 @@
/* eslint-disable */
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
// Apache mode by gloony

@ -1,4 +1,5 @@
/* eslint-disable */
import CodeMirror from 'codemirror'
// Require Codemirror elixir mode from npm modules and register it here
import registerElixirMode from 'codemirror-mode-elixir'
export * from 'codemirror-mode-elixir'

@ -1,2 +1,3 @@
/* eslint-disable */
import CodeMirror from 'codemirror'
import 'codemirror-graphql/mode'

@ -1,3 +1,4 @@
/* eslint-disable */
const CodeMirror = require('codemirror')
CodeMirror.defineMode('nimrod', function(conf, parserConf) {

@ -1,3 +1,4 @@
/* eslint-disable */
;(function(global) {
'use strict'

@ -36,3 +36,7 @@ export const fileToDataURL = blob =>
reader.onload = e => res(e.target.result)
reader.readAsDataURL(blob)
})
export function isImage(file) {
return file.type.split('/')[0] === 'image'
}

@ -10,7 +10,7 @@
"test": "npm run cy:run --",
"deploy": "now --public --docker -e NODE_ENV=production",
"prettier": "prettier --config .prettierrc --write *.js {components,handlers,lib,pages}/*.js",
"lint": "prettier --config .prettierrc *.js {components,handlers,lib,pages}/*.js",
"lint": "eslint .",
"precommit": "npm run contrib:build && git add README.md && lint-staged",
"contrib:add": "all-contributors add",
"contrib:build": "all-contributors generate",
@ -23,13 +23,11 @@
"codemirror": "^5.36.0",
"codemirror-graphql": "^0.6.12",
"codemirror-mode-elixir": "^1.1.1",
"cross-env": "^5.1.3",
"dom-to-image": "^2.5.2",
"downshift": "^1.28.0",
"dropperx": "^0.1.0",
"express": "^4.16.2",
"form-data": "^2.2.0",
"graphql": "^0.13.2",
"graphql": "^0.11.7",
"highlight.js": "^9.12.0",
"history": "^4.7.2",
"isomorphic-fetch": "^2.2.1",
@ -50,18 +48,22 @@
"react-dnd-html5-backend": "^2.4.1",
"react-dom": "16.2.0",
"react-image-crop": "^3.0.9",
"react-spinkit": "^3.0.0",
"react-spinner": "^0.2.7",
"react-syntax-highlight": "^15.3.1",
"resize-observer-polyfill": "^1.5.0",
"tohash": "^1.0.2",
"twitter": "^1.7.1",
"unsplash-js": "^4.8.0"
"unsplash-js": "^4.8.0",
"unstated": "^2.0.2"
},
"devDependencies": {
"@zeit/next-css": "^0.1.5",
"all-contributors-cli": "^4.7.0",
"babel-eslint": "8",
"cypress": "^2.1.0",
"eslint": "^4.19.1",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-react": "^7.7.0",
"hex2rgb": "^2.2.0",
"husky": "^0.14.3",
"lint-staged": "^7.0.4",

@ -1,3 +1,4 @@
import React from 'react'
import Document, { Head, Main, NextScript } from 'next/document'
export default class extends Document {

@ -1,3 +1,4 @@
import React from 'react'
import Page from '../components/Page'
import { COLORS } from '../lib/constants'

@ -2,7 +2,7 @@
import React from 'react'
// Ours
import Editor from '../components/Editor'
import Carbon from '../components/Carbon'
import Page from '../components/Page'
import api from '../lib/api'
import { getQueryStringState, updateQueryString } from '../lib/routing'
@ -23,6 +23,7 @@ class Index extends React.Component {
return { content, initialState }
}
} catch (e) {
// eslint-disable-next-line
console.log(e)
}
return { initialState }
@ -31,7 +32,7 @@ class Index extends React.Component {
render() {
return (
<Page enableHeroText={true}>
<Editor {...this.props} onUpdate={onEditorUpdate} tweet={api.tweet} />
<Carbon {...this.props} onUpdate={onEditorUpdate} tweet={api.tweet} />
</Page>
)
}

@ -1,3 +1,4 @@
/* eslint-disable no-console */
const express = require('express')
const morgan = require('morgan')
const bodyParser = require('body-parser')

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save