├── .gitattributes
├── src
├── lib
│ ├── noop.js
│ ├── to-px.js
│ ├── is-ssr.js
│ ├── aspect-ratio-16-9.js
│ ├── is-dev.js
│ ├── is-editor.js
│ ├── is-mac.js
│ ├── debounce.js
│ ├── eq.js
│ ├── is-empty.js
│ ├── pick-by.js
│ ├── clipboard.js
│ ├── screenshot-url.js
│ ├── share-code.js
│ ├── notification.js
│ ├── compress-json.js
│ └── local-storage.js
├── components
│ ├── presets
│ │ ├── scope.js
│ │ ├── index.js
│ │ ├── simple.js
│ │ ├── microlink.js
│ │ ├── bytesandhumans.js
│ │ ├── article.js
│ │ ├── rauchg.js
│ │ ├── news.js
│ │ ├── vercell.js
│ │ ├── fauna.js
│ │ └── thepracticaldev.js
│ ├── if.js
│ ├── icons
│ │ ├── svg.js
│ │ ├── info.js
│ │ ├── theme.js
│ │ ├── keyboard.js
│ │ └── github.js
│ ├── link.js
│ ├── code.js
│ ├── button.js
│ ├── main.js
│ ├── button-icon.js
│ ├── choose.js
│ ├── lazy-image.js
│ ├── tabs.js
│ ├── overlay.js
│ ├── inline.macro.js
│ ├── searchable-select.js
│ ├── drag-bars.js
│ ├── live-editor.js
│ └── json-viewer.js
├── hooks
│ ├── use-loading.js
│ ├── use-key-bindings.js
│ ├── use-query-state.js
│ └── use-screenshot-url.js
├── pages
│ ├── _document.js
│ ├── _app.js
│ └── index.js
└── theme.js
├── .npmrc
├── README.md
├── .babelrc
├── jsconfig.json
├── next.config.js
├── .editorconfig
├── .gitignore
├── .travis.yml
├── LICENSE.md
├── package.json
└── CHANGELOG.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/src/lib/noop.js:
--------------------------------------------------------------------------------
1 | export default () => {}
2 |
--------------------------------------------------------------------------------
/src/lib/to-px.js:
--------------------------------------------------------------------------------
1 | export default n => `${n}px`
2 |
--------------------------------------------------------------------------------
/src/lib/is-ssr.js:
--------------------------------------------------------------------------------
1 | export default typeof window === 'undefined'
2 |
--------------------------------------------------------------------------------
/src/lib/aspect-ratio-16-9.js:
--------------------------------------------------------------------------------
1 | export default width => (width * 9) / 16
2 |
--------------------------------------------------------------------------------
/src/lib/is-dev.js:
--------------------------------------------------------------------------------
1 | export default process.env.NODE_ENV === 'development'
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | unsafe-perm=true
2 | save-prefix=~
3 | shrinkwrap=false
4 | save=false
5 |
--------------------------------------------------------------------------------
/src/lib/is-editor.js:
--------------------------------------------------------------------------------
1 | export default Router => Router.asPath.startsWith('/editor')
2 |
--------------------------------------------------------------------------------
/src/lib/is-mac.js:
--------------------------------------------------------------------------------
1 | import isSSR from './is-ssr'
2 | export default !isSSR && window.navigator.platform.match('Mac')
3 |
--------------------------------------------------------------------------------
/src/lib/debounce.js:
--------------------------------------------------------------------------------
1 | import { debounce } from 'throttle-debounce'
2 | export default (fn, ms = 600) => debounce(ms, fn)
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
--------------------------------------------------------------------------------
/src/lib/eq.js:
--------------------------------------------------------------------------------
1 | import stringify from 'fast-safe-stringify'
2 | export default (str1, str2) => stringify(str1) === stringify(str2)
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": ["macros", ["babel-plugin-styled-components", { "ssr": true }]]
4 | }
5 |
--------------------------------------------------------------------------------
/src/lib/is-empty.js:
--------------------------------------------------------------------------------
1 | export default obj =>
2 | [Object, Array].includes((obj || {}).constructor) &&
3 | !Object.entries(obj || {}).length
4 |
--------------------------------------------------------------------------------
/src/lib/pick-by.js:
--------------------------------------------------------------------------------
1 | export default obj => {
2 | Object.keys(obj).forEach(key => obj[key] == null && delete obj[key])
3 | return obj
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/presets/scope.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import Link from '../link'
3 |
4 | export * from 'theme-ui'
5 | export { Link, styled }
6 |
--------------------------------------------------------------------------------
/src/lib/clipboard.js:
--------------------------------------------------------------------------------
1 | export const read = async () => navigator.clipboard.readText()
2 | export const write = async text => navigator.clipboard.writeText(text)
3 | export default { read, write }
4 |
--------------------------------------------------------------------------------
/src/lib/screenshot-url.js:
--------------------------------------------------------------------------------
1 | import { getApiUrl } from '@microlink/mql'
2 |
3 | export default (url, opts) => {
4 | const [screenshotUrl] = getApiUrl(url, opts)
5 | return screenshotUrl
6 | }
7 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/package.json": ["./package.json"],
6 | "@/*": ["./src/*"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | experimental: {
3 | jsconfigPaths: true
4 | },
5 | exportTrailingSlash: true,
6 | exportPathMap: function () {
7 | return {
8 | '/': { page: '/' },
9 | '/editor': { page: '/' }
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/share-code.js:
--------------------------------------------------------------------------------
1 | export default url => `
2 |
3 |
4 |
5 | `
6 |
--------------------------------------------------------------------------------
/src/lib/notification.js:
--------------------------------------------------------------------------------
1 | import { createSnackbar } from '@snackbar/core'
2 |
3 | export default (text, props) =>
4 | createSnackbar(text, {
5 | actions: [
6 | {
7 | text: '×'
8 | }
9 | ],
10 | timeout: 2000,
11 | maxStack: 2,
12 | ...props
13 | })
14 |
--------------------------------------------------------------------------------
/src/lib/compress-json.js:
--------------------------------------------------------------------------------
1 | import Msgpack from 'msgpack5'
2 | import URLSafeBase64 from 'urlsafe-base64'
3 |
4 | const msgpack = new Msgpack()
5 |
6 | export const marshall = value => URLSafeBase64.encode(msgpack.encode(value))
7 |
8 | export const unmarshall = value => msgpack.decode(URLSafeBase64.decode(value))
9 |
--------------------------------------------------------------------------------
/src/components/if.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 |
3 | const If = props =>
4 | props.condition ? (props.render ? props.render() : props.children) : null
5 |
6 | If.propTypes = {
7 | condition: PropTypes.bool.isRequired,
8 | children: PropTypes.node,
9 | render: PropTypes.func
10 | }
11 |
12 | export default If
13 |
--------------------------------------------------------------------------------
/src/lib/local-storage.js:
--------------------------------------------------------------------------------
1 | /* global localStorage */
2 |
3 | import isSSR from './is-ssr'
4 | import noop from './noop'
5 |
6 | export const get = isSSR ? noop : window.localStorage.getItem.bind(localStorage)
7 | export const set = isSSR ? noop : window.localStorage.setItem.bind(localStorage)
8 |
9 | export default { get, set }
10 |
--------------------------------------------------------------------------------
/src/components/icons/svg.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Svg = styled('svg')``
4 |
5 | export default ({ color, size, ...props }) => (
6 |
14 | )
15 |
--------------------------------------------------------------------------------
/src/components/link.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | export default props => {
4 | useEffect(() => {
5 | const link = document.createElement('link')
6 | Object.keys(props).forEach(key => (link[key] = props[key]))
7 | document.head.appendChild(link)
8 | return () => document.head.removeChild(link)
9 | }, [])
10 | return false
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/code.js:
--------------------------------------------------------------------------------
1 | import { Box } from 'theme-ui'
2 |
3 | export default ({ sx, ...props }) => (
4 |
18 | )
19 |
--------------------------------------------------------------------------------
/src/components/button.js:
--------------------------------------------------------------------------------
1 | import { Button } from 'theme-ui'
2 |
3 | export default ({ sx, ...props }) => (
4 |
16 | )
17 |
--------------------------------------------------------------------------------
/src/components/main.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { Box } from 'theme-ui'
3 |
4 | const Main = styled(Box)`
5 | /* Take the remaining width */
6 | flex: 1;
7 |
8 | /* Make it scrollable */
9 | overflow: auto;
10 |
11 | display: flex;
12 | flex-direction: column;
13 | `
14 |
15 | Main.defaultProps = {
16 | as: 'main'
17 | }
18 |
19 | export default Main
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | max_line_length = 80
13 | indent_brace_style = 1TBS
14 | spaces_around_operators = true
15 | quote_type = auto
16 |
17 | [package.json]
18 | indent_style = space
19 | indent_size = 2
20 |
--------------------------------------------------------------------------------
/src/components/icons/info.js:
--------------------------------------------------------------------------------
1 | import Svg from './svg'
2 |
3 | export default ({ size = '24', ...props }) => (
4 |
16 | )
17 |
--------------------------------------------------------------------------------
/src/hooks/use-loading.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import isEditor from '@/lib/is-editor'
3 | import isEmpty from '@/lib/is-empty'
4 | import Router from 'next/router'
5 |
6 | export default () => {
7 | const [isLoading, setIsLoading] = useState(true)
8 |
9 | useEffect(() => {
10 | if (!isEditor(Router) && isEmpty(Router.query)) {
11 | return Router.push('/editor', '/editor', { shallow: true })
12 | }
13 | setIsLoading(false)
14 | }, [])
15 |
16 | return isLoading
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/icons/theme.js:
--------------------------------------------------------------------------------
1 | import Svg from './svg'
2 |
3 | export default ({ size = '24', ...props }) => (
4 |
13 | )
14 |
--------------------------------------------------------------------------------
/src/components/presets/index.js:
--------------------------------------------------------------------------------
1 | import vercell from './vercell'
2 | import simple from './simple'
3 | import article from './article'
4 | import rauchg from './rauchg'
5 | import bytesandhumans from './bytesandhumans'
6 | import microlink from './microlink'
7 | import thePracticalDev from './thepracticaldev'
8 | import fauna from './fauna'
9 | import news from './news'
10 |
11 | export default {
12 | article,
13 | bytesandhumans,
14 | fauna,
15 | microlink,
16 | news,
17 | rauchg,
18 | simple,
19 | thePracticalDev,
20 | vercell
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/icons/keyboard.js:
--------------------------------------------------------------------------------
1 | import Svg from './svg'
2 |
3 | export default ({ size = '24', ...props }) => (
4 |
16 | )
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ############################
2 | # npm
3 | ############################
4 | node_modules
5 | npm-debug.log
6 | .node_history
7 | yarn.lock
8 | package-lock.json
9 |
10 | ############################
11 | # tmp, editor & OS files
12 | ############################
13 | .tmp
14 | *.swo
15 | *.swp
16 | *.swn
17 | *.swm
18 | .DS_Store
19 | *#
20 | *~
21 | .idea
22 | *sublime*
23 | nbproject
24 |
25 | ############################
26 | # Tests
27 | ############################
28 | testApp
29 | coverage
30 | .nyc_output
31 |
32 | ############################
33 | # Other
34 | ############################
35 | .env
36 | .envrc
37 | .next
38 | .now
39 |
--------------------------------------------------------------------------------
/src/components/button-icon.js:
--------------------------------------------------------------------------------
1 | import themeBase from '@/theme'
2 | import styled from 'styled-components'
3 | import { Button } from 'theme-ui'
4 |
5 | const ButtonIcon = styled(Button)`
6 | display: flex;
7 | cursor: pointer;
8 | background: none;
9 | border: 0;
10 | outline: 0;
11 | padding: 0;
12 |
13 | svg {
14 | transition: fill ${themeBase.transition.medium},
15 | stroke ${themeBase.transition.medium};
16 | stroke: ${({ color }) => color};
17 | fill: ${({ color }) => color};
18 | }
19 |
20 | &:hover svg {
21 | stroke: ${({ hoverColor }) => hoverColor};
22 | fill: ${({ hoverColor }) => hoverColor};
23 | }
24 | `
25 |
26 | export default ButtonIcon
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - lts/*
5 |
6 | after_success: npm run coverage
7 |
8 | stages:
9 | - Test
10 | - name: Release
11 | if: branch = master AND commit_message !~ /(release|no-release)/
12 |
13 | jobs:
14 | include:
15 | - stage: Release
16 | install: npm install --no-package-lock
17 | before_deploy:
18 | - git config user.email ${GITHUB_EMAIL:-"travis@travis-ci.org"}
19 | - git config user.name ${GITHUB_USER:-"Travis CI"}
20 | - git remote set-url origin https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
21 | - git checkout master
22 | deploy:
23 | skip_cleanup: true
24 | provider: script
25 | script: npm run release
26 | on:
27 | branch: master
28 |
--------------------------------------------------------------------------------
/src/components/choose.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import If from './if'
4 |
5 | const Choose = props => {
6 | let when = null
7 | let otherwise = null
8 |
9 | React.Children.forEach(props.children, children => {
10 | if (children.props.condition === undefined) {
11 | otherwise = children
12 | } else if (!when && children.props.condition === true) {
13 | when = children
14 | }
15 | })
16 |
17 | return when || otherwise
18 | }
19 |
20 | Choose.propTypes = {
21 | children: PropTypes.node
22 | }
23 |
24 | Choose.When = If
25 |
26 | Choose.Otherwise = ({ render, children }) => (render ? render() : children)
27 |
28 | Choose.Otherwise.propTypes = {
29 | children: PropTypes.node,
30 | render: PropTypes.func
31 | }
32 |
33 | export default Choose
34 |
--------------------------------------------------------------------------------
/src/pages/_document.js:
--------------------------------------------------------------------------------
1 | import NextDocument from 'next/document'
2 | import { ServerStyleSheet } from 'styled-components'
3 |
4 | export default class Document extends NextDocument {
5 | static async getInitialProps (ctx) {
6 | const sheet = new ServerStyleSheet()
7 | const originalRenderPage = ctx.renderPage
8 |
9 | try {
10 | ctx.renderPage = () =>
11 | originalRenderPage({
12 | enhanceApp: App => props => sheet.collectStyles()
13 | })
14 |
15 | const initialProps = await NextDocument.getInitialProps(ctx)
16 | return {
17 | ...initialProps,
18 | styles: (
19 | <>
20 | {initialProps.styles}
21 | {sheet.getStyleElement()}
22 | >
23 | )
24 | }
25 | } finally {
26 | sheet.seal()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/icons/github.js:
--------------------------------------------------------------------------------
1 | import Svg from './svg'
2 |
3 | export default ({ size = '20', ...props }) => (
4 |
12 | )
13 |
--------------------------------------------------------------------------------
/src/components/lazy-image.js:
--------------------------------------------------------------------------------
1 | import { useState, createElement, useEffect } from 'react'
2 | import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'
3 | import { Image } from 'theme-ui'
4 | import noop from '@/lib/noop'
5 |
6 | const Placeholder = ({ sx, theme, ...props }) => (
7 |
8 |
9 |
10 | )
11 |
12 | const LazyImage = ({ onError, ...props }) => {
13 | const [isLoading, setLoading] = useState(true)
14 |
15 | useEffect(() => {
16 | const img = document.createElement('img')
17 | img.onerror = onError
18 | img.onload = () => {
19 | img.onload = null
20 | img.onerror = null
21 | setLoading(false)
22 | }
23 | img.src = props.src
24 | }, [])
25 |
26 | const Component = isLoading ? Placeholder : Image
27 | return createElement(Component, props)
28 | }
29 |
30 | LazyImage.defaultProps = {
31 | onError: noop
32 | }
33 |
34 | export default LazyImage
35 |
--------------------------------------------------------------------------------
/src/hooks/use-key-bindings.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import isEditor from '@/lib/is-editor'
3 | import Router from 'next/router'
4 |
5 | const isCtrl = e => e.metaKey || e.ctrlKey
6 |
7 | // https://keycode.info
8 | export default (initialKeyBindings = {}, eventListener = 'keydown') => {
9 | const [keyBindings] = useState(initialKeyBindings)
10 |
11 | useEffect(() => {
12 | if (!isEditor(Router)) return
13 |
14 | document.addEventListener(
15 | eventListener,
16 | event => {
17 | const { code } = event
18 | const keyBinding = keyBindings[code]
19 | if (keyBinding === undefined) return
20 | const condition = keyBinding.ctrl ? isCtrl(event) : true
21 | if (!condition) return
22 | event.preventDefault()
23 | keyBinding.fn(event)
24 | },
25 | false
26 | )
27 |
28 | return () =>
29 | Object.keys(keyBindings).forEach(keyBinding =>
30 | document.removeEventListener(eventListener, keyBindings[keyBinding])
31 | )
32 | }, [])
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/tabs.js:
--------------------------------------------------------------------------------
1 | import { Tab, Tabs as TabsBase, TabList, TabPanel } from 'react-tabs'
2 | import styled from 'styled-components'
3 | import themeBase from '@/theme'
4 |
5 | export const Tabs = styled(TabsBase)`
6 | .react-tabs {
7 | -webkit-tap-highlight-color: transparent;
8 | }
9 |
10 | .react-tabs__tab-list {
11 | border-bottom: 1px solid ${props => props.theme.borderColor};
12 | margin: 0 0 ${themeBase.space[3]};
13 | padding: 0;
14 | }
15 |
16 | .react-tabs__tab {
17 | display: inline-block;
18 | border: 1px solid transparent;
19 | border-bottom: none;
20 | bottom: -1px;
21 | position: relative;
22 | list-style: none;
23 | padding: 6px 12px;
24 | cursor: pointer;
25 | }
26 |
27 | .react-tabs__tab--selected {
28 | border-bottom: 2px solid ${props => props.theme.color};
29 | color: ${props => props.theme.color};
30 | outline: 0;
31 | }
32 |
33 | .react-tabs__tab-panel {
34 | display: none;
35 | min-height: 130px;
36 | }
37 |
38 | .react-tabs__tab-panel--selected {
39 | display: block;
40 | }
41 | `
42 | export { Tab, TabList, TabPanel }
43 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright © 2020 microlink.io (microlink.io)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/hooks/use-query-state.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { flatten, unflatten } from 'flat'
3 | import { encode } from 'qss'
4 | import isSSR from '@/lib/is-ssr'
5 | import eq from '@/lib/eq'
6 |
7 | const fromLocation = isSSR
8 | ? () => ({})
9 | : () => {
10 | const urlObj = new URL(window.location)
11 | const query = Object.fromEntries(urlObj.searchParams.entries())
12 | const decodeQuery = Object.keys(query).reduce(
13 | (acc, key) => ({ ...acc, [key]: decodeURIComponent(query[key]) }),
14 | {}
15 | )
16 | return decodeQuery
17 | }
18 |
19 | const condition = isSSR ? [] : [window.location.search]
20 |
21 | export default () => {
22 | const [query, setQuery] = useState(unflatten(fromLocation()))
23 |
24 | useEffect(() => {
25 | const newQuery = fromLocation()
26 | if (!eq(query, newQuery)) setQuery(newQuery)
27 | }, condition)
28 |
29 | const set = (obj, { replace = false } = {}) => {
30 | const newQuery = flatten(replace ? obj : { ...fromLocation(), ...obj })
31 | setQuery(newQuery)
32 | window.history.pushState(
33 | {},
34 | '',
35 | `${window.location.pathname}?${encode(newQuery)}`
36 | )
37 | }
38 |
39 | return [query, set]
40 | }
41 |
--------------------------------------------------------------------------------
/src/hooks/use-screenshot-url.js:
--------------------------------------------------------------------------------
1 | import getScreenshotUrl from '@/lib/screenshot-url'
2 | import { useState, useEffect } from 'react'
3 | import isDev from '@/lib/is-dev'
4 |
5 | const shortenUrl = isDev
6 | ? 'http://localhost:3000/?adblock=false&element=%23screenshot&embed=screenshot.url&meta=false&screenshot&waitUntil.0=load&waitUntil.1=networkidle0&url='
7 | : 'https://i.microlink.io/'
8 |
9 | const getUrl = () => {
10 | const urlObj = new URL(window.location)
11 | urlObj.pathname = ''
12 | return urlObj.toString()
13 | }
14 |
15 | const getCardUrl = ({ endpoint }) => {
16 | if (!isDev && !endpoint) {
17 | return `${shortenUrl}${encodeURIComponent(getUrl())}`
18 | }
19 | return getScreenshotUrl(getUrl(), {
20 | force: !!isDev,
21 | endpoint: isDev ? 'http://localhost:3000' : endpoint,
22 | adblock: false,
23 | element: '#screenshot',
24 | embed: 'screenshot.url',
25 | meta: false,
26 | screenshot: true,
27 | waitUntil: ['load', 'networkidle0']
28 | })
29 | }
30 |
31 | export default queryVariables => {
32 | const [screenshotUrl, setScreenshotUrl] = useState('')
33 |
34 | const sync = queryVariables => setScreenshotUrl(getCardUrl(queryVariables))
35 |
36 | useEffect(() => sync(queryVariables), [])
37 |
38 | return [screenshotUrl, sync]
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/presets/simple.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Link, Flex, Text } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
13 |
22 |
30 |
38 |
39 | >
40 |
41 | )
42 |
43 | const query = {
44 | headline: 'Add your headline',
45 | caption: 'Add your caption',
46 | bg: 'black',
47 | color: 'white'
48 | }
49 |
50 | export default {
51 | name: 'simple',
52 | code,
53 | query
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/overlay.js:
--------------------------------------------------------------------------------
1 | import { Box } from 'theme-ui'
2 |
3 | export default ({
4 | backgroundColor,
5 | color,
6 | children,
7 | onClose,
8 | fullWidth,
9 | isOpen,
10 | ...props
11 | }) => {
12 | const onDismiss = event => {
13 | if (event.target.dataset.overlayAction === 'close') {
14 | onClose(event)
15 | }
16 | }
17 |
18 | return (
19 |
38 |
54 | {children}
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/inline.macro.js:
--------------------------------------------------------------------------------
1 | const { createMacro } = require('babel-plugin-macros')
2 | const prettier = require('prettier/standalone')
3 | const parserBabel = require('prettier/parser-babel')
4 |
5 | const BABEL_OPTS = {
6 | parser: 'babel',
7 | plugins: [parserBabel]
8 | }
9 |
10 | /**
11 | * https://prettier.io/docs/en/options.html
12 | */
13 | const PRETTIER_CONFIG = {
14 | semi: true,
15 | singleQuote: true,
16 | jsxSingleQuote: true,
17 | printWidth: 30,
18 | tabWidth: 2
19 | }
20 |
21 | const toPrettier = ({ referencePath, babel, prettierOpts }) => {
22 | const children = referencePath.parentPath.parentPath.get('children')
23 | const body = children[1].getSource()
24 | let formattedBody = prettier.format(body, prettierOpts)
25 | if (formattedBody.endsWith(';\n')) {
26 | formattedBody = formattedBody.substring(0, formattedBody.length - 2)
27 | }
28 | referencePath.parentPath.parentPath.replaceWith(
29 | babel.types.stringLiteral(formattedBody)
30 | )
31 | }
32 |
33 | const createInlineJSXMacro = prettierOpts => ({ references, babel }) => {
34 | const { default: defaultImport = [] } = references
35 |
36 | defaultImport.forEach(referencePath => {
37 | if (referencePath.parentPath.type === 'JSXOpeningElement') {
38 | // prettify code inside jsx
39 | toPrettier({ referencePath, babel, prettierOpts })
40 | }
41 | })
42 | }
43 |
44 | const create = prettierOpts => createMacro(createInlineJSXMacro(prettierOpts))
45 |
46 | module.exports = create({ ...BABEL_OPTS, ...PRETTIER_CONFIG })
47 |
--------------------------------------------------------------------------------
/src/components/presets/microlink.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Image, Box, Link, Flex, Text } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
13 |
20 |
21 |
25 |
33 |
46 |
47 |
48 | >
49 |
50 | )
51 |
52 | const query = {
53 | color: '#313b53',
54 | bg: '#313B53',
55 | title: 'Microlink Cards',
56 | logo: 'https://cdn.microlink.io/logo/logo.svg'
57 | }
58 |
59 | export default { name: 'microlink', code, query }
60 |
--------------------------------------------------------------------------------
/src/components/presets/bytesandhumans.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Link, Box, Flex, Text } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
13 |
23 |
29 |
42 |
43 |
44 | >
45 |
46 | )
47 |
48 | const query = {
49 | title: 'Paradigmas con emoji',
50 | image: 'https://bytesandhumans.netlify.app/assets/img/posts/paradigmas.png',
51 | textColor: 'black',
52 | bgColor: 'white',
53 | primaryColor: '#85FFE0'
54 | }
55 |
56 | export default { name: 'bytesandhumans', code, query }
57 |
--------------------------------------------------------------------------------
/src/components/presets/article.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Link, Box, Text } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
13 |
24 |
32 |
41 |
51 |
52 | >
53 |
54 | )
55 |
56 | const query = {
57 | foo: 'bar'
58 | }
59 |
60 | export default { name: 'article', code, query }
61 |
--------------------------------------------------------------------------------
/src/components/presets/rauchg.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Image, Box, Flex, Text } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
13 |
18 |
19 |
29 | {query.domain}
30 |
31 |
32 |
33 |
41 |
54 |
55 | >
56 |
57 | )
58 |
59 | const query = {
60 | bg: 'white',
61 | color: 'black',
62 | logo: 'https://svgur.com/i/KBR.svg',
63 | domain: 'rauchg.com',
64 | title: '2020 in Review'
65 | }
66 |
67 | export default { name: 'rauchg', code, query }
68 |
--------------------------------------------------------------------------------
/src/components/searchable-select.js:
--------------------------------------------------------------------------------
1 | import SearchableSelect from 'react-select'
2 | import * as polished from 'polished'
3 | import themeBase from '@/theme'
4 |
5 | export default ({ bg, color, ...props }) => {
6 | const secondaryColor = polished.lighten(0.1, bg)
7 | const theme = () => {
8 | return {
9 | borderRadius: 4,
10 | colors: {
11 | primary: color,
12 | primary75: secondaryColor,
13 | primary50: secondaryColor,
14 | primary25: secondaryColor,
15 | danger: '#DE350B',
16 | dangerLight: '#FFBDAD',
17 | neutral0: bg,
18 | neutral5: color,
19 | neutral10: color,
20 | neutral20: color,
21 | neutral30: color,
22 | neutral40: color,
23 | neutral50: color,
24 | neutral60: color,
25 | neutral70: color,
26 | neutral80: color,
27 | neutral90: color
28 | },
29 | spacing: { baseUnit: 4, controlHeight: 38, menuGutter: 8 }
30 | }
31 | }
32 |
33 | const fontStyle = {
34 | fontFamily: themeBase.fonts.sans,
35 | fontSize: themeBase.fontSizes[1]
36 | }
37 |
38 | const styles = {
39 | singleValue: provided => ({
40 | ...provided,
41 | ...fontStyle
42 | }),
43 | valueContainer: provided => ({
44 | ...provided,
45 | padding: '2px 8px'
46 | }),
47 | menu: provided => ({
48 | ...provided,
49 | ...fontStyle
50 | }),
51 | indicatorSeparator: () => ({ display: 'none' }),
52 | control: (provided, { isFocused }) => ({
53 | ...provided,
54 | opacity: isFocused ? 1 : 0.75,
55 | cursor: 'pointer',
56 | boxShadow: 'none'
57 | })
58 | }
59 |
60 | return
61 | }
62 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | import nightOwlLight from 'prism-react-renderer/themes/nightOwlLight'
2 | import palenight from 'prism-react-renderer/themes/palenight'
3 | import nightOwl from 'prism-react-renderer/themes/nightOwl'
4 | import ultramin from 'prism-react-renderer/themes/ultramin'
5 | import dracula from 'prism-react-renderer/themes/dracula'
6 | import github from 'prism-react-renderer/themes/github'
7 |
8 | import toPx from '@/lib/to-px'
9 |
10 | const defaultTheme = github
11 |
12 | export const editorTheme = {
13 | default: defaultTheme,
14 | ultramin,
15 | dracula,
16 | nightOwl,
17 | nightOwlLight,
18 | palenight
19 | }
20 |
21 | const speed = {
22 | quickly: 150,
23 | normal: 300,
24 | slowly: 450
25 | }
26 |
27 | export const theme = {
28 | borders: [0, '1px solid'],
29 | colors: {
30 | modes: editorTheme
31 | },
32 | fonts: {
33 | sans:
34 | 'Inter, -apple-system, BlinkMacSystemFont, "Helvetica Neue", "Segoe UI", Oxygen, Ubuntu, Cantarell, "Open Sans", sans-serif',
35 | mono:
36 | '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace'
37 | },
38 | speed,
39 | transition: {
40 | short: `${speed.quickly}ms cubic-bezier(.25,.8,.25,1)`,
41 | medium: `${speed.normal}ms cubic-bezier(.25,.8,.25,1)`,
42 | long: `${speed.slowly}ms cubic-bezier(.4, 0, .2, 1)`
43 | },
44 | breakpoints: [576, 768, 991, 1220].map(toPx),
45 | fontSizes: [12, 14, 16, 20, 24, 32, 48, 64, 96].map(toPx),
46 | fontWeights: {
47 | lighter: 100,
48 | light: 200,
49 | normal: 400,
50 | regular: 500,
51 | bold: 600
52 | },
53 | space: [0, 4, 8, 16, 32, 64, 128, 256, 512].map(toPx),
54 | styles: {
55 | root: {
56 | fontFamily: 'sans',
57 | lineHeight: 1.5,
58 | margin: 0,
59 | overflow: 'hidden'
60 | }
61 | }
62 | }
63 |
64 | export default theme
65 |
--------------------------------------------------------------------------------
/src/components/drag-bars.js:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 | import styled, { css } from 'styled-components'
3 |
4 | const Dragger = styled('div').attrs(({ isDrag, isHorizontal }) => ({
5 | style: {
6 | [!isHorizontal ? 'width' : 'height']: !isDrag ? '10px' : '100vw',
7 | position: !isDrag ? 'absolute' : 'fixed',
8 | zIndex: !isDrag ? 'initial' : 9999,
9 | transform: !isDrag ? `translate${!isHorizontal ? 'X' : 'Y'}(-50%)` : 'none'
10 | }
11 | }))`
12 | left: 0;
13 | top: 0;
14 | bottom: 0;
15 | cursor: ew-resize;
16 | will-change: transform, position, width, height, z-index;
17 |
18 | ${({ isHorizontal }) => css`
19 | ${!isHorizontal ? 'bottom' : 'right'}: 0;
20 | cursor: ${!isHorizontal ? 'ew-resize' : 'ns-resize'};
21 | `}
22 | `
23 |
24 | const DragBar = ({ isHorizontal = false, onDrag = () => {} }) => {
25 | const [isDrag, setIsDrag] = useState(false)
26 | const onMouseMove = useCallback(e => {
27 | const vw = Math.max(
28 | document.documentElement[!isHorizontal ? 'clientWidth' : 'clientHeight'],
29 | window[!isHorizontal ? 'innerWidth' : 'innerHeight'] || 0
30 | )
31 | const cursorPos = e[!isHorizontal ? 'clientX' : 'clientY']
32 | const percent = Math.round(100 - (cursorPos / vw) * 100)
33 | return onDrag(`${percent}%`)
34 | }, [])
35 |
36 | const addListener = useCallback(() => {
37 | setIsDrag(true)
38 | return document.addEventListener('mousemove', onMouseMove)
39 | }, [onMouseMove])
40 |
41 | const removeListener = useCallback(() => {
42 | setIsDrag(false)
43 | return document.removeEventListener('mousemove', onMouseMove)
44 | }, [onMouseMove])
45 |
46 | return (
47 |
54 | )
55 | }
56 |
57 | export const VerticalDragBar = props =>
58 |
59 | export const HorizontalDragBar = props =>
60 |
--------------------------------------------------------------------------------
/src/components/presets/news.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Flex, Link, Box, Text, Image } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
13 |
22 |
35 |
36 |
42 |
43 | wellpaid.io
44 |
45 |
46 |
47 |
48 |
56 | What NOT to do when remote working
57 |
58 |
59 | Lorem ipsum dolor sit amet consectetur adipisicing elit.
60 | Temporibus consectetur totam sequi ducimus, laboriosam voluptatum,
61 | expedita fuga hic quibusdam facilis quasi architecto nobis
62 |
63 |
64 |
65 |
66 | >
67 |
68 | )
69 |
70 | const query = {
71 | foo: 'bar'
72 | }
73 |
74 | export default { name: 'news', code, query }
75 |
--------------------------------------------------------------------------------
/src/components/presets/vercell.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Image, Link, Box, Text } from './scope'
5 |
6 | const code = (
7 |
8 |
25 |
29 |
30 |
41 |
51 |
52 |
53 | )
54 |
55 | const query = {
56 | headline: 'Serverless Deployments',
57 | caption: 'with Vercell',
58 | logo: 'vercell',
59 | theme: 'dark',
60 | bg: {
61 | light: 'white',
62 | dark: 'black'
63 | },
64 | color: {
65 | light: 'black',
66 | dark: 'white'
67 | },
68 | radial: {
69 | light: 'lightgray',
70 | dark: 'dimgray'
71 | },
72 | logos: {
73 | light: {
74 | vercell:
75 | 'https://assets.zeit.co/image/upload/front/assets/design/zeit-black-triangle.svg',
76 | next:
77 | 'https://assets.zeit.co/image/upload/front/assets/design/nextjs-black-logo.svg',
78 | hyper:
79 | 'https://assets.zeit.co/image/upload/front/assets/design/hyper-color-logo.svg'
80 | },
81 | dark: {
82 | vercell:
83 | 'https://assets.zeit.co/image/upload/front/assets/design/zeit-white-triangle.svg',
84 | next:
85 | 'https://assets.zeit.co/image/upload/front/assets/design/nextjs-white-logo.svg',
86 | hyper:
87 | 'https://assets.zeit.co/image/upload/front/assets/design/hyper-bw-logo.svg'
88 | }
89 | }
90 | }
91 |
92 | export default {
93 | name: 'Vercell',
94 | code,
95 | query
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/live-editor.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import theme from '@/theme'
3 | import * as scope from '@/components/presets/scope'
4 |
5 | import {
6 | LiveProvider as BaseProvider,
7 | LiveEditor as BaseEditor,
8 | LiveError as BaseError,
9 | LivePreview as BasePreview
10 | } from 'react-live'
11 |
12 | const BASE_HEIGHT = 441
13 | const BASE_WIDTH = 843
14 | const ratios = [1, 1, 1, 1]
15 |
16 | const LivePreviewWrapper = styled('div')`
17 | cursor: pointer;
18 | height: 100%;
19 | width: 100%;
20 | margin: auto;
21 | padding: ${({ isEditor }) => (isEditor ? theme.space[3] : 0)};
22 |
23 | ${({ isThumbnail }) =>
24 | isThumbnail &&
25 | `
26 | height: ${props => props.thumbnailHeight};
27 | width: ${props => props.thumbnailWidth};
28 | zoom: 0.5;
29 | `}
30 |
31 | ${({ isEditor, isThumbnail }) =>
32 | !isEditor &&
33 | !isThumbnail &&
34 | `
35 | ${ratios.reduce(
36 | (acc, ratio, index) =>
37 | acc +
38 | `
39 | @media screen and (min-width: ${theme.breakpoints[index]}) {
40 | height: ${ratio * BASE_HEIGHT}px;
41 | width: ${ratio * BASE_WIDTH}px;
42 | }
43 | `,
44 | ''
45 | )}
46 | `}
47 | `
48 |
49 | LivePreviewWrapper.defaultProps = {
50 | id: 'screenshot'
51 | }
52 |
53 | export const LiveError = styled(BaseError)`
54 | position: relative;
55 | margin: auto;
56 | padding: ${theme.space[3]};
57 | display: block;
58 | background: #ff5555;
59 | color: #f8f8f2;
60 | white-space: pre-wrap;
61 | text-align: left;
62 | font-size: ${theme.fontSizes[2]};
63 | font-family: ${theme.fonts.mono};
64 | bottom: 0;
65 | width: calc(100% - ${theme.space[5]});
66 |
67 | @media screen and (min-width: ${theme.breakpoints[2]}) {
68 | position: absolute;
69 | width: 70%;
70 | }
71 | `
72 |
73 | export const LivePreview = styled(BasePreview)`
74 | > div {
75 | height: inherit;
76 | width: inherit;
77 | }
78 | `
79 |
80 | LivePreview.defaultProps = {
81 | Component: LivePreviewWrapper
82 | }
83 |
84 | const LiveProviderBase = styled(BaseProvider)``
85 |
86 | LiveProviderBase.defaultProps = {
87 | scope,
88 | theme: undefined,
89 | noInline: false
90 | }
91 |
92 | export const LiveProvider = ({ queryVariables: query, ...props }) => {
93 | const extendedScope = { ...scope, query }
94 | return
95 | }
96 |
97 | export const LiveEditor = styled(BaseEditor)`
98 | pre,
99 | textarea {
100 | padding: 0 !important;
101 | font-family: ${theme.fonts.mono} !important;
102 | font-weight: ${theme.fontWeights.light} !important;
103 | }
104 | textarea:focus {
105 | outline: none;
106 | }
107 | `
108 |
--------------------------------------------------------------------------------
/src/components/presets/fauna.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Flex, Link, Box, Text, Image } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
13 |
20 |
27 |
31 |
36 |
37 |
46 | {query.title}
47 |
48 |
49 |
62 | Podcast
63 |
64 |
72 | with Fauna
73 |
74 |
75 |
76 |
77 |
84 | Disscenting Static.fun
85 |
86 |
94 | Allen Hai
95 |
96 | Software Engineer @ ZEIT
97 |
98 |
99 |
100 |
101 |
102 | >
103 |
104 | )
105 |
106 | const query = {
107 | title: 'Build_ Fearlessly'
108 | }
109 |
110 | export default { name: 'fauna', code, query }
111 |
--------------------------------------------------------------------------------
/src/components/json-viewer.js:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic'
2 | import styled from 'styled-components'
3 | import * as polished from 'polished'
4 |
5 | const DynamicReactJson = dynamic(import('react-json-view'), { ssr: false })
6 |
7 | const ThemeWrapper = styled.div`
8 | div.key-modal-request > div > span {
9 | background: transparent !important;
10 | }
11 |
12 | textarea,
13 | input {
14 | border-radius: 4px;
15 | outline: 0;
16 | background: transparent !important;
17 | border: 1px solid ${props => props.color} !important;
18 | color: ${props => props.color} !important;
19 | }
20 | .key-modal-submit {
21 | position: relative;
22 | right: 4px;
23 | }
24 | .key-modal-submit svg {
25 | color: ${props => props.addColor} !important;
26 | }
27 |
28 | .click-to-add-icon {
29 | position: relative;
30 | top: 6px;
31 | }
32 |
33 | .click-to-edit,
34 | .click-to-remove {
35 | position: relative;
36 | top: 4px;
37 | }
38 |
39 | .click-to-add-icon svg {
40 | color: ${props => props.plusColor} !important;
41 | }
42 |
43 | .edit-check svg {
44 | color: ${props => props.editColor} !important;
45 | }
46 |
47 | .edit-cancel svg,
48 | .click-to-remove svg {
49 | color: ${props => props.removeColor} !important;
50 | }
51 | `
52 |
53 | export default ({ onChange, children, theme, ...props }) => {
54 | const stringColor = theme.styles.find(item => item.types.includes('string'))
55 | .style.color
56 | const secondaryColor = polished.lighten(0.1, theme.plain.backgroundColor)
57 | const terciaryColor = polished.rgba(theme.plain.color, 0.6)
58 | const removeColor = theme.styles.find(item => item.types.includes('tag'))
59 | .style.color
60 | const editColor = theme.styles.find(item => item.types.includes('operator'))
61 | const plusColor = theme.styles.find(item => item.types.includes('attr-name'))
62 | .style.color
63 |
64 | return (
65 |
72 | {
80 | onChange(value.updated_src)
81 | return true
82 | }}
83 | onAdd={value => {
84 | onChange(value.updated_src)
85 | return true
86 | }}
87 | onDelete={value => {
88 | onChange(value.updated_src)
89 | return true
90 | }}
91 | theme={{
92 | base00: theme.plain.backgroundColor,
93 | base01: theme.plain.color,
94 | base02: terciaryColor, // line
95 | base03: secondaryColor,
96 | base04: secondaryColor,
97 | base05: secondaryColor,
98 | base06: theme.plain.color,
99 | base07: stringColor, // key
100 | base08: theme.plain.color,
101 | base09: stringColor, // value, close bg
102 | base0A: theme.plain.color,
103 | base0B: theme.plain.color,
104 | base0C: theme.plain.color,
105 | base0D: terciaryColor, // arrow
106 | base0E: theme.plain.color,
107 | base0F: theme.plain.color
108 | }}
109 | />
110 |
111 | )
112 | }
113 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "microlink-cards",
3 | "description": "The easiest way to generate dynamic social images at scale.",
4 | "homepage": "https://github.com/microlinkhq/cards",
5 | "version": "0.0.36",
6 | "author": {
7 | "email": "hello@microlink.io",
8 | "name": "microlink.io",
9 | "url": "https://microlink.io"
10 | },
11 | "contributors": [
12 | {
13 | "name": "Kiko Beats",
14 | "email": "josefrancisco.verdu@gmail.com"
15 | }
16 | ],
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/microlinkhq/cards.git"
20 | },
21 | "bugs": {
22 | "url": "https://github.com/microlinkhq/cards/issues"
23 | },
24 | "keywords": [
25 | "cards",
26 | "img",
27 | "meta",
28 | "metatags",
29 | "microlink",
30 | "og-image",
31 | "opengraph",
32 | "seo",
33 | "share"
34 | ],
35 | "dependencies": {
36 | "@mdx-js/loader": "~1.5.8",
37 | "@microlink/mql": "~0.6.11",
38 | "@next/mdx": "~9.3.4",
39 | "@snackbar/core": "~1.7.0",
40 | "babel-plugin-macros": "~2.8.0",
41 | "cycled": "~1.1.0",
42 | "deep-object-diff": "~1.1.0",
43 | "fast-safe-stringify": "~2.0.7",
44 | "msgpack5": "~4.2.1",
45 | "next": "~9.3.4",
46 | "polished": "~3.5.1",
47 | "prettier": "~2.0.4",
48 | "prism-react-renderer": "~1.0.2",
49 | "qss": "~2.0.3",
50 | "react": "~16.13.1",
51 | "react-dom": "~16.13.1",
52 | "react-json-view": "~1.19.1",
53 | "react-live": "~2.2.2",
54 | "react-loading-skeleton": "~2.0.1",
55 | "react-select": "~3.1.0",
56 | "react-tabs": "~3.1.0",
57 | "styled-components": "~5.1.0",
58 | "theme-ui": "~0.3.1",
59 | "throttle-debounce": "~2.1.0",
60 | "tickedoff": "~1.0.2",
61 | "urlsafe-base64": "~1.0.0"
62 | },
63 | "devDependencies": {
64 | "@commitlint/cli": "latest",
65 | "@commitlint/config-conventional": "latest",
66 | "babel-plugin-styled-components": "latest",
67 | "conventional-github-releaser": "latest",
68 | "finepack": "latest",
69 | "git-authors-cli": "latest",
70 | "git-dirty": "latest",
71 | "husky": "latest",
72 | "lint-staged": "latest",
73 | "npm-check-updates": "latest",
74 | "prettier-standard": "latest",
75 | "standard": "latest",
76 | "standard-markdown": "latest",
77 | "standard-version": "latest"
78 | },
79 | "engines": {
80 | "node": "12.x"
81 | },
82 | "scripts": {
83 | "build": "NODE_ENV=production next build",
84 | "clean": "rm -rf node_modules",
85 | "contributors": "(git-authors-cli && finepack && git add package.json && git commit -m 'build: contributors' --no-verify) || true",
86 | "dev": "NODE_ENV=development next -p 3001",
87 | "export": "next export",
88 | "lint": "standard-markdown README.md && standard",
89 | "postrelease": "npm run release:tags && npm run release:github",
90 | "prerelease": "npm run update:check && npm run contributors",
91 | "pretest": "npm run lint",
92 | "release": "standard-version -a",
93 | "release:github": "conventional-github-releaser -p angular",
94 | "release:tags": "git push --follow-tags origin HEAD:master",
95 | "start": "next start",
96 | "test": "exit 0",
97 | "update": "ncu -u",
98 | "update:check": "ncu -- --error-level 2"
99 | },
100 | "private": true,
101 | "license": "MIT",
102 | "commitlint": {
103 | "extends": [
104 | "@commitlint/config-conventional"
105 | ]
106 | },
107 | "husky": {
108 | "hooks": {
109 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
110 | "pre-commit": "lint-staged"
111 | }
112 | },
113 | "lint-staged": {
114 | "package.json": [
115 | "finepack"
116 | ],
117 | "*.js": [
118 | "prettier-standard"
119 | ],
120 | "*.md": [
121 | "standard-markdown"
122 | ]
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/components/presets/thepracticaldev.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import Inline from '../inline.macro'
4 | import { Link, Flex, Image, Box, Text } from './scope'
5 |
6 | const code = (
7 |
8 | <>
9 |
16 |
29 |
33 |
45 |
50 |
55 |
65 |
73 |
82 |
90 |
91 |
92 |
98 | {query.logos.map((image, index) => {
99 | return (
100 |
110 | )
111 | })}
112 |
113 |
114 |
115 |
116 | >
117 |
118 | )
119 |
120 | const query = {
121 | title: 'Learn Web Development for Free',
122 | avatar: 'https://kikobeats.com/images/avatar-glitch.jpg',
123 | author: 'Kiko Beats',
124 | date: '29 Apr',
125 | bg: '#EAF1F7',
126 | color: '#000',
127 | logos: [
128 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/99/Unofficial_JavaScript_logo_2.svg/490px-Unofficial_JavaScript_logo_2.svg.png',
129 | 'https://miro.medium.com/max/1024/1*dOizsWycMALIUfqbpNvaMQ.png',
130 | 'https://vuejs.org/images/logo.png'
131 | ]
132 | }
133 |
134 | export default { name: 'thepracticaldev', code, query }
135 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [0.0.36](https://github.com/microlinkhq/cards/compare/v0.0.35...v0.0.36) (2020-04-21)
6 |
7 | ### [0.0.35](https://github.com/microlinkhq/cards/compare/v0.0.34...v0.0.35) (2020-04-21)
8 |
9 | ### [0.0.34](https://github.com/microlinkhq/cards/compare/v0.0.33...v0.0.34) (2020-04-21)
10 |
11 | ### [0.0.33](https://github.com/microlinkhq/cards/compare/v0.0.32...v0.0.33) (2020-04-21)
12 |
13 | ### [0.0.32](https://github.com/microlinkhq/cards/compare/v0.0.31...v0.0.32) (2020-04-21)
14 |
15 | ### [0.0.31](https://github.com/microlinkhq/cards/compare/v0.0.30...v0.0.31) (2020-04-21)
16 |
17 |
18 | ### Bug Fixes
19 |
20 | * remove debounce over store ([9a0af74](https://github.com/microlinkhq/cards/commit/9a0af74420419171acb3e0b8d0f39f693382ccaa))
21 |
22 | ### [0.0.30](https://github.com/microlinkhq/cards/compare/v0.0.29...v0.0.30) (2020-04-21)
23 |
24 |
25 | ### Bug Fixes
26 |
27 | * select options font style ([038f1cb](https://github.com/microlinkhq/cards/commit/038f1cb5a5aec2b144249f95e5852c7505ef7e63))
28 |
29 | ### [0.0.29](https://github.com/microlinkhq/cards/compare/v0.0.28...v0.0.29) (2020-04-19)
30 |
31 | ### [0.0.28](https://github.com/microlinkhq/cards/compare/v0.0.27...v0.0.28) (2020-04-18)
32 |
33 | ### [0.0.27](https://github.com/microlinkhq/cards/compare/v0.0.26...v0.0.27) (2020-04-18)
34 |
35 |
36 | ### Bug Fixes
37 |
38 | * typo ([fdc74d0](https://github.com/microlinkhq/cards/commit/fdc74d01d35e187cc4a83d5a05324704d05c9f01))
39 |
40 | ### [0.0.26](https://github.com/microlinkhq/cards/compare/v0.0.25...v0.0.26) (2020-04-18)
41 |
42 | ### [0.0.25](https://github.com/microlinkhq/cards/compare/v0.0.24...v0.0.25) (2020-04-18)
43 |
44 | ### [0.0.24](https://github.com/microlinkhq/cards/compare/v0.0.23...v0.0.24) (2020-04-17)
45 |
46 | ### [0.0.23](https://github.com/microlinkhq/cards/compare/v0.0.22...v0.0.23) (2020-04-17)
47 |
48 |
49 | ### Bug Fixes
50 |
51 | * remove slash ([ace6946](https://github.com/microlinkhq/cards/commit/ace694629452333c0dfba6117e6a78f71858f1b5))
52 | * use the correct endpoint ([d9012c0](https://github.com/microlinkhq/cards/commit/d9012c023057f2b66f70a0a901ff34f704521e66))
53 |
54 | ### [0.0.22](https://github.com/microlinkhq/cards/compare/v0.0.21...v0.0.22) (2020-04-17)
55 |
56 |
57 | ### Bug Fixes
58 |
59 | * use shorten url alias ([48747ca](https://github.com/microlinkhq/cards/commit/48747caa53d436119ce88c33bc82bbdc3692f448))
60 |
61 | ### [0.0.21](https://github.com/microlinkhq/cards/compare/v0.0.20...v0.0.21) (2020-04-17)
62 |
63 |
64 | ### Bug Fixes
65 |
66 | * decode url ([bc279d0](https://github.com/microlinkhq/cards/commit/bc279d02e125dac6c3dbd2572db4db2a220ad4b1))
67 | * linter ([18869f3](https://github.com/microlinkhq/cards/commit/18869f3ad4b453b6c394260e1e74259af1190d73))
68 | * query short url ([e9542d7](https://github.com/microlinkhq/cards/commit/e9542d7d6deb1e014849bcaa0d35b85f71dcbbf0))
69 | * use query color ([c03bead](https://github.com/microlinkhq/cards/commit/c03bead39b76c7e780a307a2a22676a8d0a85e74))
70 |
71 | ### [0.0.20](https://github.com/microlinkhq/cards/compare/v0.0.19...v0.0.20) (2020-04-17)
72 |
73 | ### [0.0.19](https://github.com/microlinkhq/cards/compare/v0.0.18...v0.0.19) (2020-04-16)
74 |
75 |
76 | ### Bug Fixes
77 |
78 | * call set query ([e0a1bf1](https://github.com/microlinkhq/cards/commit/e0a1bf1f5adbf37c83d4776a98c465a03f0f031a))
79 |
80 | ### [0.0.18](https://github.com/microlinkhq/cards/compare/v0.0.17...v0.0.18) (2020-04-16)
81 |
82 |
83 | ### Bug Fixes
84 |
85 | * encode properly ([f5cf54b](https://github.com/microlinkhq/cards/commit/f5cf54b6f3c6a2c0974fabb7c441b368ec86fee1))
86 |
87 | ### [0.0.17](https://github.com/microlinkhq/cards/compare/v0.0.16...v0.0.17) (2020-04-16)
88 |
89 |
90 | ### Bug Fixes
91 |
92 | * update query ([e42c2a4](https://github.com/microlinkhq/cards/commit/e42c2a45ac1627394962d52e269e09737e77899b))
93 |
94 | ### [0.0.16](https://github.com/microlinkhq/cards/compare/v0.0.15...v0.0.16) (2020-04-16)
95 |
96 | ### [0.0.15](https://github.com/microlinkhq/cards/compare/v0.0.14...v0.0.15) (2020-04-16)
97 |
98 | ### [0.0.14](https://github.com/microlinkhq/cards/compare/v0.0.13...v0.0.14) (2020-04-16)
99 |
100 |
101 | ### Bug Fixes
102 |
103 | * avoid decode twice ([50f6a69](https://github.com/microlinkhq/cards/commit/50f6a69a9b0af6f6045614851001cab21f7dee80))
104 | * decode ([68c9311](https://github.com/microlinkhq/cards/commit/68c9311559372acc62cef0b3ff95ef93971cdb57))
105 |
106 | ### [0.0.13](https://github.com/microlinkhq/cards/compare/v0.0.12...v0.0.13) (2020-04-16)
107 |
108 | ### [0.0.12](https://github.com/microlinkhq/cards/compare/v0.0.11...v0.0.12) (2020-04-16)
109 |
110 | ### [0.0.11](https://github.com/microlinkhq/cards/compare/v0.0.10...v0.0.11) (2020-04-16)
111 |
112 | ### [0.0.10](https://github.com/microlinkhq/cards/compare/v0.0.9...v0.0.10) (2020-04-16)
113 |
114 |
115 | ### Bug Fixes
116 |
117 | * set load after defer ([990cf00](https://github.com/microlinkhq/cards/commit/990cf001af75893bac6d606861af65b8c991f1ae))
118 |
119 | ### [0.0.9](https://github.com/microlinkhq/cards/compare/v0.0.8...v0.0.9) (2020-04-16)
120 |
121 | ### [0.0.8](https://github.com/microlinkhq/cards/compare/v0.0.7...v0.0.8) (2020-04-16)
122 |
123 |
124 | ### Bug Fixes
125 |
126 | * reset query between presets ([c48fe17](https://github.com/microlinkhq/cards/commit/c48fe17b0c65d8d7b10123bcdf253ba523d755b0))
127 |
128 | ### [0.0.7](https://github.com/microlinkhq/cards/compare/v0.0.6...v0.0.7) (2020-04-16)
129 |
130 |
131 | ### Bug Fixes
132 |
133 | * ensure to flat/unflat query ([d28b35d](https://github.com/microlinkhq/cards/commit/d28b35d388b4f3b93fd2ee0747c406f48ca2e2ef)), closes [#13](https://github.com/microlinkhq/cards/issues/13)
134 |
135 | ### [0.0.6](https://github.com/microlinkhq/cards/compare/v0.0.5...v0.0.6) (2020-04-15)
136 |
137 |
138 | ### Bug Fixes
139 |
140 | * remove CMD key ([9044e56](https://github.com/microlinkhq/cards/commit/9044e565af6fbdddc8fcd8a4732d53762acb0b70))
141 |
142 | ### [0.0.5](https://github.com/microlinkhq/cards/compare/v0.0.4...v0.0.5) (2020-04-15)
143 |
144 |
145 | ### Bug Fixes
146 |
147 | * sync screenshot url ([399f40e](https://github.com/microlinkhq/cards/commit/399f40ea9f2a27c5f59fb6dbb5ee8457c315a63c))
148 | * use ctrl in key bindings ([151aa20](https://github.com/microlinkhq/cards/commit/151aa20ba836f38a53cb77a1896f41e8e5358673))
149 |
150 | ### [0.0.4](https://github.com/microlinkhq/cards/compare/v0.0.2...v0.0.4) (2020-04-15)
151 |
152 | ### [0.0.3](https://github.com/microlinkhq/cards/compare/v0.0.2...v0.0.3) (2020-04-15)
153 |
154 | ### 0.0.2 (2020-04-15)
155 |
156 |
157 | ### Bug Fixes
158 |
159 | * add prod url ([15abf5a](https://github.com/microlinkhq/cards/commit/15abf5af5f5bd428d3b1f57298b85cbb7dee0b05))
160 | * adjust preview resolution ([ac56695](https://github.com/microlinkhq/cards/commit/ac566950ba1dd09ad1c7be9979ba118d0d8b0946))
161 | * border preview ([ec2ae67](https://github.com/microlinkhq/cards/commit/ec2ae67eb7fe28f35863f089112e7a5a04c2d4fd))
162 | * cmd+s for saving ([8d824a9](https://github.com/microlinkhq/cards/commit/8d824a92ce1991e218440b2fcd6e53d0afdf36e8))
163 | * container scroll ([a0aeeef](https://github.com/microlinkhq/cards/commit/a0aeeefd636960238f40cfc8e84f8e772c08f335))
164 | * image proportion ([a3da7b9](https://github.com/microlinkhq/cards/commit/a3da7b98987bdc5fb3035254335d8233724a7b4e))
165 | * linter ([1bd1284](https://github.com/microlinkhq/cards/commit/1bd12843271a1e3a47bd192d319a36d111db5a71))
166 | * prop name ([d702880](https://github.com/microlinkhq/cards/commit/d70288036e815895e54742d39c06689efc3e3c0d))
167 | * remove debug ([65ad867](https://github.com/microlinkhq/cards/commit/65ad867a6afbc30a19b9fb82e036513f3101604b))
168 |
169 | ### 0.0.1 (2020-04-15)
170 |
171 |
172 | ### Bug Fixes
173 |
174 | * add prod url ([15abf5a](https://github.com/microlinkhq/cards/commit/15abf5af5f5bd428d3b1f57298b85cbb7dee0b05))
175 | * adjust preview resolution ([ac56695](https://github.com/microlinkhq/cards/commit/ac566950ba1dd09ad1c7be9979ba118d0d8b0946))
176 | * border preview ([ec2ae67](https://github.com/microlinkhq/cards/commit/ec2ae67eb7fe28f35863f089112e7a5a04c2d4fd))
177 | * cmd+s for saving ([8d824a9](https://github.com/microlinkhq/cards/commit/8d824a92ce1991e218440b2fcd6e53d0afdf36e8))
178 | * container scroll ([a0aeeef](https://github.com/microlinkhq/cards/commit/a0aeeefd636960238f40cfc8e84f8e772c08f335))
179 | * image proportion ([a3da7b9](https://github.com/microlinkhq/cards/commit/a3da7b98987bdc5fb3035254335d8233724a7b4e))
180 | * linter ([1bd1284](https://github.com/microlinkhq/cards/commit/1bd12843271a1e3a47bd192d319a36d111db5a71))
181 | * prop name ([d702880](https://github.com/microlinkhq/cards/commit/d70288036e815895e54742d39c06689efc3e3c0d))
182 | * remove debug ([65ad867](https://github.com/microlinkhq/cards/commit/65ad867a6afbc30a19b9fb82e036513f3101604b))
183 |
--------------------------------------------------------------------------------
/src/pages/_app.js:
--------------------------------------------------------------------------------
1 | import NextApp from 'next/app'
2 | import Head from 'next/head'
3 | import { createGlobalStyle } from 'styled-components'
4 | import { ThemeProvider } from 'theme-ui'
5 | import theme from '@/theme'
6 | import pkg from '@/package.json'
7 |
8 | const GlobalStylesheet = createGlobalStyle`
9 | * {
10 | box-sizing: border-box;
11 | }
12 | html {
13 | background: rgba(255, 255, 255, 0.1);
14 | }
15 | .snackbars {
16 | display: block;
17 | position: fixed;
18 | left: 0;
19 | bottom: 0;
20 | width: 100%;
21 | height: 0;
22 | z-index: 999;
23 | overflow: visible;
24 | }
25 | .snackbar {
26 | position: fixed;
27 | box-sizing: border-box;
28 | left: 50%;
29 | bottom: 14px;
30 | width: 344px;
31 | margin-left: -172px;
32 | transform-origin: center;
33 | will-change: transform;
34 | transition: transform 300ms ease, opacity 300ms ease;
35 | }
36 | .snackbar[aria-hidden='false'] {
37 | -webkit-animation: snackbar-show 300ms ease 1;
38 | animation: snackbar-show 300ms ease 1;
39 | }
40 | .snackbar[aria-hidden='true'] {
41 | -webkit-animation: snackbar-hide 300ms ease forwards 1;
42 | animation: snackbar-hide 300ms ease forwards 1;
43 | }
44 | @media (min-width: 1080px) {
45 | .snackbars-right .snackbar {
46 | left: auto;
47 | right: 20px;
48 | margin-left: 0;
49 | }
50 | .snackbars-left .snackbar {
51 | left: 20px;
52 | margin-left: 0;
53 | }
54 | }
55 | @-webkit-keyframes snackbar-show {
56 | from {
57 | opacity: 0;
58 | transform: translate3d(0, 100%, 0)
59 | }
60 | }
61 | @keyframes snackbar-show {
62 | from {
63 | opacity: 0;
64 | transform: translate3d(0, 100%, 0)
65 | }
66 | }
67 | @-webkit-keyframes snackbar-hide {
68 | to {
69 | opacity: 0;
70 | transform: translateY(100%);
71 | }
72 | }
73 | @keyframes snackbar-hide {
74 | to {
75 | opacity: 0;
76 | transform: translateY(100%);
77 | }
78 | }
79 | @media (max-width: 400px) {
80 | .snackbar {
81 | width: 100%;
82 | bottom: 0;
83 | left: 0;
84 | margin-left: 0;
85 | border-radius: 0;
86 | }
87 | }
88 | .snackbar--container {
89 | display: flex;
90 | background: #2a2a2a;
91 | border-radius: 2px;
92 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
93 | color: #eee;
94 | cursor: default;
95 | margin-bottom: 10px;
96 | }
97 | .snackbar--text {
98 | flex: 1 1 auto;
99 | padding: 16px;
100 | font-size: 100%;
101 | font-family: ${theme.fonts.sans};
102 | }
103 | .snackbar--button {
104 | position: relative;
105 | flex: 0 1 auto;
106 | padding: 8px;
107 | height: 36px;
108 | margin: auto 8px auto -8px;
109 | background: none;
110 | border: none;
111 | border-radius: 3px;
112 | color: white;
113 | font-weight: inherit;
114 | letter-spacing: 0.05em;
115 | font-size: 100%;
116 | text-transform: uppercase;
117 | text-align: center;
118 | cursor: pointer;
119 | overflow: hidden;
120 | transition: background-color 200ms ease;
121 | outline: none;
122 | }
123 |
124 | .snackbar--button:focus:before {
125 | content: '';
126 | position: absolute;
127 | left: 50%;
128 | top: 50%;
129 | width: 120%;
130 | height: 0;
131 | padding: 0 0 120%;
132 | margin: -60% 0 0 -60%;
133 | background: rgba(255, 255, 255, 0.1);
134 | border-radius: 50%;
135 | transform-origin: center;
136 | will-change: transform;
137 | -webkit-animation: focus-ring 300ms ease-out forwards 1;
138 | animation: focus-ring 300ms ease-out forwards 1;
139 | pointer-events: none;
140 | }
141 |
142 | @-webkit-keyframes focus-ring {
143 | from {
144 | transform: scale(0.01);
145 | }
146 | }
147 |
148 | @keyframes focus-ring {
149 | from {
150 | transform: scale(0.01);
151 | }
152 | }
153 |
154 | #overlay-container[aria-hidden='false'] {
155 | -webkit-animation: snackbar-show 300ms ease 1;
156 | animation: snackbar-show 300ms ease 1;
157 | }
158 |
159 | #overlay-container[aria-hidden='true'] {
160 | -webkit-animation: snackbar-hide 300ms ease forwards 1;
161 | animation: snackbar-hide 300ms ease forwards 1;
162 | }
163 |
164 | .react-loading-skeleton {
165 | display: block !important;
166 | }
167 | `
168 | const meta = {
169 | title: 'Microlink Cards',
170 | description: pkg.description,
171 | image: 'https://cdn.microlink.io/banner/cards.png',
172 | logo: 'https://cdn.microlink.io/logo/trim.png',
173 | url: 'https://cards.microlink.io',
174 | siteName: 'Microlink Cards',
175 | type: 'website'
176 | }
177 |
178 | export default class App extends NextApp {
179 | render () {
180 | const { Component, pageProps } = this.props
181 |
182 | return (
183 |
184 |
185 |
186 | {/* */}
187 |
188 |
189 | {/* */}
190 |
191 |
192 |
193 | {meta.title}
194 |
198 | {/* */}
199 |
200 |
201 |
202 | {/* */}
203 |
204 |
205 |
206 |
207 | {/* */}
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 | {/* */}
216 |
221 |
226 |
231 |
236 |
241 |
246 |
251 |
256 |
262 |
268 |
274 |
280 |
286 |
290 |
291 |
292 |
293 | )
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import { HorizontalDragBar, VerticalDragBar } from '@/components/drag-bars'
2 | import { Tab, Tabs, TabList, TabPanel } from '@/components/tabs'
3 | import { Image, Text, Box, Flex, useThemeUI } from 'theme-ui'
4 | import SearchableSelect from '@/components/searchable-select'
5 | import { marshall, unmarshall } from '@/lib/compress-json'
6 | import useScreenshotUrl from '@/hooks/use-screenshot-url'
7 | import KeyboardIcon from '@/components/icons/keyboard'
8 | import useKeyBindings from '@/hooks/use-key-bindings'
9 | import useQueryState from '@/hooks/use-query-state'
10 | import GitHubIcon from '@/components/icons/github'
11 | import aspectRatio from '@/lib/aspect-ratio-16-9'
12 | import JSONViewer from '@/components/json-viewer'
13 | import ButtonIcon from '@/components/button-icon'
14 | import ThemeIcon from '@/components/icons/theme'
15 | import InfoIcon from '@/components/icons/info'
16 | import notification from '@/lib/notification'
17 | import useLoading from '@/hooks/use-loading'
18 | import presets from '@/components/presets'
19 | import Overlay from '@/components/overlay'
20 | import Button from '@/components/button'
21 | import shareCode from '@/lib/share-code'
22 | import Choose from '@/components/choose'
23 | import { diff } from 'deep-object-diff'
24 | import store from '@/lib/local-storage'
25 | import clipboard from '@/lib/clipboard'
26 | import debounce from '@/lib/debounce'
27 | import Main from '@/components/main'
28 | import Code from '@/components/code'
29 | import isEmpty from '@/lib/is-empty'
30 | import * as polished from 'polished'
31 | import isMac from '@/lib/is-mac'
32 | import Router from 'next/router'
33 | import { useState } from 'react'
34 | import themeBase from '@/theme'
35 | import toPx from '@/lib/to-px'
36 | import defer from 'tickedoff'
37 | import Cycled from 'cycled'
38 |
39 | import {
40 | LiveProvider,
41 | LiveEditor,
42 | LiveError,
43 | LivePreview
44 | } from '@/components/live-editor'
45 |
46 | import pkg from '@/package.json'
47 |
48 | const DEFAULT_PRESET = 'rauchg'
49 | const ASIDE_HEIGHT_KEY = 'sidebar-json-height'
50 | const ASIDE_WIDTH_KEY = 'sidebar-width'
51 | const DEFAULT_ASIDE_WIDTH = '40%'
52 | const DEFAULT_ASIDE_HEIGHT = '25%'
53 | const ASIDE_MIN_WIDTH = '40%'
54 | const ASIDE_MAX_WIDTH = '60%'
55 | const ASIDE_MIN_HEIGHT = '15%'
56 | const ASIDE_MAX_HEIGHT = '70%'
57 |
58 | const OVERLAY_STATE = {
59 | PREVIEW: 'preview',
60 | ABOUT: 'about',
61 | KEYBINDINGS: 'keybindings'
62 | }
63 |
64 | const PREVIEW_WIDTH = 500
65 | const PREVIEW_HEIGHT = aspectRatio(PREVIEW_WIDTH)
66 |
67 | const updateQuery = debounce(({ setQuery, code, queryVariables }) => {
68 | let newQuery = {}
69 | if (!isEmpty(code)) newQuery.p = marshall(code)
70 | if (!isEmpty(queryVariables)) newQuery = { ...newQuery, ...queryVariables }
71 | setQuery(newQuery)
72 | })
73 |
74 | const updateStore = ({ key, value }) => store.set(key, value)
75 |
76 | const cycledMode = new Cycled(Object.keys(themeBase.colors.modes))
77 | const nextMode = () => cycledMode.next()
78 |
79 | export default () => {
80 | const isLoading = useLoading()
81 | const [query, setQuery] = useQueryState()
82 | const { theme, colorMode, setColorMode } = useThemeUI()
83 | const [isOverlay, setOverlay] = useState('')
84 | const [asideWidth, setAsideWidth] = useState(
85 | store.get(ASIDE_WIDTH_KEY) || DEFAULT_ASIDE_WIDTH
86 | )
87 |
88 | const [jsonHeight, setJsonHeight] = useState(
89 | store.get(ASIDE_HEIGHT_KEY) || DEFAULT_ASIDE_HEIGHT
90 | )
91 |
92 | const [preset, setPreset] = useState(presets[query.preset || DEFAULT_PRESET])
93 |
94 | const [code, setCode] = useState(() => {
95 | if (isEmpty(query)) return preset.code
96 | const { p } = query
97 | if (isEmpty(p)) return preset.code
98 | return unmarshall(p)
99 | })
100 |
101 | const [queryVariables, setQueryVariables] = useState(() => {
102 | if (isEmpty(query)) return preset.query
103 | const { p, preset: queryPreset, ...queryVariables } = query
104 | if (isEmpty(queryVariables)) return preset.query
105 | return { ...preset.query, ...queryVariables }
106 | })
107 |
108 | const [screenshotUrl, syncScreenshotUrl] = useScreenshotUrl(queryVariables)
109 |
110 | const showOverlay = state => () => {
111 | syncScreenshotUrl(queryVariables)
112 | defer(() => setOverlay(state))
113 | }
114 |
115 | const hideOverlay = () => setOverlay('')
116 |
117 | async function toClipboard (text, name) {
118 | await clipboard.write(text)
119 | notification(`Copied ${name} to clipboard`)
120 | }
121 |
122 | const changeTheme = () => setColorMode(nextMode())
123 |
124 | useKeyBindings({
125 | Escape: { fn: hideOverlay },
126 | KeyH: { ctrl: true, fn: showOverlay(OVERLAY_STATE.KEYBINDINGS) },
127 | KeyI: { ctrl: true, fn: showOverlay(OVERLAY_STATE.ABOUT) },
128 | KeyP: { ctrl: true, fn: changeTheme },
129 | KeyS: { ctrl: true, fn: showOverlay(OVERLAY_STATE.PREVIEW) }
130 | })
131 |
132 | const handleCode = newCode => {
133 | setCode(newCode)
134 | updateQuery({ setQuery, code: newCode })
135 | }
136 |
137 | const handleQueryVariables = newJSON => {
138 | setQueryVariables(newJSON)
139 | updateQuery({ setQuery, queryVariables: diff(preset.query, newJSON) })
140 | }
141 |
142 | const handleAsideWidthReize = width => {
143 | setAsideWidth(width)
144 | updateStore({ key: ASIDE_WIDTH_KEY, value: width })
145 | }
146 |
147 | const handleAsideHeightResize = height => {
148 | setJsonHeight(height)
149 | updateStore({ key: ASIDE_HEIGHT_KEY, value: height })
150 | }
151 |
152 | const handleSelectChange = event => {
153 | const presetName = event.value
154 | const newPreset = presets[presetName]
155 | setPreset(newPreset)
156 | setCode(newPreset.code)
157 | setQueryVariables(newPreset.query)
158 | setQuery({ p: undefined, preset: presetName }, { replace: true })
159 | }
160 |
161 | if (isLoading) return null
162 |
163 | const isEditor = Router.asPath.startsWith('/editor')
164 | const editorTheme = theme.colors.modes[colorMode]
165 | const color = editorTheme.plain.color
166 | const bg = editorTheme.plain.backgroundColor
167 | const borderColor = polished.rgba(color, 0.1)
168 | const iconColor = polished.rgba(color, 0.75)
169 |
170 | const stringColor = theme.colors.styles.find(item =>
171 | item.types.includes('string')
172 | ).style.color
173 |
174 | const OverlayHeader = ({ sx, children }) => {
175 | return (
176 |
177 |
186 | {children}
187 |
188 |
189 | )
190 | }
191 |
192 | const OverlayFooter = () => {
193 | return (
194 |
195 |
200 |
201 | )
202 | }
203 |
204 | const OverlayKeyBindings = () => {
205 | const ctrl = isMac ? 'cmd' : 'ctrl'
206 | return (
207 | <>
208 |
209 |
218 | Combination
219 | Description
220 |
221 | {[
222 | {
223 | combination: [ctrl, ' + ', 'h'],
224 | description: 'Show keybindings information'
225 | },
226 | {
227 | combination: [ctrl, ' + ', 'i'],
228 | description: 'Show information about the project'
229 | },
230 | {
231 | combination: [ctrl, ' + ', 's'],
232 | description: 'Get the current image URL'
233 | },
234 | {
235 | combination: [ctrl, ' + ', 'click'],
236 | description: 'Edit the selected value on query variables'
237 | },
238 | {
239 | combination: [ctrl, ' + ', 'p'],
240 | description: 'Change the editor theme'
241 | },
242 | {
243 | combination: ['esc'],
244 | description: 'Close the active modal'
245 | }
246 | ].map(({ combination, description }) => (
247 |
252 |
253 |
263 | {combination.map(key => (
264 |
274 | ))}
275 |
276 |
277 | {description}
278 |
279 | ))}
280 |
281 |
282 | >
283 | )
284 | }
285 |
286 | const OverlayAbout = () => {
287 | return (
288 | <>
289 |
290 |
294 |
295 |
296 |
297 | Microlink Cards generates social images on demand, ready to be
298 | embed in your{' '}
299 |
300 | <meta>
301 | {' '}
302 | tags.
303 |
304 |
305 | The tool is an interactive editor that allows generate your images
306 | writing them with code and feed with dynamic content.
307 |
308 |
309 | It's powered by{' '}
310 |
317 | Microlink API
318 |
319 | , the fastest and scalable headless browser platform on the cloud.
320 |
321 |
322 | It starts from free and code is available on{' '}
323 |
330 | GitHub
331 |
332 | .
333 |
334 |
335 | >
336 | )
337 | }
338 |
339 | const OverlayPreview = () => {
340 | return (
341 | <>
342 |
347 | toClipboard(screenshotUrl, 'URL')}
352 | />
353 |
354 |
355 |
356 | Add it to your website by copying the code below
357 |
358 |
359 |
360 |
361 |
362 | SEO tags
363 | HTML
364 | Markdown
365 | Direct URL
366 |
367 |
368 |
369 | toClipboard(e.target.textContent, 'SEO Tags')}
375 | children={shareCode(screenshotUrl)}
376 | />
377 |
378 |
379 | `}
385 | onClick={e => toClipboard(e.target.textContent, 'HTML')}
386 | />
387 |
388 |
389 | toClipboard(e.target.textContent, 'Markdown')}
396 | />
397 |
398 |
399 | toClipboard(e.target.textContent, 'URL')}
406 | />
407 |
408 |
409 |
410 |
411 | >
412 | )
413 | }
414 |
415 | return (
416 |
421 |
428 |
429 |
430 |
431 |
432 |
433 |
434 |
435 |
436 |
437 |
438 |
439 |
440 |
444 |
445 |
446 | {isEditor && (
447 |
468 |
469 |
470 |
483 |
484 |
489 | ({
494 | value: key,
495 | label: presets[key].name
496 | }))}
497 | onChange={handleSelectChange}
498 | />
499 |
500 |
501 |
506 |
507 |
508 |
513 |
520 |
521 |
522 |
523 |
530 |
531 |
532 |
533 |
534 |
543 |
544 |
545 |
546 |
547 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
571 |
572 |
573 |
583 |
584 |
592 |
597 |
598 |
599 |
600 |
601 | )}
602 |
603 |
604 | )
605 | }
606 |
--------------------------------------------------------------------------------