expose CodeMirrorLink, load only necessary theme in embed

implement copy button in Carbon window controls

add copy to queryParam

use next/head and metatags in /embed

make editor have router prop

fix now.json rewrites

allow local stylesheets in embed
main
Mike Fix 6 years ago
parent 1d85149a97
commit c0ff116de8

@ -10,6 +10,11 @@ import Watermark from '../components/svg/Watermark'
import { COLORS, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, DEFAULT_SETTINGS } from '../lib/constants' import { COLORS, LANGUAGE_MODE_HASH, LANGUAGE_NAME_HASH, DEFAULT_SETTINGS } from '../lib/constants'
class Carbon extends React.PureComponent { class Carbon extends React.PureComponent {
static defaultProps = {
onAspectRatioChange: () => {},
updateCode: () => {}
}
componentDidMount() { componentDidMount() {
const ro = new ResizeObserver(entries => { const ro = new ResizeObserver(entries => {
const cr = entries[0].contentRect const cr = entries[0].contentRect
@ -69,6 +74,8 @@ class Carbon extends React.PureComponent {
titleBar={this.props.titleBar} titleBar={this.props.titleBar}
theme={config.windowTheme} theme={config.windowTheme}
handleTitleBarChange={this.props.updateTitleBar} handleTitleBarChange={this.props.updateTitleBar}
code={this.props.children}
copyable={this.props.copyable}
/> />
) : null} ) : null}
<CodeMirror <CodeMirror

@ -0,0 +1,36 @@
// TODO publish rucksack and import from there
import React from 'react'
import CopyToClipboard from 'react-copy-to-clipboard'
class CopyButton extends React.Component {
constructor(props) {
super(props)
this.state = {
copied: false
}
this.onCopy = this.onCopy.bind(this)
}
onCopy() {
this.setState({ copied: true })
const component = this
setTimeout(
() => component.setState({ copied: false }),
this.props.interval == null ? 1000 : this.props.interval
)
}
render() {
return (
<CopyToClipboard text={this.props.text} onCopy={this.onCopy}>
{this.props.children({
copied: this.state.copied
})}
</CopyToClipboard>
)
}
}
export default CopyButton

@ -58,7 +58,7 @@ class Dropdown extends React.PureComponent {
userInputtedValue = '' userInputtedValue = ''
render() { render() {
const { button, color, selected, onChange } = this.props const { button, color, selected, onChange, itemWrapper } = this.props
const { itemsToShow, inputValue } = this.state const { itemsToShow, inputValue } = this.state
const minWidth = calcMinWidth(button, selected, itemsToShow) const minWidth = calcMinWidth(button, selected, itemsToShow)
@ -72,13 +72,13 @@ class Dropdown extends React.PureComponent {
onChange={onChange} onChange={onChange}
onUserAction={this.onUserAction} onUserAction={this.onUserAction}
> >
{renderDropdown({ button, color, list: itemsToShow, selected, minWidth })} {renderDropdown({ button, color, list: itemsToShow, selected, minWidth, itemWrapper })}
</Downshift> </Downshift>
) )
} }
} }
const renderDropdown = ({ button, color, list, minWidth }) => ({ const renderDropdown = ({ button, color, list, minWidth, itemWrapper }) => ({
isOpen, isOpen,
highlightedIndex, highlightedIndex,
selectedItem, selectedItem,
@ -104,6 +104,7 @@ const renderDropdown = ({ button, color, list, minWidth }) => ({
<ListItem <ListItem
key={index} key={index}
color={color} color={color}
itemWrapper={itemWrapper}
{...getItemProps({ {...getItemProps({
item, item,
isSelected: selectedItem === item, isSelected: selectedItem === item,
@ -212,12 +213,16 @@ const ListItems = ({ children, color }) => {
) )
} }
const ListItem = ({ children, color, isHighlighted, isSelected, ...rest }) => { const ListItem = ({ children, color, isHighlighted, isSelected, itemWrapper, ...rest }) => {
const itemColor = color || COLORS.SECONDARY const itemColor = color || COLORS.SECONDARY
return ( return (
<li {...rest} role="option" className="dropdown-list-item"> <li {...rest} role="option" className="dropdown-list-item">
<span className="dropdown-list-item-text">{children}</span> {itemWrapper ? (
itemWrapper({ children, color: itemColor })
) : (
<span className="dropdown-list-item-text">{children}</span>
)}
{isSelected ? <CheckMark /> : null} {isSelected ? <CheckMark /> : null}
<style jsx> <style jsx>
{` {`

@ -14,6 +14,7 @@ import Dropdown from './Dropdown'
import BackgroundSelect from './BackgroundSelect' import BackgroundSelect from './BackgroundSelect'
import Settings from './Settings' import Settings from './Settings'
import Toolbar from './Toolbar' import Toolbar from './Toolbar'
import ExportButton from './ExportButton'
import Overlay from './Overlay' import Overlay from './Overlay'
import Carbon from './Carbon' import Carbon from './Carbon'
import { import {
@ -37,8 +38,9 @@ import { getState, escapeHtml, unescapeHtml } from '../lib/util'
const saveButtonOptions = { const saveButtonOptions = {
button: true, button: true,
color: '#c198fb', color: '#c198fb',
selected: { id: 'SAVE_IMAGE', name: 'Save Image' }, selected: { id: 'SAVE_IMAGE', name: 'Export Image' },
list: ['png', 'svg', 'open ↗'].map(id => ({ id, name: id.toUpperCase() })) list: ['png', 'svg', 'copy embed', 'open ↗'].map(id => ({ id, name: id.toUpperCase() })),
itemWrapper: props => <ExportButton {...props} />
} }
class Editor extends React.Component { class Editor extends React.Component {
@ -52,7 +54,7 @@ class Editor extends React.Component {
online: true online: true
} }
this.save = this.save.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.updateCode = this.updateSetting.bind(this, 'code')
@ -70,7 +72,7 @@ class Editor extends React.Component {
} }
async componentDidMount() { async componentDidMount() {
const { asPath = '' } = this.props const { asPath = '' } = this.props.router
const { query, pathname } = url.parse(asPath, true) const { query, pathname } = url.parse(asPath, true)
const path = escapeHtml(pathname.split('/').pop()) const path = escapeHtml(pathname.split('/').pop())
const queryParams = getQueryStringState(query) const queryParams = getQueryStringState(query)
@ -187,7 +189,11 @@ class Editor extends React.Component {
this.setState({ [key]: value }) this.setState({ [key]: value })
} }
save({ id: format = 'png' }) { export({ id: format = 'png' }) {
if (format === 'copy embed') {
return
}
const link = document.createElement('a') const link = document.createElement('a')
const timestamp = this.state.timestamp ? `_${formatTimestamp()}` : '' const timestamp = this.state.timestamp ? `_${formatTimestamp()}` : ''
@ -305,7 +311,7 @@ class Editor extends React.Component {
style={{ marginRight: '8px' }} style={{ marginRight: '8px' }}
/> />
)} )}
<Dropdown {...saveButtonOptions} onChange={this.save} /> <Dropdown {...saveButtonOptions} onChange={this.export} />
</div> </div>
</Toolbar> </Toolbar>

@ -0,0 +1,46 @@
import React from 'react'
import { withRouter } from 'next/router'
import CopyButton from './CopyButton'
const toIFrame = url =>
`<iframe
src="https://carbon.now.sh/embed${url}"
style="transform:scale(0.7); width:1024px; height:473px; border:0; overflow:hidden;"
sandbox="allow-scripts allow-same-origin">
</iframe>
`
function ExportButton({ router, children, color }) {
return (
<React.Fragment>
{children === 'COPY EMBED' ? (
<CopyButton text={toIFrame(router.asPath)}>
{({ copied }) => <button>{copied ? 'COPIED!' : 'EMBED CODE'}</button>}
</CopyButton>
) : (
<button>{children}</button>
)}
<style jsx>
{`
button {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
vertical-align: baseline;
color: ${color};
background: transparent;
cursor: pointer;
}
&:active {
outline: none;
}
`}
</style>
</React.Fragment>
)
}
export default withRouter(ExportButton)

@ -4,12 +4,46 @@ import Reset from './style/Reset'
import Font from './style/Font' import Font from './style/Font'
import Typography from './style/Typography' import Typography from './style/Typography'
const LOCAL_STYLESHEETS = ['one-dark', 'verminal', 'night-owl', 'nord'] export const LOCAL_STYLESHEETS = ['one-dark', 'verminal', 'night-owl', 'nord']
const CDN_STYLESHEETS = THEMES.filter( const CDN_STYLESHEETS = THEMES.filter(
t => t.hasStylesheet !== false && LOCAL_STYLESHEETS.indexOf(t.id) < 0 t => t.hasStylesheet !== false && LOCAL_STYLESHEETS.indexOf(t.id) < 0
) )
export const CodeMirrorLink = () => (
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/codemirror.min.css"
/>
)
export const MetaTags = () => (
<>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Carbon is the easiest way to create and share beautiful images of your source code."
/>
<meta name="application-name" content="Carbon" />
<meta name="twitter:title" content="Carbon" />
<meta
name="twitter:description"
content="Carbon is the easiest way to create and share beautiful images of your source code."
/>
<meta name="og:title" content="Carbon" />
<meta
name="og:description"
content="Carbon is the easiest way to create and share beautiful images of your source code."
/>
<meta name="og:image" content="/static/banner.png" />
<meta name="theme-color" content="#121212" />
<title>Carbon</title>
<link rel="shortcut icon" href="/static/favicon.ico" />
</>
)
/* /*
* Before supporting <link rel="preload"> verify that it is widely supported in FireFox * Before supporting <link rel="preload"> verify that it is widely supported in FireFox
* with out a flag here: https://caniuse.com/#feat=link-rel-preload * with out a flag here: https://caniuse.com/#feat=link-rel-preload
@ -19,38 +53,14 @@ export default function Meta() {
return ( return (
<div className="meta"> <div className="meta">
<Head> <Head>
<meta charSet="utf-8" /> <MetaTags />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="Carbon is the easiest way to create and share beautiful images of your source code."
/>
<meta name="application-name" content="Carbon" />
<meta name="twitter:title" content="Carbon" />
<meta
name="twitter:description"
content="Carbon is the easiest way to create and share beautiful images of your source code."
/>
<meta name="og:title" content="Carbon" />
<meta
name="og:description"
content="Carbon is the easiest way to create and share beautiful images of your source code."
/>
<meta name="og:image" content="/static/banner.png" />
<meta name="theme-color" content="#121212" />
<title>Carbon</title>
<link rel="manifest" href="/static/manifest.json" />
<link rel="shortcut icon" href="/static/favicon.ico" />
<link rel="stylesheet" href="/static/react-crop.css" /> <link rel="stylesheet" href="/static/react-crop.css" />
<link rel="manifest" href="/static/manifest.json" />
<link <link
rel="stylesheet" rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/theme/seti.min.css" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/theme/seti.min.css"
/> />
<link <CodeMirrorLink />
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/codemirror.min.css"
/>
<link <link
rel="stylesheet" rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/theme/solarized.min.css" href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/theme/solarized.min.css"

@ -1,7 +1,40 @@
import React from 'react' import React from 'react'
import CopyButton from './CopyButton'
import { COLORS } from '../lib/constants'
import { Controls, ControlsBW } from './svg/Controls' import { Controls, ControlsBW } from './svg/Controls'
import CopySVG from './svg/Copy'
import CheckMark from './svg/Checkmark'
const size = 24
function renderCopyButton({ copied }) {
return (
<button>
{copied ? (
<CheckMark color={COLORS.GRAY} width={size} height={size} />
) : (
<CopySVG size={size} color={COLORS.GRAY} />
)}
<style jsx>
{`
button {
border: none;
cursor: pointer;
color: ${COLORS.SECONDARY};
background: transparent;
}
&:active {
outline: none;
}
`}
</style>
</button>
)
}
export default ({ titleBar, theme, handleTitleBarChange }) => ( export default ({ titleBar, theme, handleTitleBarChange, copyable, code }) => (
<div className="window-controls"> <div className="window-controls">
{theme === 'bw' ? <ControlsBW /> : <Controls />} {theme === 'bw' ? <ControlsBW /> : <Controls />}
<div className="window-title-container"> <div className="window-title-container">
@ -12,6 +45,11 @@ export default ({ titleBar, theme, handleTitleBarChange }) => (
onChange={e => handleTitleBarChange(e.target.value)} onChange={e => handleTitleBarChange(e.target.value)}
/> />
</div> </div>
{copyable && (
<div className="copy-button">
<CopyButton text={code}>{renderCopyButton}</CopyButton>
</div>
)}
<style jsx> <style jsx>
{` {`
div { div {
@ -40,6 +78,13 @@ export default ({ titleBar, theme, handleTitleBarChange }) => (
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
} }
.copy-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 16px;
}
`} `}
</style> </style>
</div> </div>

@ -1,9 +1,9 @@
import React from 'react' import React from 'react'
export default () => ( export default ({ width = 9, height = 8, color = '#FFFFFF' }) => (
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="8" viewBox="0 0 9 7"> <svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 9 7">
<polygon <polygon
fill="#FFFFFF" fill={color}
fillRule="evenodd" fillRule="evenodd"
points="2.852 5.016 8.275 0 9 .67 2.852 6.344 0 3.711 .713 3.042" points="2.852 5.016 8.275 0 9 .67 2.852 6.344 0 3.711 .713 3.042"
/> />

@ -0,0 +1,29 @@
import React from 'react'
const SVG_RATIO = 0.81
const Copy = ({ size, color }) => {
const width = size * SVG_RATIO
const height = size
return (
<svg
width={width}
height={height}
viewBox="0 0 13 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 0H3.40385C2.55385 0 1.84615 0.669231 1.84615 1.51923V1.84615H1.55769C0.707692 1.84615 0 2.51538 0 3.36538V14.4423C0 15.2923 0.707692 16 1.55769 16H9.55769C10.4077 16 11.0769 15.2923 11.0769 14.4423V14.1538H11.4038C12.2538 14.1538 12.9231 13.4462 12.9231 12.5962V4.92308L8 0ZM8 1.71538L11.2077 4.92308H8V1.71538ZM9.84615 14.4423C9.84615 14.6231 9.71538 14.7692 9.55769 14.7692H1.55769C1.38846 14.7692 1.23077 14.6115 1.23077 14.4423V3.36538C1.23077 3.20769 1.37692 3.07692 1.55769 3.07692H1.84615V12.9038C1.84615 13.7538 2.24615 14.1538 3.09615 14.1538H9.84615V14.4423ZM11.6923 12.5962C11.6923 12.7769 11.5615 12.9231 11.4038 12.9231H3.40385C3.23462 12.9231 3.07692 12.7654 3.07692 12.5962V1.51923C3.07692 1.36154 3.22308 1.23077 3.40385 1.23077H6.76923V6.15385H11.6923V12.5962Z"
fill={color}
/>
</svg>
)
}
Copy.defaultProps = {
size: 16
}
export default Copy

@ -30,7 +30,8 @@ const mappings = [
{ field: 'code:code' }, { field: 'code:code' },
{ field: 'es:exportSize' }, { field: 'es:exportSize' },
{ field: 'wm:watermark', type: 'bool' }, { field: 'wm:watermark', type: 'bool' },
{ field: 'ts:timestamp', type: 'bool' } { field: 'ts:timestamp', type: 'bool' },
{ field: 'copy', type: 'bool' }
] ]
const reverseMappings = mappings.map(mapping => const reverseMappings = mappings.map(mapping =>

@ -6,6 +6,7 @@ module.exports = (/* phase, { defaultConfig } */) => {
exportPathMap() { exportPathMap() {
return { return {
'/about': { page: '/about' }, '/about': { page: '/about' },
'/embed': { page: '/embed' },
'/index': { page: '/index' }, '/index': { page: '/index' },
'/': { page: '/' } '/': { page: '/' }
} }

@ -5,7 +5,7 @@
"static": { "static": {
"rewrites": [ "rewrites": [
{ {
"source": "!/about", "source": "!/?(about)?(embed)",
"destination": "/index.html" "destination": "/index.html"
} }
] ]

@ -40,6 +40,7 @@
"react-click-outside": "^3.0.0", "react-click-outside": "^3.0.0",
"react-codemirror2": "^5.1.0", "react-codemirror2": "^5.1.0",
"react-color": "^2.13.8", "react-color": "^2.13.8",
"react-copy-to-clipboard": "^5.0.1",
"react-dnd": "^5.0.0", "react-dnd": "^5.0.0",
"react-dnd-html5-backend": "^5.0.0", "react-dnd-html5-backend": "^5.0.0",
"react-dom": "16.3.*", "react-dom": "16.3.*",

@ -0,0 +1,71 @@
// Theirs
import React from 'react'
import Head from 'next/head'
import { withRouter } from 'next/router'
import url from 'url'
// Ours
import { LOCAL_STYLESHEETS, CodeMirrorLink, MetaTags } from '../components/Meta'
import Carbon from '../components/Carbon'
import { DEFAULT_CODE, DEFAULT_SETTINGS } from '../lib/constants'
import { getQueryStringState } from '../lib/routing'
const Page = props => (
<div>
<Head>
<MetaTags />
{LOCAL_STYLESHEETS.indexOf(props.theme) > -1 ? (
<link rel="stylesheet" href={`/static/themes/${props.theme}.css`} />
) : (
<link
rel="stylesheet"
href={`//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/theme/${props.theme}.min.css`}
/>
)}
<CodeMirrorLink />
</Head>
{props.children}
<style jsx global>
{`
html,
body {
margin: 0;
background: transparent;
min-height: 0;
}
`}
</style>
</div>
)
class Embed extends React.Component {
state = {
...DEFAULT_SETTINGS,
code: DEFAULT_CODE,
mounted: false
}
componentDidMount() {
const { asPath = '' } = this.props.router
const { query } = url.parse(asPath, true)
const queryParams = getQueryStringState(query)
const initialState = Object.keys(queryParams).length ? queryParams : {}
this.setState({ ...initialState, copyable: queryParams.copy !== false, mounted: true })
}
render() {
return (
<Page theme={this.state.theme}>
{this.state.mounted && (
<Carbon config={this.state} copyable={this.state.copyable}>
{this.state.code}
</Carbon>
)}
</Page>
)
}
}
export default withRouter(Embed)

@ -22,7 +22,12 @@ class Index extends React.Component {
render() { render() {
return ( return (
<Page enableHeroText={true}> <Page enableHeroText={true}>
<Editor {...this.props.router} onUpdate={this.onEditorUpdate} api={api} onReset={onReset} /> <Editor
router={this.props.router}
onUpdate={this.onEditorUpdate}
api={api}
onReset={onReset}
/>
</Page> </Page>
) )
} }

@ -1577,6 +1577,12 @@ copy-descriptor@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
copy-to-clipboard@^3:
version "3.0.8"
resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.0.8.tgz#f4e82f4a8830dce4666b7eb8ded0c9bcc313aba9"
dependencies:
toggle-selection "^1.0.3"
core-js@^1.0.0: core-js@^1.0.0:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
@ -4579,7 +4585,7 @@ prop-types@15.6.0:
loose-envify "^1.3.1" loose-envify "^1.3.1"
object-assign "^4.1.1" object-assign "^4.1.1"
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.2: prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2:
version "15.6.2" version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies: dependencies:
@ -4712,6 +4718,13 @@ react-color@^2.13.8:
reactcss "^1.2.0" reactcss "^1.2.0"
tinycolor2 "^1.4.1" tinycolor2 "^1.4.1"
react-copy-to-clipboard@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
dependencies:
copy-to-clipboard "^3"
prop-types "^15.5.8"
"react-dnd-html5-backend@^4.0.2 || ^5.0.0", react-dnd-html5-backend@^5.0.0: "react-dnd-html5-backend@^4.0.2 || ^5.0.0", react-dnd-html5-backend@^5.0.0:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-5.0.1.tgz#0b578d79c5c01317c70414c8d717f632b919d4f1" resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-5.0.1.tgz#0b578d79c5c01317c70414c8d717f632b919d4f1"
@ -5635,6 +5648,10 @@ to-regex@^3.0.1, to-regex@^3.0.2:
regex-not "^1.0.2" regex-not "^1.0.2"
safe-regex "^1.1.0" safe-regex "^1.1.0"
toggle-selection@^1.0.3:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
tohash@^1.0.2: tohash@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/tohash/-/tohash-1.0.2.tgz#9e66e497da0cfd77ba85f9663065adf2d8c99981" resolved "https://registry.yarnpkg.com/tohash/-/tohash-1.0.2.tgz#9e66e497da0cfd77ba85f9663065adf2d8c99981"

Loading…
Cancel
Save