Add post-download prompts

This commit is contained in:
Matt Swensen 2020-07-04 22:57:37 -06:00
parent fec01bd991
commit 473000ce27
No known key found for this signature in database
GPG Key ID: 3F9E482BFC526F35
13 changed files with 254 additions and 114 deletions

@ -1,3 +1,13 @@
# themer web UI
See [themer.dev](https://themer.dev).
## Running the local dev server
With API functions:
STRIPE_SECRET_KEY=<key> netlify dev
Front-end only:
yarn start

23
web/src/ButtonLink.js Normal file

@ -0,0 +1,23 @@
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
import { ExternalIcon } from './Icons';
import styles from './ButtonLink.module.css';
export default ({ external, children, ...props }) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<a
className={ styles.buttonLink }
style={{
color: getActiveColorOrFallback(['accent5']),
'--button-link-resting-background-color': getActiveColorOrFallback(['shade1'], true),
'--button-link-hover-background-color': getActiveColorOrFallback(['shade2'], true),
'--button-link-active-background-color': getActiveColorOrFallback(['shade0'], true),
}}
{ ...props }
>
{ children }
{ external ? (<ExternalIcon className={ styles.externalIcon } />) : null }
</a>
)
}

@ -0,0 +1,28 @@
.buttonLink {
display: inline-flex;
align-items: center;
text-decoration: none;
border-style: solid;
border-width: var(--border-size);
border-radius: var(--border-radius-size);
border-color: currentColor;
font-size: var(--size-regular);
line-height: var(--line-height);
font-family: 'Fira Code', monospace;
padding: var(--size-small-4) var(--size-small-1);
cursor: pointer;
transition: var(--button-transition);
background-color: var(--button-link-resting-background-color);
}
.buttonLink:hover {
background-color: var(--button-link-hover-background-color);
}
.buttonLink:active {
background-color: var(--button-link-active-background-color);
}
.externalIcon {
margin-left: var(--size-small-3);
}

@ -6,17 +6,19 @@ import {
useStripe,
useElements
} from "@stripe/react-stripe-js";
import Color from 'color';
import styles from './CheckoutModal.module.css';
import ThemeContext from './ThemeContext';
import Banner from './Banner';
import Button from './Button';
import useEscListener from './useEscListener';
import { currencyOptions } from './PriceInput';
import Modal from './Modal';
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
const Form = ({ price, onClose, onComplete }) => {
const CheckoutModal = ({ price, onClose, onComplete }) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
const currency = currencyOptions.find(({isoCode}) => isoCode === price.code);
const [succeeded, setSucceeded] = useState(false);
const [error, setError] = useState(null);
const [processing, setProcessing] = useState('');
@ -26,8 +28,6 @@ const Form = ({ price, onClose, onComplete }) => {
const stripe = useStripe();
const elements = useElements();
const { getActiveColorOrFallback } = useContext(ThemeContext);
useEffect(() => {
(async function fetchIntent() {
const response = await window
@ -98,68 +98,21 @@ const Form = ({ price, onClose, onComplete }) => {
}
};
return succeeded ? (
<div className={ styles.successWrapper }>
<Banner color={ getActiveColorOrFallback(['accent3']) }>
Thank you for your support!
</Banner>
<Button
className={ styles.close }
secondary
onClick={ onClose }
>Close</Button>
</div>
) : (
<form className={ styles.form } onSubmit={submit}>
<div
className={ styles.card }
style={{ borderColor: getActiveColorOrFallback(['shade7']) }}
>
<CardElement options={ cardStyle } onChange={ cardChange } />
</div>
<Button
type="button"
className={ styles.cancel }
secondary
onClick={ onClose }
>Cancel</Button>
<Button
className={ styles.submit }
disabled={ disabled || processing || succeeded }
>
{processing ? 'Processing...' : 'Pay & download' }
</Button>
{error ? (
<Banner
className={ styles.error }
color={ getActiveColorOrFallback(['accent0']) }
>
{error}
</Banner>
) : null}
</form>
);
}
export default ({ price, onClose, onComplete }) => {
useEscListener(onClose);
const { getActiveColorOrFallback } = useContext(ThemeContext);
const currency = currencyOptions.find(({isoCode}) => isoCode === price.code);
return (
<div
className={ styles.scrim }
style={{
backgroundColor: Color(getActiveColorOrFallback(['shade0'], true)).fade(0.25).hsl(),
}}
>
<div
className={ styles.content }
style={{
backgroundColor: getActiveColorOrFallback(['shade0'], true),
borderColor: getActiveColorOrFallback(['shade7']),
boxShadow: `0 0 var(--size-large-1) var(--size-large-1) ${getActiveColorOrFallback(['shade0'], true)}`,
}}
>
<form className={ styles.form } onSubmit={submit}>
<Modal onClose={ onClose } footer={ (
<>
<Button
type="button"
className={ styles.cancel }
secondary
onClick={ onClose }
>Cancel</Button>
<Button disabled={ disabled || processing || succeeded }>
{processing ? 'Processing...' : 'Pay & download' }
</Button>
</>
) }>
<div
className={ styles.price }
style={{ color: getActiveColorOrFallback(['shade7']) }}
@ -168,10 +121,27 @@ export default ({ price, onClose, onComplete }) => {
<span className={ styles.symbol }>{currency.symbol}</span>
{currency.toDisplay(price.amount)}
</div>
<Elements stripe={ stripePromise }>
<Form price={ price } onClose={ onClose } onComplete={ onComplete } />
</Elements>
</div>
</div>
<div
className={ styles.card }
style={{ borderColor: getActiveColorOrFallback(['shade7']) }}
>
<CardElement options={ cardStyle } onChange={ cardChange } />
</div>
{error ? (
<Banner
className={ styles.error }
color={ getActiveColorOrFallback(['accent0']) }
>
{error}
</Banner>
) : null}
</Modal>
</form>
);
}
export default props => (
<Elements stripe={ stripePromise }>
<CheckoutModal { ...props } />
</Elements>
);

@ -1,24 +1,3 @@
.scrim {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
}
.content {
border-radius: var(--border-radius-size);
border-width: var(--large-border-size);
border-style: solid;
width: var(--size-large-9);
max-width: calc(100% - calc(var(--size-regular) * 2));
padding: var(--size-regular);
}
.price {
text-align: center;
margin-bottom: var(--size-small-1);
@ -43,18 +22,7 @@
margin-right: var(--size-small-1);
}
.submit {
margin-top: var(--size-regular);
}
.error {
margin-top: var(--size-small-1);
}
.successWrapper {
text-align: center;
}
.close {
margin-top: var(--size-regular);
}

@ -7,6 +7,7 @@ import saveAs from 'file-saver';
import ThemeContext from './ThemeContext';
import PriceInput from './PriceInput';
import CheckoutModal from './CheckoutModal';
import SupportModal from './SupportModal';
export default () => {
const [alacritty, setAlacritty] = useState(false);
@ -49,7 +50,11 @@ export default () => {
const [xresources, setXresources] = useState(false);
const { getActiveColorOrFallback, preparedColorSet, cliColorSet } = useContext(ThemeContext);
const [price, setPrice] = useState({ code: 'usd', amount: 900 });
const [showCheckoutModal, setShowCheckoutModal] = useState(false);
const [showSupportModal, setShowSupportModal] = useState(false);
const download = async () => {
const selections = {
alacritty,
@ -99,15 +104,13 @@ export default () => {
window.location.href,
cliColorSet,
);
zip.generateAsync({ type: 'blob' }).then(contents => {
saveAs(contents, 'themer.zip');
});
const contents = await zip.generateAsync({ type: 'blob' })
saveAs(contents, 'themer.zip');
window.__ssa__log('download zip', { selections });
setShowSupportModal(true);
window.__ssa__log('open support modal');
};
const [price, setPrice] = useState({ code: 'usd', amount: 900 });
const [showCheckoutModal, setShowCheckoutModal] = useState(false);
return (
<>
<div className={ styles.fieldsetWrapper }>
@ -395,7 +398,18 @@ export default () => {
setShowCheckoutModal(false);
window.__ssa__log('close checkout modal');
} }
onComplete={ download }
onComplete={ () => {
setShowCheckoutModal(false);
download();
} }
/>
) : null }
{ showSupportModal ? (
<SupportModal
onClose={ () => {
setShowSupportModal(false);
window.__ssa__log('close support modal');
} }
/>
) : null }
</>

@ -26,3 +26,11 @@ export const CloseIcon = () => (
<path d="M3,3 L13,13 M3,13 L13,3" strokeWidth="2" fill="none" fillRule="evenodd" strokeLinecap="square" stroke="currentColor"/>
</svg>
);
export const ExternalIcon = ({ className }) => (
<svg className={ className } width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 2L3 2C1.89543 2 1 2.89543 1 4V13C1 14.1046 1.89543 15 3 15H12C13.1046 15 14 14.1046 14 13V10H12V13H3L3 4H6V2Z" fill="currentColor"/>
<path d="M14.5 7V2.5C14.5 1.94772 14.0523 1.5 13.5 1.5H9" stroke="currentColor" fill="none"/>
<path d="M6 10L14 2" stroke="currentColor"/>
</svg>
);

@ -1,11 +1,11 @@
import React, { useContext } from "react";
import ThemeContext from "./ThemeContext";
export default props => {
export default ({ children, ...props }) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<a style={{ color: getActiveColorOrFallback(['accent5']) }} { ...props }>
{ props.children }
{ children }
</a>
);
};

35
web/src/Modal.js Normal file

@ -0,0 +1,35 @@
import React, { useContext } from 'react';
import Color from 'color';
import useEscListener from './useEscListener';
import ThemeContext from './ThemeContext';
import styles from './Modal.module.css';
export default ({ children, footer, onClose }) => {
useEscListener(onClose);
const { getActiveColorOrFallback } = useContext(ThemeContext);
return (
<div
className={ styles.scrim }
style={{
backgroundColor: Color(getActiveColorOrFallback(['shade0'], true)).fade(0.25).hsl(),
}}
>
<div
className={ styles.content }
style={{
backgroundColor: getActiveColorOrFallback(['shade0'], true),
borderColor: getActiveColorOrFallback(['shade7']),
boxShadow: `0 0 var(--size-large-1) var(--size-large-1) ${getActiveColorOrFallback(['shade0'], true)}`,
}}
>
<div className={ styles.body }>{ children }</div>
<footer
className={ styles.footer }
style={{ borderTopColor: getActiveColorOrFallback(['shade2']) }}
>
{ footer }
</footer>
</div>
</div>
)
}

30
web/src/Modal.module.css Normal file

@ -0,0 +1,30 @@
.scrim {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
display: flex;
justify-content: center;
align-items: center;
}
.content {
border-radius: var(--border-radius-size);
border-width: var(--large-border-size);
border-style: solid;
width: var(--size-large-9);
max-width: calc(100% - calc(var(--size-regular) * 2));
}
.body {
padding: var(--size-regular);
}
.footer {
border-top-width: var(--small-border-size);
border-top-style: solid;
padding: var(--size-regular);
text-align: right;
}

51
web/src/SupportModal.js Normal file

@ -0,0 +1,51 @@
import React, { useContext } from 'react';
import Banner from './Banner';
import Button from './Button';
import Modal from './Modal';
import ThemeContext from './ThemeContext';
import ButtonLink from './ButtonLink';
import styles from './SupportModal.module.css';
export default ({ onClose }) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
const tweetIntentUrl = 'https://twitter.com/intent/tweet' +
'?text=' + encodeURIComponent('Check out my personal development environment theme:') +
'&url=' + encodeURIComponent(window.location.href) +
'&via=themerdev';
return (
<Modal onClose={ onClose } footer={ (
<Button
secondary
onClick={ onClose }
>Close</Button>
) }>
<Banner color={ getActiveColorOrFallback(['accent3']) }>
Thank you for using themer!
</Banner>
<p
className={ styles.prompt }
style={{ color: getActiveColorOrFallback(['shade7']) }}
>
Share a direct link to your theme's color configuration:
</p>
<ButtonLink
href={ tweetIntentUrl }
target="_blank"
rel="noopener noreferrer"
external={ true }
>Tweet</ButtonLink>
<p
className={ styles.prompt }
style={{ color: getActiveColorOrFallback(['shade7']) }}
>
Follow @themerdev on Twitter for new themes, wallpapers, and features as soon as they launch:
</p>
<ButtonLink
href="https://twitter.com/intent/follow?screen_name=themerdev"
target="_blank"
rel="noopener noreferrer"
external={ true }
>Follow</ButtonLink>
</Modal>
);
}

@ -0,0 +1,3 @@
.prompt {
margin: var(--size-regular) 0 var(--size-small-2);
}

@ -14,7 +14,7 @@ export default () => {
'--handle-active-background-color': getActiveColorOrFallback(['shade0'], true),
color: getActiveColorOrFallback(['shade7']),
}}
href="https://twitter.com/themerdev"
href="https://twitter.com/intent/follow?screen_name=themerdev"
target="_blank"
rel="noopener noreferrer"
>Follow @themerdev</a>