Add post-download prompts
This commit is contained in:
parent
fec01bd991
commit
473000ce27
@ -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
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>
|
||||
)
|
||||
}
|
28
web/src/ButtonLink.module.css
Normal file
28
web/src/ButtonLink.module.css
Normal file
@ -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
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
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
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>
|
||||
);
|
||||
}
|
3
web/src/SupportModal.module.css
Normal file
3
web/src/SupportModal.module.css
Normal file
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user