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'
class Carbon extends React.PureComponent {
static defaultProps = {
onAspectRatioChange: () => {},
updateCode: () => {}
}
componentDidMount() {
const ro = new ResizeObserver(entries => {
const cr = entries[0].contentRect
@ -69,6 +74,8 @@ class Carbon extends React.PureComponent {
titleBar={this.props.titleBar}
theme={config.windowTheme}
handleTitleBarChange={this.props.updateTitleBar}
code={this.props.children}
copyable={this.props.copyable}
/>
) : null}
<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 = ''
render() {
const { button, color, selected, onChange } = this.props
const { button, color, selected, onChange, itemWrapper } = this.props
const { itemsToShow, inputValue } = this.state
const minWidth = calcMinWidth(button, selected, itemsToShow)
@ -72,13 +72,13 @@ class Dropdown extends React.PureComponent {
onChange={onChange}
onUserAction={this.onUserAction}
>
{renderDropdown({ button, color, list: itemsToShow, selected, minWidth })}
{renderDropdown({ button, color, list: itemsToShow, selected, minWidth, itemWrapper })}
</Downshift>
)
}
}
const renderDropdown = ({ button, color, list, minWidth }) => ({
const renderDropdown = ({ button, color, list, minWidth, itemWrapper }) => ({
isOpen,
highlightedIndex,
selectedItem,
@ -104,6 +104,7 @@ const renderDropdown = ({ button, color, list, minWidth }) => ({
<ListItem
key={index}
color={color}
itemWrapper={itemWrapper}
{...getItemProps({
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
return (
<li {...rest} role="option" className="dropdown-list-item">
{itemWrapper ? (
itemWrapper({ children, color: itemColor })
) : (
<span className="dropdown-list-item-text">{children}</span>
)}
{isSelected ? <CheckMark /> : null}
<style jsx>
{`

@ -14,6 +14,7 @@ import Dropdown from './Dropdown'
import BackgroundSelect from './BackgroundSelect'
import Settings from './Settings'
import Toolbar from './Toolbar'
import ExportButton from './ExportButton'
import Overlay from './Overlay'
import Carbon from './Carbon'
import {
@ -37,8 +38,9 @@ import { getState, escapeHtml, unescapeHtml } from '../lib/util'
const saveButtonOptions = {
button: true,
color: '#c198fb',
selected: { id: 'SAVE_IMAGE', name: 'Save Image' },
list: ['png', 'svg', 'open ↗'].map(id => ({ id, name: id.toUpperCase() }))
selected: { id: 'SAVE_IMAGE', name: 'Export Image' },
list: ['png', 'svg', 'copy embed', 'open ↗'].map(id => ({ id, name: id.toUpperCase() })),
itemWrapper: props => <ExportButton {...props} />
}
class Editor extends React.Component {
@ -52,7 +54,7 @@ class Editor extends React.Component {
online: true
}
this.save = this.save.bind(this)
this.export = this.export.bind(this)
this.upload = this.upload.bind(this)
this.updateSetting = this.updateSetting.bind(this)
this.updateCode = this.updateSetting.bind(this, 'code')
@ -70,7 +72,7 @@ class Editor extends React.Component {
}
async componentDidMount() {
const { asPath = '' } = this.props
const { asPath = '' } = this.props.router
const { query, pathname } = url.parse(asPath, true)
const path = escapeHtml(pathname.split('/').pop())
const queryParams = getQueryStringState(query)
@ -187,7 +189,11 @@ class Editor extends React.Component {
this.setState({ [key]: value })
}
save({ id: format = 'png' }) {
export({ id: format = 'png' }) {
if (format === 'copy embed') {
return
}
const link = document.createElement('a')
const timestamp = this.state.timestamp ? `_${formatTimestamp()}` : ''
@ -305,7 +311,7 @@ class Editor extends React.Component {
style={{ marginRight: '8px' }}
/>
)}
<Dropdown {...saveButtonOptions} onChange={this.save} />
<Dropdown {...saveButtonOptions} onChange={this.export} />
</div>
</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,21 +4,21 @@ import Reset from './style/Reset'
import Font from './style/Font'
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(
t => t.hasStylesheet !== false && LOCAL_STYLESHEETS.indexOf(t.id) < 0
)
/*
* 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
*/
export default function Meta() {
const onBrowser = typeof window !== 'undefined'
return (
<div className="meta">
<Head>
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" />
@ -40,17 +40,27 @@ export default function Meta() {
<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" />
</>
)
/*
* 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
*/
export default function Meta() {
const onBrowser = typeof window !== 'undefined'
return (
<div className="meta">
<Head>
<MetaTags />
<link rel="stylesheet" href="/static/react-crop.css" />
<link rel="manifest" href="/static/manifest.json" />
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/theme/seti.min.css"
/>
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/codemirror.min.css"
/>
<CodeMirrorLink />
<link
rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.39.2/theme/solarized.min.css"

@ -1,7 +1,40 @@
import React from 'react'
import CopyButton from './CopyButton'
import { COLORS } from '../lib/constants'
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">
{theme === 'bw' ? <ControlsBW /> : <Controls />}
<div className="window-title-container">
@ -12,6 +45,11 @@ export default ({ titleBar, theme, handleTitleBarChange }) => (
onChange={e => handleTitleBarChange(e.target.value)}
/>
</div>
{copyable && (
<div className="copy-button">
<CopyButton text={code}>{renderCopyButton}</CopyButton>
</div>
)}
<style jsx>
{`
div {
@ -40,6 +78,13 @@ export default ({ titleBar, theme, handleTitleBarChange }) => (
text-align: center;
font-size: 14px;
}
.copy-button {
cursor: pointer;
position: absolute;
top: 20px;
right: 16px;
}
`}
</style>
</div>

@ -1,9 +1,9 @@
import React from 'react'
export default () => (
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="8" viewBox="0 0 9 7">
export default ({ width = 9, height = 8, color = '#FFFFFF' }) => (
<svg xmlns="http://www.w3.org/2000/svg" width={width} height={height} viewBox="0 0 9 7">
<polygon
fill="#FFFFFF"
fill={color}
fillRule="evenodd"
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: 'es:exportSize' },
{ field: 'wm:watermark', type: 'bool' },
{ field: 'ts:timestamp', type: 'bool' }
{ field: 'ts:timestamp', type: 'bool' },
{ field: 'copy', type: 'bool' }
]
const reverseMappings = mappings.map(mapping =>

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

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

@ -40,6 +40,7 @@
"react-click-outside": "^3.0.0",
"react-codemirror2": "^5.1.0",
"react-color": "^2.13.8",
"react-copy-to-clipboard": "^5.0.1",
"react-dnd": "^5.0.0",
"react-dnd-html5-backend": "^5.0.0",
"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() {
return (
<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>
)
}

@ -1577,6 +1577,12 @@ copy-descriptor@^0.1.0:
version "0.1.1"
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:
version "1.2.7"
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"
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"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies:
@ -4712,6 +4718,13 @@ react-color@^2.13.8:
reactcss "^1.2.0"
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:
version "5.0.1"
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"
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:
version "1.0.2"
resolved "https://registry.yarnpkg.com/tohash/-/tohash-1.0.2.tgz#9e66e497da0cfd77ba85f9663065adf2d8c99981"

Loading…
Cancel
Save