diff --git a/components/Billing.js b/components/Billing.js
new file mode 100644
index 0000000..7b0ad67
--- /dev/null
+++ b/components/Billing.js
@@ -0,0 +1,277 @@
+import React from 'react'
+import { Elements, StripeProvider, CardElement, injectStripe } from 'react-stripe-elements'
+import { useAsyncCallback } from '@dawnlabs/tacklebox'
+
+import Button from './Button'
+import Input from './Input'
+import { useAuth } from './AuthContext'
+import LoginButton from './LoginButton'
+
+import { COLORS } from '../lib/constants'
+
+const X = (
+
+
+
+
+)
+
+function Billing(props) {
+ const user = useAuth()
+
+ const [submit, { error, loading, data: success }] = useAsyncCallback(async e => {
+ e.preventDefault()
+
+ const name = e.target.name.value.trim()
+
+ const res = await props.stripe.createToken({ name })
+
+ if (res.error) {
+ throw res.error.message
+ }
+
+ return {}
+ })
+
+ if (!user) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {success ? (
+
+
Thank you for supporting Carbon!
+
+ However, Carbon Diamond is not quite ready yet.
+
+ Your card has not been charged or saved today.
+
+ We greatly appreciate your support, and will contact you when these premium features
+ launch!
+
+
+ — the Carbon Team{' '}
+
+ 💛🖤
+
+
+
+ ) : (
+
+
+ Upgrade to Diamond
+
+ ($5.00 / month)
+
+
Please enter a credit or debit card:
+
+
+ )}
+
+
+ )
+}
+
+const BillingWithStripe = injectStripe(Billing)
+
+export default function() {
+ const [stripe, setStripe] = React.useState(null)
+ React.useEffect(() => {
+ setStripe(window.Stripe(process.env.STRIPE_PUBLIC_KEY))
+ }, [])
+ return (
+
+
+
+
+
+ )
+}
diff --git a/components/LoginButton.js b/components/LoginButton.js
index 7b08784..42f2538 100644
--- a/components/LoginButton.js
+++ b/components/LoginButton.js
@@ -1,5 +1,5 @@
import React from 'react'
-// import Link from 'next/link'
+import Link from 'next/link'
import firebase, { login, logout } from '../lib/client'
import Button from './Button'
@@ -8,14 +8,26 @@ import { useAuth } from './AuthContext'
function Drawer(props) {
return (
-
+
- {/*
-
- Settings
+
+
+ Snippets{' '}
+ {/* FILES? */}
- */}
-
+
+
+
+ {' '}
+ Account
+
+
+
Sign Out
@@ -23,8 +35,13 @@ function Drawer(props) {
{`
.flex {
display: flex;
+ flex-direction: column;
height: 100%;
}
+ img {
+ position: relative;
+ margin-right: 1rem;
+ }
`}
@@ -61,7 +78,7 @@ function LoginButton({ isVisible, toggleVisibility }) {
>
{user ? user.displayName : 'Sign in/up'}
diff --git a/cypress/integration/background-color.spec.js b/cypress/integration/background-color.spec.js
index 471868f..619a198 100644
--- a/cypress/integration/background-color.spec.js
+++ b/cypress/integration/background-color.spec.js
@@ -21,7 +21,7 @@ describe('background color', () => {
cy.visit('/')
openPicker()
closePicker()
- cy.get(picker).should('not.be.visible')
+ cy.get(picker).should('not.exist')
})
it('changes background color to dark red', () => {
diff --git a/lib/api.js b/lib/api.js
index 425782a..39a0417 100644
--- a/lib/api.js
+++ b/lib/api.js
@@ -83,6 +83,21 @@ function getSnippet(uid = '', { host } = {}) {
})
}
+function listSnippets(page, headers) {
+ return client
+ .get(`/snippets`, {
+ params: {
+ page
+ },
+ headers
+ })
+ .then(res => res.data)
+ .catch(e => {
+ console.error(e)
+ throw e
+ })
+}
+
function isNotDefaultSetting(v, k) {
return v === DEFAULT_SETTINGS[k] || !Object.prototype.hasOwnProperty.call(DEFAULT_SETTINGS, k)
}
@@ -131,6 +146,7 @@ const createSnippet = debounce(data => updateSnippet(null, data), ms('5s'), {
export default {
snippet: {
get: getSnippet,
+ list: listSnippets,
update: debounce(updateSnippet, ms('1s'), { leading: true, trailing: true }),
create: createSnippet,
delete: id => deleteSnippet(id)
diff --git a/lib/client.js b/lib/client.js
index 70bbf48..35eae05 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -30,4 +30,12 @@ export function login(provider) {
.catch(console.error)
}
+export function loginGitHub() {
+ const provider = new firebase.auth.GithubAuthProvider()
+ provider.setCustomParameters({
+ allow_signup: 'true'
+ })
+ return login(provider)
+}
+
export default firebase.apps.length ? firebase : null
diff --git a/next.config.js b/next.config.js
index 2960969..f38061a 100644
--- a/next.config.js
+++ b/next.config.js
@@ -28,7 +28,8 @@ const config = withOffline({
FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
FIREBASE_FE_APP_ID: process.env.FIREBASE_FE_APP_ID,
- FIREBASE_API_KEY: process.env.FIREBASE_API_KEY
+ FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
+ STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY
}
})
diff --git a/now.json b/now.json
index 13f15f7..ac8cf37 100644
--- a/now.json
+++ b/now.json
@@ -26,17 +26,27 @@
"Service-Worker-Allowed": "/"
}
},
+ {
+ "src": "/",
+ "continue": true,
+ "headers": {
+ "X-Frame-Options": "SAMEORIGIN"
+ }
+ },
{
"src": "^/(.*)/?",
"continue": true,
"headers": {
- "X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer-when-downgrade",
"Feature-Policy": "geolocation 'self'; microphone 'self'; camera 'self'"
}
},
+ {
+ "src": "/embed/",
+ "dest": "/embed.html"
+ },
{ "handle": "filesystem" }
],
"build": {
@@ -45,7 +55,8 @@
"FIREBASE_PROJECT_ID": "@carbon-firebase-project-id",
"FIREBASE_MESSAGING_SENDER_ID": "@carbon-firebase-messaging-sender-id",
"FIREBASE_FE_APP_ID": "@carbon-firebase-fe-app-id",
- "FIREBASE_API_KEY": "@carbon-firebase-api-key"
+ "FIREBASE_API_KEY": "@carbon-firebase-api-key",
+ "STRIPE_PUBLIC_KEY": "@carbon-stripe-public-key"
}
},
"github": {
diff --git a/package.json b/package.json
index ae0a1c0..748cd14 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"codemirror-mode-elixir": "^1.1.2",
"codemirror-solidity": "^0.2.1",
"cross-env": "^5.2.0",
+ "date-fns": "^2.0.1",
"dom-to-image": "^2.6.0",
"downshift": "^3.2.12",
"dropperx": "^1.0.1",
@@ -51,6 +52,7 @@
"react-image-crop": "^6.0.16",
"react-is": "^16.9.0",
"react-spinner": "^0.2.7",
+ "react-stripe-elements": "^4.0.1",
"react-syntax-highlight": "^15.3.1",
"tohash": "^1.0.2"
},
diff --git a/pages/_document.js b/pages/_document.js
index f7d475e..781388b 100644
--- a/pages/_document.js
+++ b/pages/_document.js
@@ -5,7 +5,9 @@ export default class extends Document {
render() {
return (
-
+
+
+
diff --git a/pages/about.js b/pages/about.js
index 271f33a..aaf79d1 100644
--- a/pages/about.js
+++ b/pages/about.js
@@ -24,7 +24,7 @@ export default () => (
diff --git a/pages/account.js b/pages/account.js
new file mode 100644
index 0000000..65a4e5c
--- /dev/null
+++ b/pages/account.js
@@ -0,0 +1,258 @@
+// Theirs
+import React from 'react'
+import dynamic from 'next/dynamic'
+
+// Ours
+import Button from '../components/Button'
+import Page from '../components/Page'
+import MenuButton from '../components/MenuButton'
+import { MetaLinks } from '../components/Meta'
+import { useAuth } from '../components/AuthContext'
+
+import { loginGitHub, logout } from '../lib/client'
+import { COLORS } from '../lib/constants'
+
+const Billing = dynamic(() => import('../components/Billing'), {
+ loading: () =>
+})
+
+function logoutThunk() {
+ return logout
+}
+
+const soon = ⓘ
+
+function Plan({ selectBilling }) {
+ const user = useAuth()
+
+ function handleSelectFree() {
+ if (!user) {
+ loginGitHub()
+ }
+ }
+
+ function handleSelectUpgrade() {
+ if (!user) {
+ return loginGitHub()
+ }
+
+ selectBilling()
+ }
+
+ return (
+
+
+
+
+
+
+ Free
+
+
+ Diamond
+
+
+
+
+
+ PNG/SVG Exports
+ ✔
+ ✔
+
+
+ Full visual editor
+ ✔
+ ✔
+
+
+ Custom backgrounds
+ ✔
+ ✔
+
+
+ GitHub Gist support
+ ✔
+ ✔
+
+
+ Saved snippets
+ 1000
+ ∞
+
+
+ Embed saved snippets
+
+ ✔
+
+
+ API Access {soon}
+
+ ✔
+
+
+ Saved custom themes/presets {soon}
+
+ ✔
+
+
+ Twitter card unfurls {soon}
+
+ ✔
+
+
+
+ FREE FOREVER
+ $5.00 / month
+
+
+
+
+
+ {user ? 'Current' : 'Get Started'}
+
+
+
+
+ Upgrade
+
+
+
+
+
+
+
+ )
+}
+
+function Settings() {
+ const [selected, select] = React.useState('Plan')
+ const user = useAuth()
+
+ function selectMenu(name) {
+ return () => select(name)
+ }
+ return (
+
+
+
+
+
+
+ {/* */}
+
+
+
+ {selected === 'Plan' &&
}
+ {selected === 'Billing' &&
}
+
+
+ {user &&
}
+
+
+ )
+}
+
+function SettingsPage() {
+ return (
+
+
+
+
+ )
+}
+
+export default SettingsPage
diff --git a/pages/snippets.js b/pages/snippets.js
new file mode 100644
index 0000000..5e2e308
--- /dev/null
+++ b/pages/snippets.js
@@ -0,0 +1,213 @@
+// Theirs
+import React from 'react'
+import Link from 'next/link'
+import Router from 'next/router'
+import formatDistanceToNow from 'date-fns/formatDistanceToNow'
+import { useAsyncCallback } from '@dawnlabs/tacklebox'
+
+import Button from '../components/Button'
+import LoginButton from '../components/LoginButton'
+import { useAuth } from '../components/AuthContext'
+import { useAPI } from '../components/ApiContext'
+
+import { MetaLinks } from '../components/Meta'
+import Carbon from '../components/Carbon'
+
+import { COLORS, DEFAULT_SETTINGS } from '../lib/constants'
+
+// Ours
+import Page from '../components/Page'
+
+function correctTimestamp(n) {
+ if (n < 9e12) {
+ return n * 1000
+ }
+ return n
+}
+
+function Snippet(props) {
+ const config = { ...DEFAULT_SETTINGS, ...props, fontSize: '2px', windowControls: false }
+
+ return (
+
+
+
+
+
+
+ {props.code}
+
+
+
{props.title || props.id}
+
+ Edited {formatDistanceToNow(correctTimestamp(props.updatedAt), { addSuffix: true })}
+
+
+
+
+
+
+ )
+}
+
+function ActionButton(props) {
+ return (
+
+ )
+}
+
+function useOnMount() {
+ const [mounted, mount] = React.useState(false)
+ React.useEffect(() => {
+ mount(true)
+ }, [])
+
+ return mounted
+}
+
+function SnippetsPage() {
+ const user = useAuth()
+ const api = useAPI()
+
+ const [snippets, setSnippets] = React.useState([])
+ const [page, setPage] = React.useState(0)
+
+ const mounted = useOnMount()
+
+ const [loadMore, { loading, data: previousRes }] = useAsyncCallback(api.snippet.list)
+
+ React.useEffect(() => {
+ if (user) {
+ const authorization = user.ra
+ loadMore(page, { authorization }).then(newSnippets =>
+ setSnippets(curr => curr.concat(newSnippets))
+ )
+ }
+ }, [loadMore, page, user])
+
+ if (!user) {
+ return
+ }
+
+ return (
+
+ {snippets.map(snippet => (
+
+ ))}
+ {snippets.length && previousRes && previousRes.length < 10 ? null : (
+
{
+ if (snippets.length) return setPage(p => p + 1)
+
+ Router.push('/')
+ }}
+ >
+ {loading ? 'Loading...' : !snippets.length ? 'Create snippet +' : 'Load more +'}
+
+ )}
+
+
+ )
+}
+
+export default () => (
+
+
+
+
+)
diff --git a/static/github.svg b/static/svg/github.svg
similarity index 100%
rename from static/github.svg
rename to static/svg/github.svg
diff --git a/static/open-source-companies.svg b/static/svg/open-source-companies.svg
similarity index 100%
rename from static/open-source-companies.svg
rename to static/svg/open-source-companies.svg
diff --git a/static/svg/person.svg b/static/svg/person.svg
new file mode 100644
index 0000000..0a52cd6
--- /dev/null
+++ b/static/svg/person.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/svg/snippets.svg b/static/svg/snippets.svg
new file mode 100644
index 0000000..084b0c6
--- /dev/null
+++ b/static/svg/snippets.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 7bb2656..c5c2306 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2974,6 +2974,11 @@ date-fns@^1.27.2:
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
+date-fns@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.1.tgz#c5f30e31d3294918e6b6a82753a4e719120e203d"
+ integrity sha512-C14oTzTZy8DH1Eq8N78owrCWvf3+cnJw88BTK/N3DYWVxDJuJzPaNdplzYxDYuuXXGvqBcO4Vy5SOrwAooXSWw==
+
date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
@@ -7126,6 +7131,13 @@ react-spinner@^0.2.7:
resolved "https://registry.yarnpkg.com/react-spinner/-/react-spinner-0.2.7.tgz#ea3ca3375dd7a54edbb5cc01d17496a2e2fc14db"
integrity sha1-6jyjN13XpU7btcwB0XSWouL8FNs=
+react-stripe-elements@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/react-stripe-elements/-/react-stripe-elements-4.0.1.tgz#89b52c909a80f17afc7907313c3b3c3f21fa125d"
+ integrity sha512-S+O2+hphs6ASz29l85nj6mpS7YWTa3NMwZTonIMt4+8xrfS/jET+0Xd3cNdJoGkHiCtLEId6UimqivONK+liOw==
+ dependencies:
+ prop-types "^15.5.10"
+
react-syntax-highlight@^15.3.1:
version "15.3.1"
resolved "https://registry.yarnpkg.com/react-syntax-highlight/-/react-syntax-highlight-15.3.1.tgz#b44f6f77e2783e8f74c4b30b50d5a886cc35fc1f"