mirror of
https://github.com/nickzuber/meteorite.git
synced 2024-10-05 15:47:33 +03:00
Run prettier on ./src
This commit is contained in:
parent
1bae149082
commit
693084125c
45
src/App.js
45
src/App.js
@ -1,47 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import amplitude from 'amplitude-js';
|
||||
import {
|
||||
Redirect,
|
||||
Router,
|
||||
Location,
|
||||
LocationProvider
|
||||
} from "@reach/router";
|
||||
import { routes } from './constants';
|
||||
import { AuthProvider } from './providers/Auth';
|
||||
import {
|
||||
Home,
|
||||
Login,
|
||||
Pricing,
|
||||
Guide,
|
||||
NotificationsRedesign,
|
||||
} from './pages';
|
||||
|
||||
import {Redirect, Router, Location, LocationProvider} from '@reach/router';
|
||||
import {routes} from './constants';
|
||||
import {AuthProvider} from './providers/Auth';
|
||||
import {Home, Login, Pricing, Guide, NotificationsRedesign} from './pages';
|
||||
|
||||
// Initalize Amplitude for this session.
|
||||
amplitude.init('752af9db0250fe93d507f42362a2977d');
|
||||
|
||||
function gtag () {
|
||||
function gtag() {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
function gaTrack (options) {
|
||||
function gaTrack(options) {
|
||||
gtag('config', 'UA-154218045-1', options);
|
||||
}
|
||||
|
||||
// Effectively track each new page.
|
||||
function PageTracker ({location}) {
|
||||
React.useEffect(() => {
|
||||
gaTrack({
|
||||
page_location: location,
|
||||
page_path: location.pathname
|
||||
});
|
||||
}, [location]);
|
||||
function PageTracker({location}) {
|
||||
React.useEffect(
|
||||
() => {
|
||||
gaTrack({
|
||||
page_location: location,
|
||||
page_path: location.pathname
|
||||
});
|
||||
},
|
||||
[location]
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function RedirectShell () {
|
||||
function RedirectShell() {
|
||||
return <Redirect noThrow to={routes.NOTIFICATIONS} />;
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import {css, jsx} from '@emotion/core';
|
||||
|
||||
function color (rand) {
|
||||
function color(rand) {
|
||||
// const c = [
|
||||
// '#4caf50',
|
||||
// '#e91e63',
|
||||
@ -10,94 +10,135 @@ function color (rand) {
|
||||
// '#e6d435',
|
||||
// '#B424E6',
|
||||
// ];
|
||||
const c = [
|
||||
'#B424E6',
|
||||
'#1ACA6B',
|
||||
'#E62465',
|
||||
'#FFCD4C',
|
||||
'#4C84FF',
|
||||
'#E9519A',
|
||||
];
|
||||
const c = ['#B424E6', '#1ACA6B', '#E62465', '#FFCD4C', '#4C84FF', '#E9519A'];
|
||||
return c[~~(c.length * rand)];
|
||||
}
|
||||
|
||||
function opacity (xid, max) {
|
||||
function opacity(xid, max) {
|
||||
// Max value (best possible score).
|
||||
const cap = 1;
|
||||
// Best value to base distribution on (the center value).
|
||||
const best = Math.floor(max / 2);
|
||||
// How spread out the distribution is from the best value.
|
||||
const distribution = 8;
|
||||
const result = Math.min(1, cap * Math.pow(
|
||||
Math.E,
|
||||
(-0.5) * (Math.pow(xid - best, 2) / (Math.pow(distribution, 2)))
|
||||
));
|
||||
const result = Math.min(
|
||||
1,
|
||||
cap *
|
||||
Math.pow(
|
||||
Math.E,
|
||||
-0.5 * (Math.pow(xid - best, 2) / Math.pow(distribution, 2))
|
||||
)
|
||||
);
|
||||
return Math.max(1 - result, 0.05) * 1.75;
|
||||
}
|
||||
|
||||
function range (min, max, rand) {
|
||||
function range(min, max, rand) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(rand * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function getConfetti (seed) {
|
||||
function getConfetti(seed) {
|
||||
const stroke = color(seed);
|
||||
const rotation = ~~(seed * 180);
|
||||
|
||||
const svgs = [
|
||||
(
|
||||
<svg css={css`transform: rotate(${rotation}deg);`} width="12" height="9" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 0L11.1962 9H0.803848L6 0Z" fill={stroke}/>
|
||||
</svg>
|
||||
),
|
||||
(
|
||||
<svg css={css`transform: rotate(${rotation}deg);`} width="12" height="9" viewBox="0 0 12 9" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6 0L11.1962 9H0.803848L6 0Z"
|
||||
fill={stroke}
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
(
|
||||
<svg css={css`transform: rotate(${rotation}deg);`} width="14" height="18" viewBox="0 0 14 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M12 2C0.517508 6.06139 6.6799 11.5502 2 16"
|
||||
stroke={stroke}
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
(
|
||||
<svg css={css`transform: rotate(${rotation}deg);`} width="8" height="13" viewBox="0 0 8 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 0L8 6.5L4 13L0 6.5L4 0Z"
|
||||
fill={stroke}
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
(
|
||||
<svg css={css`transform: rotate(${rotation}deg);`} width="8" height="14" viewBox="0 0 8 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 6.6393C8 9.44331 2.33469 14 0.577298 14C-1.1801 14 1.63591 9.44331 1.63591 6.6393C1.63591 3.83529 -0.119245 0 1.63815 0C3.39555 0 8 3.83529 8 6.6393Z"
|
||||
fill={stroke}
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
(
|
||||
<svg css={css`transform: rotate(${rotation}deg);`} width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M9.15408 5.477C8.15168 8.67191 7 13.5 5.15408 11.5C3.30816 9.5 2.90284 9.34138 1.15517 5.47695C-2.00009 -1.49991 3.99732 1.50011 5.15408 3.977C6.15408 1.47707 11.5 -2 9.15408 5.477Z"
|
||||
fill={stroke}/>
|
||||
</svg>
|
||||
),
|
||||
(
|
||||
<svg css={css`transform: rotate(${rotation}deg);`} width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="5" cy="5" r="5" fill={stroke}/>
|
||||
</svg>
|
||||
)
|
||||
<svg
|
||||
css={css`
|
||||
transform: rotate(${rotation}deg);
|
||||
`}
|
||||
width="12"
|
||||
height="9"
|
||||
viewBox="0 0 12 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M6 0L11.1962 9H0.803848L6 0Z" fill={stroke} />
|
||||
</svg>,
|
||||
<svg
|
||||
css={css`
|
||||
transform: rotate(${rotation}deg);
|
||||
`}
|
||||
width="12"
|
||||
height="9"
|
||||
viewBox="0 0 12 9"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M6 0L11.1962 9H0.803848L6 0Z" fill={stroke} />
|
||||
</svg>,
|
||||
<svg
|
||||
css={css`
|
||||
transform: rotate(${rotation}deg);
|
||||
`}
|
||||
width="14"
|
||||
height="18"
|
||||
viewBox="0 0 14 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2C0.517508 6.06139 6.6799 11.5502 2 16"
|
||||
stroke={stroke}
|
||||
stroke-width="4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>,
|
||||
<svg
|
||||
css={css`
|
||||
transform: rotate(${rotation}deg);
|
||||
`}
|
||||
width="8"
|
||||
height="13"
|
||||
viewBox="0 0 8 13"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M4 0L8 6.5L4 13L0 6.5L4 0Z" fill={stroke} />
|
||||
</svg>,
|
||||
<svg
|
||||
css={css`
|
||||
transform: rotate(${rotation}deg);
|
||||
`}
|
||||
width="8"
|
||||
height="14"
|
||||
viewBox="0 0 8 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M8 6.6393C8 9.44331 2.33469 14 0.577298 14C-1.1801 14 1.63591 9.44331 1.63591 6.6393C1.63591 3.83529 -0.119245 0 1.63815 0C3.39555 0 8 3.83529 8 6.6393Z"
|
||||
fill={stroke}
|
||||
/>
|
||||
</svg>,
|
||||
<svg
|
||||
css={css`
|
||||
transform: rotate(${rotation}deg);
|
||||
`}
|
||||
width="10"
|
||||
height="12"
|
||||
viewBox="0 0 10 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M9.15408 5.477C8.15168 8.67191 7 13.5 5.15408 11.5C3.30816 9.5 2.90284 9.34138 1.15517 5.47695C-2.00009 -1.49991 3.99732 1.50011 5.15408 3.977C6.15408 1.47707 11.5 -2 9.15408 5.477Z"
|
||||
fill={stroke}
|
||||
/>
|
||||
</svg>,
|
||||
<svg
|
||||
css={css`
|
||||
transform: rotate(${rotation}deg);
|
||||
`}
|
||||
width="10"
|
||||
height="10"
|
||||
viewBox="0 0 10 10"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="5" cy="5" r="5" fill={stroke} />
|
||||
</svg>
|
||||
];
|
||||
// css={css`transform: rotate(${rotation}deg);`}
|
||||
|
||||
@ -111,33 +152,84 @@ const Confetti = ({offset, row, seed, index, max}) => {
|
||||
const fade = opacity(index, max);
|
||||
offset -= 320;
|
||||
return (
|
||||
<div css={css`
|
||||
z-index: 1;
|
||||
opacity: ${fade};
|
||||
transform: scale(1.2);
|
||||
position: absolute;
|
||||
left: ${offset}px;
|
||||
top: ${topDelta}px;
|
||||
`}
|
||||
<div
|
||||
css={css`
|
||||
z-index: 1;
|
||||
opacity: ${fade};
|
||||
transform: scale(1.2);
|
||||
position: absolute;
|
||||
left: ${offset}px;
|
||||
top: ${topDelta}px;
|
||||
`}
|
||||
>
|
||||
{getConfetti(seed)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function ConfettiSection ({spacing = 82, amount = 18}) {
|
||||
export default function ConfettiSection({spacing = 82, amount = 18}) {
|
||||
const row = new Array(amount).fill(0).map((_, i) => i * spacing);
|
||||
|
||||
return (
|
||||
<div css={css`
|
||||
z-index: 0;
|
||||
`}>
|
||||
{row.map((offset, i) => <Confetti index={i} row={0} offset={offset} seed={Math.random()} max={amount} />)}
|
||||
{row.map((offset, i) => <Confetti index={i} row={1} offset={offset} seed={Math.random()} max={amount} />)}
|
||||
{row.map((offset, i) => <Confetti index={i} row={2} offset={offset} seed={Math.random()} max={amount} />)}
|
||||
{row.map((offset, i) => <Confetti index={i} row={3} offset={offset} seed={Math.random()} max={amount} />)}
|
||||
{row.map((offset, i) => <Confetti index={i} row={4} offset={offset} seed={Math.random()} max={amount} />)}
|
||||
{row.map((offset, i) => <Confetti index={i} row={5} offset={offset} seed={Math.random()} max={amount} />)}
|
||||
<div
|
||||
css={css`
|
||||
z-index: 0;
|
||||
`}
|
||||
>
|
||||
{row.map((offset, i) => (
|
||||
<Confetti
|
||||
index={i}
|
||||
row={0}
|
||||
offset={offset}
|
||||
seed={Math.random()}
|
||||
max={amount}
|
||||
/>
|
||||
))}
|
||||
{row.map((offset, i) => (
|
||||
<Confetti
|
||||
index={i}
|
||||
row={1}
|
||||
offset={offset}
|
||||
seed={Math.random()}
|
||||
max={amount}
|
||||
/>
|
||||
))}
|
||||
{row.map((offset, i) => (
|
||||
<Confetti
|
||||
index={i}
|
||||
row={2}
|
||||
offset={offset}
|
||||
seed={Math.random()}
|
||||
max={amount}
|
||||
/>
|
||||
))}
|
||||
{row.map((offset, i) => (
|
||||
<Confetti
|
||||
index={i}
|
||||
row={3}
|
||||
offset={offset}
|
||||
seed={Math.random()}
|
||||
max={amount}
|
||||
/>
|
||||
))}
|
||||
{row.map((offset, i) => (
|
||||
<Confetti
|
||||
index={i}
|
||||
row={4}
|
||||
offset={offset}
|
||||
seed={Math.random()}
|
||||
max={amount}
|
||||
/>
|
||||
))}
|
||||
{row.map((offset, i) => (
|
||||
<Confetti
|
||||
index={i}
|
||||
row={5}
|
||||
offset={offset}
|
||||
seed={Math.random()}
|
||||
max={amount}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,19 +1,21 @@
|
||||
import React from 'react';
|
||||
import curve from './curve.svg';
|
||||
|
||||
export default function Curve ({ style, ...props }) {
|
||||
export default function Curve({style, ...props}) {
|
||||
return (
|
||||
<div style={{
|
||||
background: `url(${curve}) center bottom`,
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '190vw',
|
||||
paddingBottom: '4.5%',
|
||||
backgroundSize: 'cover',
|
||||
marginBottom: '-1px',
|
||||
...style
|
||||
}} />
|
||||
<div
|
||||
style={{
|
||||
background: `url(${curve}) center bottom`,
|
||||
position: 'absolute',
|
||||
bottom: '0',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '190vw',
|
||||
paddingBottom: '4.5%',
|
||||
backgroundSize: 'cover',
|
||||
marginBottom: '-1px',
|
||||
...style
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -4,14 +4,12 @@ import styled from '@emotion/styled';
|
||||
const Message = styled('p')({
|
||||
color: '#eb3349',
|
||||
fontWeight: 500,
|
||||
'a': {
|
||||
a: {
|
||||
color: '#eb3349',
|
||||
fontWeight: 500,
|
||||
fontWeight: 500
|
||||
}
|
||||
});
|
||||
|
||||
export default function ErrorMessage ({children, props}) {
|
||||
return (
|
||||
<Message {...props}>{children}</Message>
|
||||
);
|
||||
export default function ErrorMessage({children, props}) {
|
||||
return <Message {...props}>{children}</Message>;
|
||||
}
|
||||
|
@ -68,23 +68,30 @@ import pr_closed from './svg/github/pr-closed.svg';
|
||||
import pr_open from './svg/github/pr-open.svg';
|
||||
import pr_merged from './svg/github/pr-merged.svg';
|
||||
|
||||
const SvgIcon = styled('div')({
|
||||
position: 'relative',
|
||||
backgroundSize: 'cover',
|
||||
fontSize: 14,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}, ({size, icon, opacity, shrink}) => ({
|
||||
height: size || 24,
|
||||
width: size || 24,
|
||||
background: `url(${icon}) center center no-repeat`,
|
||||
opacity,
|
||||
transform: shrink ? `scale(${shrink})` : 'inherit'
|
||||
}));
|
||||
const SvgIcon = styled('div')(
|
||||
{
|
||||
position: 'relative',
|
||||
backgroundSize: 'cover',
|
||||
fontSize: 14,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
({size, icon, opacity, shrink}) => ({
|
||||
height: size || 24,
|
||||
width: size || 24,
|
||||
background: `url(${icon}) center center no-repeat`,
|
||||
opacity,
|
||||
transform: shrink ? `scale(${shrink})` : 'inherit'
|
||||
})
|
||||
);
|
||||
|
||||
export default function Icon ({src, ...props}) {
|
||||
return <SvgIcon {...props} icon={src}> </SvgIcon>
|
||||
export default function Icon({src, ...props}) {
|
||||
return (
|
||||
<SvgIcon {...props} icon={src}>
|
||||
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
const createIcon = src => props => <Icon {...props} src={src} />;
|
||||
|
@ -3,7 +3,7 @@ import loader from './loader.svg';
|
||||
import loaderAlt from './loader-alt.svg';
|
||||
import loaderWhite from './loader-white.svg';
|
||||
|
||||
export default function LoadingIcon ({ style, size, alt, white, ...props }) {
|
||||
export default function LoadingIcon({style, size, alt, white, ...props}) {
|
||||
let url = loader;
|
||||
if (white) {
|
||||
url = loaderWhite;
|
||||
@ -12,13 +12,16 @@ export default function LoadingIcon ({ style, size, alt, white, ...props }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
background: `url(${(url)}) center center no-repeat`,
|
||||
position: 'relative',
|
||||
height: size || 100,
|
||||
width: size || 100,
|
||||
margin: '0 auto',
|
||||
...style
|
||||
}} {...props} />
|
||||
<div
|
||||
style={{
|
||||
background: `url(${url}) center center no-repeat`,
|
||||
position: 'relative',
|
||||
height: size || 100,
|
||||
width: size || 100,
|
||||
margin: '0 auto',
|
||||
...style
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -2,16 +2,21 @@ import React from 'react';
|
||||
import meteoriteLogo from './icon-gray.png';
|
||||
import meteoriteLogoWhite from './logo-white.png';
|
||||
|
||||
export default function Logo ({ style, size, white = false, ...props }) {
|
||||
export default function Logo({style, size, white = false, ...props}) {
|
||||
return (
|
||||
<div style={{
|
||||
background: `url(${white ? meteoriteLogoWhite : meteoriteLogo}) center center no-repeat`,
|
||||
backgroundSize: 'contain',
|
||||
position: 'relative',
|
||||
cursor: props.onClick ? 'pointer' : 'default',
|
||||
height: size,
|
||||
width: size,
|
||||
...style
|
||||
}} {...props} />
|
||||
<div
|
||||
style={{
|
||||
background: `url(${
|
||||
white ? meteoriteLogoWhite : meteoriteLogo
|
||||
}) center center no-repeat`,
|
||||
backgroundSize: 'contain',
|
||||
position: 'relative',
|
||||
cursor: props.onClick ? 'pointer' : 'default',
|
||||
height: size,
|
||||
width: size,
|
||||
...style
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -20,8 +20,11 @@ const Button = styled('a')`
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
border-radius: 5px;
|
||||
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
-webkit-transition: color 0.15s ease-in-out,
|
||||
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
`;
|
||||
|
||||
const MainButton = styled(Button)`
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const Status = {
|
||||
QUEUED: 'queued', // Unread
|
||||
STAGED: 'staged', // Read
|
||||
CLOSED: 'closed', // Archived
|
||||
CLOSED: 'closed', // Archived
|
||||
|
||||
// Updated naming, support both for bc.
|
||||
Unread: 'queued',
|
||||
|
@ -16,7 +16,7 @@ export const withOnEnter = WrappedComponent => ({onEnter, ...props}) => (
|
||||
);
|
||||
|
||||
class Tooltip extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.id = ('tooltip-id-' + Math.random()).replace(/\./g, '');
|
||||
@ -26,7 +26,7 @@ class Tooltip extends React.Component {
|
||||
tooltipOffsetX: 0,
|
||||
tooltipOffsetY: 0,
|
||||
tooltipSpeed: 350
|
||||
}
|
||||
};
|
||||
|
||||
getTooltipElement = () => document.querySelector(`#${this.id}`);
|
||||
|
||||
@ -54,23 +54,28 @@ class Tooltip extends React.Component {
|
||||
const {width} = tooltipElement.getBoundingClientRect();
|
||||
const positionStyle = `
|
||||
top: ${y + targetHeight + window.scrollY + tooltipOffsetY}px;
|
||||
left: ${x - (width / 2) + (targetWidth / 2) + tooltipOffsetX}px;
|
||||
${this.props.dark ? `
|
||||
left: ${x - width / 2 + targetWidth / 2 + tooltipOffsetX}px;
|
||||
${
|
||||
this.props.dark
|
||||
? `
|
||||
background: #ffffff;
|
||||
color: #10293c;
|
||||
` : ''}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`;
|
||||
|
||||
tooltipElement.setAttribute('style', positionStyle);
|
||||
this.timeout = setTimeout(() => {
|
||||
tooltipElement.setAttribute(
|
||||
'style', `
|
||||
'style',
|
||||
`
|
||||
${positionStyle}
|
||||
opacity: ${this.props.dark ? 1 : 0.9};
|
||||
`
|
||||
);
|
||||
}, this.props.tooltipSpeed);
|
||||
}
|
||||
};
|
||||
|
||||
removeTooltip = () => {
|
||||
clearTimeout(this.timeout);
|
||||
@ -78,44 +83,48 @@ class Tooltip extends React.Component {
|
||||
if (tooltipElement) {
|
||||
tooltipElement.parentNode.removeChild(tooltipElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return this.props.children({
|
||||
onMouseEnter: this.onMouseEnter,
|
||||
onMouseLeave: this.removeTooltip,
|
||||
onMouseDown: this.removeTooltip,
|
||||
onMouseDown: this.removeTooltip
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const withTooltip = WrappedComponent => ({
|
||||
tooltip,
|
||||
tooltipOffsetX,
|
||||
tooltipOffsetY,
|
||||
tooltipSpeed,
|
||||
...props
|
||||
}) => (
|
||||
<Tooltip
|
||||
dark={props.dark}
|
||||
message={tooltip}
|
||||
tooltipOffsetX={tooltipOffsetX}
|
||||
tooltipOffsetY={tooltipOffsetY}
|
||||
tooltipSpeed={tooltipSpeed}
|
||||
>
|
||||
{mouseEvents => tooltip ? (
|
||||
tooltip,
|
||||
tooltipOffsetX,
|
||||
tooltipOffsetY,
|
||||
tooltipSpeed,
|
||||
...props
|
||||
}) => (
|
||||
<Tooltip
|
||||
dark={props.dark}
|
||||
message={tooltip}
|
||||
tooltipOffsetX={tooltipOffsetX}
|
||||
tooltipOffsetY={tooltipOffsetY}
|
||||
tooltipSpeed={tooltipSpeed}
|
||||
>
|
||||
{mouseEvents =>
|
||||
tooltip ? (
|
||||
<WrappedComponent {...props} {...mouseEvents} />
|
||||
) : (
|
||||
<WrappedComponent {...props} />
|
||||
)}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
function transformEventsForMobile (props) {
|
||||
function transformEventsForMobile(props) {
|
||||
return Object.keys(props).reduce((aux, prop) => {
|
||||
let propT = prop;
|
||||
switch (prop) {
|
||||
case 'onClick': propT = 'onTouchEnd'; break;
|
||||
case 'onClick':
|
||||
propT = 'onTouchEnd';
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
aux[propT] = props[prop];
|
||||
@ -123,10 +132,8 @@ function transformEventsForMobile (props) {
|
||||
}, {});
|
||||
}
|
||||
|
||||
function transformEvents (props) {
|
||||
return isMobile
|
||||
? transformEventsForMobile(props)
|
||||
: props;
|
||||
function transformEvents(props) {
|
||||
return isMobile ? transformEventsForMobile(props) : props;
|
||||
}
|
||||
|
||||
export const withOptimizedTouchEvents = BaseComponent => props => (
|
||||
|
@ -8,11 +8,11 @@ const OfflinePlugin = require('offline-plugin/runtime');
|
||||
|
||||
OfflinePlugin.install({
|
||||
onInstalled: function() {
|
||||
console.info('Offline content has been installed.')
|
||||
console.info('Offline content has been installed.');
|
||||
},
|
||||
|
||||
onUpdating: function() {
|
||||
console.info('Updating offline content.')
|
||||
console.info('Updating offline content.');
|
||||
},
|
||||
|
||||
onUpdateReady: function() {
|
||||
|
@ -5,16 +5,12 @@ import styled from '@emotion/styled';
|
||||
import {css, jsx} from '@emotion/core';
|
||||
import {Link as RouterLink} from '@reach/router';
|
||||
import {routes} from '../../constants';
|
||||
import {
|
||||
BasicPageWrapper,
|
||||
forSmallScreens,
|
||||
forMobile
|
||||
} from '../common';
|
||||
import {BasicPageWrapper, forSmallScreens, forMobile} from '../common';
|
||||
import WorkflowToggle from '../Home/WorkflowToggle';
|
||||
|
||||
import SettingsPng from '../../images/screenshots/new/settings.png'
|
||||
import ParticipatingPng from '../../images/screenshots/new/settings-participating.png'
|
||||
import WatchingPng from '../../images/screenshots/new/settings-watching.png'
|
||||
import SettingsPng from '../../images/screenshots/new/settings.png';
|
||||
import ParticipatingPng from '../../images/screenshots/new/settings-participating.png';
|
||||
import WatchingPng from '../../images/screenshots/new/settings-watching.png';
|
||||
|
||||
import '../../styles/gradient.css';
|
||||
import '../../styles/font.css';
|
||||
@ -22,16 +18,15 @@ import '../../styles/font.css';
|
||||
const themeColor = '#27B768';
|
||||
const ALT_BACKGROUND_COLOR = '#f6f2ed';
|
||||
|
||||
|
||||
const Outer = styled('div')`
|
||||
background: ${p => p.alt ? ALT_BACKGROUND_COLOR : 'none'};
|
||||
background: ${p => (p.alt ? ALT_BACKGROUND_COLOR : 'none')};
|
||||
`;
|
||||
|
||||
const Container = styled('div')`
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: ${p => p.column ? 'column' : 'row'};
|
||||
flex-direction: ${p => (p.column ? 'column' : 'row')};
|
||||
max-width: 1080px;
|
||||
min-height: 100px;
|
||||
margin: 0 auto;
|
||||
@ -40,24 +35,22 @@ const Container = styled('div')`
|
||||
${forSmallScreens(`
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
`)}
|
||||
${forMobile(`
|
||||
`)} ${forMobile(`
|
||||
margin-bottom: 2.5rem;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const LightContainer = styled(Container)`
|
||||
${forSmallScreens(`
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
`)}
|
||||
${forMobile(`
|
||||
`)} ${forMobile(`
|
||||
margin-bottom: 2.5rem;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const FlexItem = styled('div')`
|
||||
flex: ${(({flex = 1}) => flex)};
|
||||
flex: ${({flex = 1}) => flex};
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -84,8 +77,11 @@ const Button = styled(RouterLink)`
|
||||
font-size: 18px;
|
||||
line-height: 1.75;
|
||||
border-radius: 5px;
|
||||
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
-webkit-transition: color 0.15s ease-in-out,
|
||||
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
@ -131,14 +127,15 @@ const HeroTitle = styled('h1')`
|
||||
font-size: 68px;
|
||||
line-height: 68px;
|
||||
margin: 0 auto 6px 0;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-family: medium-marketing-display-font, Georgia, Cambria, Times New Roman,
|
||||
Times, serif;
|
||||
font-weight: 500;
|
||||
|
||||
${forMobile(`
|
||||
font-size: 46px;
|
||||
line-height: 54px;
|
||||
margin: 0 auto 6px;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const HeroSubtitle = styled('h1')`
|
||||
@ -155,14 +152,15 @@ const HeroSubtitle = styled('h1')`
|
||||
margin: 0 auto 32px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const Title = styled('h1')`
|
||||
font-size: 38px;
|
||||
line-height: 38px;
|
||||
margin: 0 auto 12px;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-family: medium-marketing-display-font, Georgia, Cambria, Times New Roman,
|
||||
Times, serif;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
`;
|
||||
@ -202,7 +200,7 @@ const HeroLeft = styled(FlexItem)`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
min-height: unset;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const HeroRight = styled(FlexItem)`
|
||||
@ -211,7 +209,7 @@ const HeroRight = styled(FlexItem)`
|
||||
${forMobile(`
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const PricingContainer = styled('div')`
|
||||
@ -226,13 +224,12 @@ const PricingContainer = styled('div')`
|
||||
text-align: left;
|
||||
${forMobile(`
|
||||
width: 100%;
|
||||
`)}
|
||||
|
||||
h4 {
|
||||
`)} h4 {
|
||||
margin: -2px 0 0;
|
||||
font-size: 20px;
|
||||
color: #26b768;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-family: medium-marketing-display-font, Georgia, Cambria,
|
||||
Times New Roman, Times, serif;
|
||||
}
|
||||
|
||||
p {
|
||||
@ -243,14 +240,16 @@ const PricingContainer = styled('div')`
|
||||
}
|
||||
`;
|
||||
|
||||
const Price = styled(Title)(p => `
|
||||
const Price = styled(Title)(
|
||||
p => `
|
||||
font-size: ${p.crossed ? 32 : 52}px;
|
||||
line-height: ${p.crossed ? 32 : 54}px;
|
||||
text-decoration: ${p.crossed ? 'line-through' : 'unset'};
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
text-align: left;
|
||||
`);
|
||||
`
|
||||
);
|
||||
|
||||
const Badge = styled('span')`
|
||||
font-size: 14px;
|
||||
@ -269,15 +268,13 @@ const FloatingLinkBox = styled('div')`
|
||||
background: rgb(255, 254, 253);
|
||||
flex: 1;
|
||||
position: relative;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.075);
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
border-radius: 4px;
|
||||
padding: 16px 24px;
|
||||
margin: 0 24px 0 0;
|
||||
${forMobile(`
|
||||
margin: 0 0 24px 0;
|
||||
`)}
|
||||
|
||||
h4 {
|
||||
`)} h4 {
|
||||
margin: 0;
|
||||
color: #47494b;
|
||||
font-size: 18px;
|
||||
@ -293,14 +290,14 @@ const FloatingLinkBox = styled('div')`
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
color: #00A0F5;
|
||||
color: #00a0f5;
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
a:hover {
|
||||
color: #0886c9;
|
||||
}
|
||||
a::after {
|
||||
content: " →"
|
||||
content: ' →';
|
||||
}
|
||||
`;
|
||||
|
||||
@ -316,8 +313,7 @@ const FAQItem = styled('div')`
|
||||
flex: 1 0 34%;
|
||||
${forMobile(`
|
||||
flex: 1 0 51%;
|
||||
`)}
|
||||
flex-direction: column;
|
||||
`)} flex-direction: column;
|
||||
display: flex;
|
||||
align-items: left;
|
||||
justify-content: center;
|
||||
@ -329,7 +325,8 @@ const FAQItem = styled('div')`
|
||||
justify-content: center;
|
||||
line-height: 25px;
|
||||
margin: 18px 0 8px;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-family: medium-marketing-display-font, Georgia, Cambria,
|
||||
Times New Roman, Times, serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
@ -354,18 +351,20 @@ const DotsBackground = styled('div')`
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
margin-left: -60px;
|
||||
background: radial-gradient(transparent 50%, #fffefd), \
|
||||
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==) repeat;
|
||||
background: radial-gradient(transparent 50%, #fffefd),
|
||||
\url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==)
|
||||
repeat;
|
||||
${forMobile(`
|
||||
display: none;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const Checklist = styled('div')`
|
||||
z-index: 1;
|
||||
background: #fff;
|
||||
margin: 0 auto;
|
||||
box-shadow: rgba(84,70,35,0) 0px 4px 18px, rgba(84,70,35,0.15) 0px 2px 8px;
|
||||
box-shadow: rgba(84, 70, 35, 0) 0px 4px 18px,
|
||||
rgba(84, 70, 35, 0.15) 0px 2px 8px;
|
||||
border-radius: 6px;
|
||||
min-height: 100px;
|
||||
min-width: 200px;
|
||||
@ -384,11 +383,11 @@ const ChecklistItem = styled('span')`
|
||||
color: #737b83;
|
||||
|
||||
&::before {
|
||||
content: "✓";
|
||||
content: '✓';
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
border-radius: 100%;
|
||||
background: #27B768;
|
||||
background: #27b768;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
height: 22px;
|
||||
@ -406,10 +405,11 @@ const GuideContainer = styled('div')`
|
||||
flex-direction: row;
|
||||
${forSmallScreens(`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const GuideItem = styled('div')(p => `
|
||||
const GuideItem = styled('div')(
|
||||
p => `
|
||||
flex: ${p.flex || 1};
|
||||
position: relative;
|
||||
${forSmallScreens(`
|
||||
@ -437,23 +437,33 @@ const GuideItem = styled('div')(p => `
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.175);
|
||||
border-radius: 4px;
|
||||
}
|
||||
`);
|
||||
`
|
||||
);
|
||||
|
||||
export default function Scene (props) {
|
||||
export default function Scene(props) {
|
||||
return (
|
||||
<BasicPageWrapper {...props}>
|
||||
{/* Hero */}
|
||||
<Container css={css`
|
||||
${forMobile(`
|
||||
<Container
|
||||
css={css`
|
||||
${forMobile(`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
`}>
|
||||
`)};
|
||||
`}
|
||||
>
|
||||
<HeroLeft>
|
||||
<HeroTitle>
|
||||
{'No installation.'}<br />
|
||||
{'No installation.'}
|
||||
<br />
|
||||
{'Easy setup.'}
|
||||
</HeroTitle>
|
||||
<HeroSubtitle css={css`margin-top: -35px;`}>{'You\'re at most a few click away.'}</HeroSubtitle>
|
||||
</HeroTitle>
|
||||
<HeroSubtitle
|
||||
css={css`
|
||||
margin-top: -35px;
|
||||
`}
|
||||
>
|
||||
{"You're at most a few click away."}
|
||||
</HeroSubtitle>
|
||||
<FlexBreak />
|
||||
</HeroLeft>
|
||||
<HeroRight>
|
||||
@ -462,7 +472,7 @@ export default function Scene (props) {
|
||||
<ChecklistTitle>{'Only a few simple steps.'}</ChecklistTitle>
|
||||
<ChecklistItem>{'Navigate to your GitHub settings.'}</ChecklistItem>
|
||||
<ChecklistItem>{'Turn on web notifications.'}</ChecklistItem>
|
||||
<ChecklistItem>{'And you\'re done.'}</ChecklistItem>
|
||||
<ChecklistItem>{"And you're done."}</ChecklistItem>
|
||||
</Checklist>
|
||||
</HeroRight>
|
||||
</Container>
|
||||
@ -472,8 +482,12 @@ export default function Scene (props) {
|
||||
<Container column>
|
||||
<FlexBreak height={60} />
|
||||
<Title>{'Ready. Set. Go.'}</Title>
|
||||
<Subtitle>{'You only need to flip a few switches before we can organize your notifications\
|
||||
and make your life easier.'}</Subtitle>
|
||||
<Subtitle>
|
||||
{
|
||||
'You only need to flip a few switches before we can organize your notifications\
|
||||
and make your life easier.'
|
||||
}
|
||||
</Subtitle>
|
||||
|
||||
<WorkflowToggle
|
||||
easeTimingMs={100}
|
||||
@ -481,30 +495,48 @@ export default function Scene (props) {
|
||||
{
|
||||
id: 0,
|
||||
title: '1. Visit your settings',
|
||||
description: 'Head over to GitHub and view your account settings in the notifications tab.',
|
||||
description:
|
||||
'Head over to GitHub and view your account settings in the notifications tab.',
|
||||
image: SettingsPng
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: '2. Enable Participating alerts',
|
||||
description: 'Turn on "Web and Mobile" notifications for the participating tab.',
|
||||
description:
|
||||
'Turn on "Web and Mobile" notifications for the participating tab.',
|
||||
image: ParticipatingPng
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '3. Enable Watching alerts',
|
||||
description: 'Turn on "Web and Mobile" notifications for the watching tab.',
|
||||
description:
|
||||
'Turn on "Web and Mobile" notifications for the watching tab.',
|
||||
image: WatchingPng
|
||||
},
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<FlexBreak height={100} />
|
||||
<Title css={css`font-size: 32px; line-height: 38px; margin-bottom: 24px;`}>{'Start using Meteorite absolutely free'}</Title>
|
||||
<HeroButton to={routes.LOGIN} css={css`margin: 0 auto;`}>{'Login / Sign up'}</HeroButton>
|
||||
<Title
|
||||
css={css`
|
||||
font-size: 32px;
|
||||
line-height: 38px;
|
||||
margin-bottom: 24px;
|
||||
`}
|
||||
>
|
||||
{'Start using Meteorite absolutely free'}
|
||||
</Title>
|
||||
<HeroButton
|
||||
to={routes.LOGIN}
|
||||
css={css`
|
||||
margin: 0 auto;
|
||||
`}
|
||||
>
|
||||
{'Login / Sign up'}
|
||||
</HeroButton>
|
||||
<FlexBreak height={80} />
|
||||
</Container>
|
||||
</Outer>
|
||||
</BasicPageWrapper>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { compose } from 'recompose';
|
||||
import { withAuthProvider } from '../../providers/Auth';
|
||||
import { withCookiesProvider } from '../../providers/Cookies';
|
||||
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
|
||||
import {compose} from 'recompose';
|
||||
import {withAuthProvider} from '../../providers/Auth';
|
||||
import {withCookiesProvider} from '../../providers/Cookies';
|
||||
import {OAUTH_TOKEN_COOKIE} from '../../constants/cookies';
|
||||
import Scene from './Scene';
|
||||
|
||||
class GuidePage extends React.Component {
|
||||
@ -10,17 +10,14 @@ class GuidePage extends React.Component {
|
||||
// Remove cookie and invalidate token on client.
|
||||
this.props.cookiesApi.removeCookie(OAUTH_TOKEN_COOKIE);
|
||||
this.props.authApi.invalidateToken();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<Scene
|
||||
loggedIn={!!this.props.authApi.token}
|
||||
onLogout={this.onLogout}
|
||||
/>
|
||||
<Scene loggedIn={!!this.props.authApi.token} onLogout={this.onLogout} />
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const enhance = compose(
|
||||
withAuthProvider,
|
||||
|
@ -5,20 +5,16 @@ import styled from '@emotion/styled';
|
||||
import {css, jsx} from '@emotion/core';
|
||||
import {Link as RouterLink} from '@reach/router';
|
||||
import {routes} from '../../constants';
|
||||
import {
|
||||
BasicPageWrapper,
|
||||
forSmallScreens,
|
||||
forMobile
|
||||
} from '../common';
|
||||
import {BasicPageWrapper, forSmallScreens, forMobile} from '../common';
|
||||
|
||||
import {ReactComponent as CloudOffSvg} from '../../images/svg/icons/cloud_off.svg'
|
||||
import {ReactComponent as NotificationsActiveSvg} from '../../images/svg/icons/notifications_active.svg'
|
||||
import {ReactComponent as PriorityHighSvg} from '../../images/svg/icons/priority_high.svg'
|
||||
import {ReactComponent as TuneSvg} from '../../images/svg/icons/tune.svg'
|
||||
import {ReactComponent as SpeedSvg} from '../../images/svg/icons/speed.svg'
|
||||
import {ReactComponent as GpsFixedSvg} from '../../images/svg/icons/gps_fixed.svg'
|
||||
import {ReactComponent as WbIridescentSvg} from '../../images/svg/icons/wb_iridescent.svg'
|
||||
import {ReactComponent as TimelineSvg} from '../../images/svg/icons/timeline.svg'
|
||||
import {ReactComponent as CloudOffSvg} from '../../images/svg/icons/cloud_off.svg';
|
||||
import {ReactComponent as NotificationsActiveSvg} from '../../images/svg/icons/notifications_active.svg';
|
||||
import {ReactComponent as PriorityHighSvg} from '../../images/svg/icons/priority_high.svg';
|
||||
import {ReactComponent as TuneSvg} from '../../images/svg/icons/tune.svg';
|
||||
import {ReactComponent as SpeedSvg} from '../../images/svg/icons/speed.svg';
|
||||
import {ReactComponent as GpsFixedSvg} from '../../images/svg/icons/gps_fixed.svg';
|
||||
import {ReactComponent as WbIridescentSvg} from '../../images/svg/icons/wb_iridescent.svg';
|
||||
import {ReactComponent as TimelineSvg} from '../../images/svg/icons/timeline.svg';
|
||||
|
||||
import ItemPng from '../../images/screenshots/item.png';
|
||||
import ItemTwoPng from '../../images/screenshots/item-2.png';
|
||||
@ -35,20 +31,24 @@ const WorkflowToggle = ({easeTimingMs = 200, items}) => {
|
||||
const activeItem = items[state];
|
||||
|
||||
return (
|
||||
<div css={css`
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
${forSmallScreens(`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
`}>
|
||||
<div css={css`
|
||||
flex: 1;
|
||||
<div
|
||||
css={css`
|
||||
margin-top: 32px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
${forSmallScreens(`
|
||||
flex-direction: column;
|
||||
`)};
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
flex: 1;
|
||||
${forSmallScreens(`
|
||||
margin-bottom: 24px;
|
||||
`)}
|
||||
`}>
|
||||
`)};
|
||||
`}
|
||||
>
|
||||
{items.map((item, xid) => (
|
||||
<div
|
||||
key={xid}
|
||||
@ -68,36 +68,49 @@ const WorkflowToggle = ({easeTimingMs = 200, items}) => {
|
||||
cursor: pointer;
|
||||
background: ${state === xid ? '#ffffff' : 'none'};
|
||||
transition: all 0.15s ease-in-out;
|
||||
`}>
|
||||
<h3 css={css`
|
||||
font-size: 22px;
|
||||
line-height: 26px;
|
||||
margin: 0 auto 4px;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-weight: 500;
|
||||
`}>{item.title}</h3>
|
||||
<p css={css`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
`}>{item.description}</p>
|
||||
`}
|
||||
>
|
||||
<h3
|
||||
css={css`
|
||||
font-size: 22px;
|
||||
line-height: 26px;
|
||||
margin: 0 auto 4px;
|
||||
font-family: medium-marketing-display-font, Georgia, Cambria,
|
||||
Times New Roman, Times, serif;
|
||||
font-weight: 500;
|
||||
`}
|
||||
>
|
||||
{item.title}
|
||||
</h3>
|
||||
<p
|
||||
css={css`
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div css={css`
|
||||
position: relative;
|
||||
flex: 3;
|
||||
`}>
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
flex: 3;
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src={activeItem.image}
|
||||
css={css`
|
||||
max-width: 100%;
|
||||
opacity: ${imageOpacity};
|
||||
will-change: opacity;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.175);
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.175);
|
||||
transition: opacity ${easeTimingMs}ms linear;
|
||||
border-radius: 4px;
|
||||
`} />
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { compose } from 'recompose';
|
||||
import { withAuthProvider } from '../../providers/Auth';
|
||||
import { withCookiesProvider } from '../../providers/Cookies';
|
||||
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
|
||||
import {compose} from 'recompose';
|
||||
import {withAuthProvider} from '../../providers/Auth';
|
||||
import {withCookiesProvider} from '../../providers/Cookies';
|
||||
import {OAUTH_TOKEN_COOKIE} from '../../constants/cookies';
|
||||
import Scene from './Scene';
|
||||
|
||||
class HomePage extends React.Component {
|
||||
@ -10,17 +10,14 @@ class HomePage extends React.Component {
|
||||
// Remove cookie and invalidate token on client.
|
||||
this.props.cookiesApi.removeCookie(OAUTH_TOKEN_COOKIE);
|
||||
this.props.authApi.invalidateToken();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<Scene
|
||||
loggedIn={!!this.props.authApi.token}
|
||||
onLogout={this.onLogout}
|
||||
/>
|
||||
<Scene loggedIn={!!this.props.authApi.token} onLogout={this.onLogout} />
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const enhance = compose(
|
||||
withAuthProvider,
|
||||
|
@ -1,11 +1,11 @@
|
||||
/** @jsx jsx */
|
||||
|
||||
import { Link } from "@reach/router";
|
||||
import {Link} from '@reach/router';
|
||||
import styled from '@emotion/styled';
|
||||
import {css, jsx} from '@emotion/core';
|
||||
import React from 'react';
|
||||
import { routes } from '../../constants';
|
||||
import { AuthenticationButton } from '../../components/buttons';
|
||||
import {routes} from '../../constants';
|
||||
import {AuthenticationButton} from '../../components/buttons';
|
||||
import LoadingIcon from '../../components/LoadingIcon';
|
||||
import ErrorMessage from '../../components/ErrorMessage';
|
||||
import ConfettiSection from '../../components/Confetti';
|
||||
@ -19,15 +19,16 @@ const Card = styled('div')`
|
||||
min-height: 100px;
|
||||
margin: 32px auto 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #E5E6EB;
|
||||
box-shadow: rgba(84, 70, 35, 0) 0px 2px 8px, rgba(84,70,35,0.15) 0px 1px 3px;
|
||||
border: 1px solid #e5e6eb;
|
||||
box-shadow: rgba(84, 70, 35, 0) 0px 2px 8px,
|
||||
rgba(84, 70, 35, 0.15) 0px 1px 3px;
|
||||
border-radius: 6px;
|
||||
padding: 24px 32px 52px;
|
||||
`;
|
||||
|
||||
const ButtonsContainer = styled('div')`
|
||||
display: flex;
|
||||
justifyContent: space-between;
|
||||
justifycontent: space-between;
|
||||
position: absolute;
|
||||
bottom: 24;
|
||||
left: 48;
|
||||
@ -53,29 +54,38 @@ const ButtonsContainer = styled('div')`
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Scene ({ loading, error, loggedOut, ...props }) {
|
||||
export default function Scene({loading, error, loggedOut, ...props}) {
|
||||
return (
|
||||
<div css={css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: ${WHITE};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
max-width: 1420px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
`}>
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: ${WHITE};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
max-width: 1420px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
`}
|
||||
>
|
||||
<ConfettiSection spacing={100} amount={20} />
|
||||
<Card>
|
||||
<h3>Authenticate with GitHub</h3>
|
||||
{error ? (
|
||||
<React.Fragment>
|
||||
<p>Log in with GitHub and we'll start organizing and sorting all of your notifications.</p>
|
||||
<ErrorMessage>Oops, looks like something went wrong. Try again?</ErrorMessage>
|
||||
<p>
|
||||
Log in with GitHub and we'll start organizing and sorting all of
|
||||
your notifications.
|
||||
</p>
|
||||
<ErrorMessage>
|
||||
Oops, looks like something went wrong. Try again?
|
||||
</ErrorMessage>
|
||||
<ButtonsContainer>
|
||||
<Link style={{boxShadow: '0 0 0'}} to={routes.HOME}>Go back</Link>
|
||||
<Link style={{boxShadow: '0 0 0'}} to={routes.HOME}>
|
||||
Go back
|
||||
</Link>
|
||||
<AuthenticationButton style={{boxShadow: '0 0 0'}} />
|
||||
</ButtonsContainer>
|
||||
</React.Fragment>
|
||||
@ -83,9 +93,14 @@ export default function Scene ({ loading, error, loggedOut, ...props }) {
|
||||
<LoadingIcon style={{marginTop: 50}} />
|
||||
) : loggedOut ? (
|
||||
<React.Fragment>
|
||||
<p>Log in with GitHub and we'll start organizing and sorting all of your notifications.</p>
|
||||
<p>
|
||||
Log in with GitHub and we'll start organizing and sorting all of
|
||||
your notifications.
|
||||
</p>
|
||||
<ButtonsContainer>
|
||||
<Link style={{boxShadow: '0 0 0'}} to={routes.HOME}>Go back</Link>
|
||||
<Link style={{boxShadow: '0 0 0'}} to={routes.HOME}>
|
||||
Go back
|
||||
</Link>
|
||||
<AuthenticationButton style={{boxShadow: '0 0 0'}} />
|
||||
</ButtonsContainer>
|
||||
</React.Fragment>
|
||||
|
@ -1,11 +1,11 @@
|
||||
/** @jsx jsx */
|
||||
|
||||
import { Link } from "@reach/router";
|
||||
import {Link} from '@reach/router';
|
||||
import styled from '@emotion/styled';
|
||||
import {css, jsx} from '@emotion/core';
|
||||
import React from 'react';
|
||||
import { routes } from '../../constants';
|
||||
import { AuthenticationButton } from '../../components/buttons';
|
||||
import {routes} from '../../constants';
|
||||
import {AuthenticationButton} from '../../components/buttons';
|
||||
import LoadingIcon from '../../components/LoadingIcon';
|
||||
import ErrorMessage from '../../components/ErrorMessage';
|
||||
|
||||
@ -27,8 +27,11 @@ const Button = styled(Link)`
|
||||
font-size: 1rem;
|
||||
line-height: 1.75;
|
||||
border-radius: 5px;
|
||||
-webkit-transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
-webkit-transition: color 0.15s ease-in-out,
|
||||
background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||
box-shadow 0.15s ease-in-out;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,
|
||||
border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
@ -47,8 +50,9 @@ const Card = styled('div')`
|
||||
min-height: 100px;
|
||||
margin: 32px auto 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid #E5E6EB;
|
||||
box-shadow: rgba(84, 70, 35, 0) 0px 2px 8px, rgba(84,70,35,0.15) 0px 1px 3px;
|
||||
border: 1px solid #e5e6eb;
|
||||
box-shadow: rgba(84, 70, 35, 0) 0px 2px 8px,
|
||||
rgba(84, 70, 35, 0.15) 0px 1px 3px;
|
||||
border-radius: 6px;
|
||||
padding: 24px 32px 52px;
|
||||
`;
|
||||
@ -79,25 +83,34 @@ const ButtonsContainer = styled('div')`
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Scene ({ loading, error, loggedOut, ...props }) {
|
||||
export default function Scene({loading, error, loggedOut, ...props}) {
|
||||
return (
|
||||
<div css={css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(transparent 50%, #fffefd), url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==) repeat;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
`}>
|
||||
<div
|
||||
css={css`
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: radial-gradient(transparent 50%, #fffefd),
|
||||
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==)
|
||||
repeat;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
`}
|
||||
>
|
||||
<Card>
|
||||
<h3>Authenticate with GitHub</h3>
|
||||
{error ? (
|
||||
<React.Fragment>
|
||||
<p>Log in with GitHub and we'll start organizing and sorting all of your notifications.</p>
|
||||
<ErrorMessage>Oops, looks like something went wrong. Try again?</ErrorMessage>
|
||||
<p>
|
||||
Log in with GitHub and we'll start organizing and sorting all of
|
||||
your notifications.
|
||||
</p>
|
||||
<ErrorMessage>
|
||||
Oops, looks like something went wrong. Try again?
|
||||
</ErrorMessage>
|
||||
<ButtonsContainer>
|
||||
<Button to={routes.HOME}>Go back</Button>
|
||||
<AuthenticationButton />
|
||||
@ -107,9 +120,14 @@ export default function Scene ({ loading, error, loggedOut, ...props }) {
|
||||
<LoadingIcon style={{marginTop: 50}} />
|
||||
) : loggedOut ? (
|
||||
<React.Fragment>
|
||||
<p>Log in with GitHub and we'll start organizing and sorting all of your notifications.</p>
|
||||
<p>
|
||||
Log in with GitHub and we'll start organizing and sorting all of
|
||||
your notifications.
|
||||
</p>
|
||||
<ButtonsContainer>
|
||||
<Button style={{boxShadow: '0 0 0'}} to={routes.HOME}>Go back</Button>
|
||||
<Button style={{boxShadow: '0 0 0'}} to={routes.HOME}>
|
||||
Go back
|
||||
</Button>
|
||||
<AuthenticationButton />
|
||||
</ButtonsContainer>
|
||||
</React.Fragment>
|
||||
|
@ -9,7 +9,7 @@ export default class TokenHandler extends React.Component {
|
||||
this.props.onSetLoading(true);
|
||||
fetch(`https://meteorite-gatekeeper.herokuapp.com/authenticate/${code}`)
|
||||
.then(response => response.json())
|
||||
.then(({ token, error }) => {
|
||||
.then(({token, error}) => {
|
||||
this.props.onSetLoading(false);
|
||||
if (error) {
|
||||
this.props.onSetError(true);
|
||||
@ -20,7 +20,7 @@ export default class TokenHandler extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,30 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from '@reach/router';
|
||||
import { compose } from 'recompose';
|
||||
import { withAuthProvider } from '../../providers/Auth';
|
||||
import { withNotificationsProvider } from '../../providers/Notifications';
|
||||
import { withCookiesProvider } from '../../providers/Cookies';
|
||||
import {Redirect} from '@reach/router';
|
||||
import {compose} from 'recompose';
|
||||
import {withAuthProvider} from '../../providers/Auth';
|
||||
import {withNotificationsProvider} from '../../providers/Notifications';
|
||||
import {withCookiesProvider} from '../../providers/Cookies';
|
||||
import TokenHandler from './TokenHandler';
|
||||
import Scene from './Scene.new';
|
||||
import { routes } from '../../constants';
|
||||
import {routes} from '../../constants';
|
||||
|
||||
class LoginPage extends React.Component {
|
||||
state = {
|
||||
loading: false,
|
||||
error: null
|
||||
}
|
||||
};
|
||||
|
||||
onSetLoading = loading => {
|
||||
this.setState({ loading });
|
||||
}
|
||||
this.setState({loading});
|
||||
};
|
||||
|
||||
onSetError = error => {
|
||||
this.setState({ error });
|
||||
}
|
||||
this.setState({error});
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (this.props.authApi.token) {
|
||||
return <Redirect noThrow to={routes.NOTIFICATIONS} />
|
||||
return <Redirect noThrow to={routes.NOTIFICATIONS} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -4,7 +4,7 @@ import {default as RedesignScene} from './redesign/Scene';
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable no-script-url */
|
||||
|
||||
export default function Scene ({
|
||||
export default function Scene({
|
||||
onMarkAsRead,
|
||||
fetchingNotificationsError,
|
||||
...props
|
||||
|
@ -49,25 +49,28 @@ const BaseBlob = styled('div')`
|
||||
`;
|
||||
|
||||
const Blob1 = styled(BaseBlob)`
|
||||
background: url(${(B1)}) center center no-repeat;
|
||||
background: url(${B1}) center center no-repeat;
|
||||
animation: ${blobFrames1} 25s infinite;
|
||||
`;
|
||||
|
||||
const Blob2 = styled(BaseBlob)`
|
||||
background: url(${(B2)}) center center no-repeat;
|
||||
background: url(${B2}) center center no-repeat;
|
||||
animation: ${blobFrames2} 15s infinite;
|
||||
`;
|
||||
|
||||
const Header = styled('h1')(({dark}) => `
|
||||
const Header = styled('h1')(
|
||||
({dark}) => `
|
||||
text-align: center;
|
||||
letter-spacing: -0.25px;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
color: ${dark ? '#ffffff' : '#131212'};
|
||||
`);
|
||||
`
|
||||
);
|
||||
|
||||
const Byline = styled('p')(({dark}) => `
|
||||
const Byline = styled('p')(
|
||||
({dark}) => `
|
||||
text-align: center;
|
||||
font-family: medium-content-sans-serif-font,Inter UI,system-ui,sans-serif;
|
||||
font-weight: 500;
|
||||
@ -77,9 +80,11 @@ const Byline = styled('p')(({dark}) => `
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: ${dark ? '#667386' : '#9d9b97'};
|
||||
`);
|
||||
`
|
||||
);
|
||||
|
||||
const Container = styled('div')(({opacity}) => `
|
||||
const Container = styled('div')(
|
||||
({opacity}) => `
|
||||
position: relative;
|
||||
background: none;
|
||||
height: 550px;
|
||||
@ -90,9 +95,10 @@ const Container = styled('div')(({opacity}) => `
|
||||
overflow: hidden;
|
||||
opacity: ${opacity};
|
||||
transition: all 150ms ease-in;
|
||||
`);
|
||||
`
|
||||
);
|
||||
|
||||
function EmptyState ({dark}) {
|
||||
function EmptyState({dark}) {
|
||||
const [opacity, setOpacity] = React.useState(0);
|
||||
const timer = React.useRef();
|
||||
React.useEffect(() => {
|
||||
@ -102,8 +108,16 @@ function EmptyState ({dark}) {
|
||||
|
||||
return (
|
||||
<Container opacity={opacity}>
|
||||
<Blob1 css={css`opacity: ${dark ? 0.15 : 1};`} />
|
||||
<Blob2 css={css`opacity: ${dark ? 0.15 : 1};`} />
|
||||
<Blob1
|
||||
css={css`
|
||||
opacity: ${dark ? 0.15 : 1};
|
||||
`}
|
||||
/>
|
||||
<Blob2
|
||||
css={css`
|
||||
opacity: ${dark ? 0.15 : 1};
|
||||
`}
|
||||
/>
|
||||
<Header dark={dark}>
|
||||
{"🌱 You're all caught up"}
|
||||
<Byline dark={dark}>
|
||||
|
@ -17,7 +17,11 @@ export const withTheme = C => p => (
|
||||
</ThemeContext.Consumer>
|
||||
);
|
||||
|
||||
const enhance = compose(withTheme, withTooltip, withOptimizedTouchEvents);
|
||||
const enhance = compose(
|
||||
withTheme,
|
||||
withTooltip,
|
||||
withOptimizedTouchEvents
|
||||
);
|
||||
|
||||
export const ThemeColor = darkMode => (darkMode ? '#E91356' : '#27B768');
|
||||
export const WHITE = 'rgb(255, 254, 252)';
|
||||
@ -860,7 +864,7 @@ export function ProfileSection({dark, user, onLogout}) {
|
||||
font-size: 20px;
|
||||
`}
|
||||
className="fas fa-cog"
|
||||
></i>
|
||||
/>
|
||||
)}
|
||||
<ProfileName>{user && user.name ? user.name : 'Settings'}</ProfileName>
|
||||
<i
|
||||
@ -868,7 +872,7 @@ export function ProfileSection({dark, user, onLogout}) {
|
||||
css={css`
|
||||
transform: ${menuShow ? 'rotate(180deg)' : 'rotate(0deg)'};
|
||||
`}
|
||||
></i>
|
||||
/>
|
||||
</ProfileContainer>
|
||||
<InteractionMenu
|
||||
show={menuShow}
|
||||
@ -997,8 +1001,8 @@ export const IconLink = enhance(
|
||||
? DarkTheme.Gray
|
||||
: '#bfc5d1'
|
||||
: p.dark
|
||||
? WHITE
|
||||
: 'inherit'
|
||||
? WHITE
|
||||
: 'inherit'
|
||||
};
|
||||
}
|
||||
&:before {
|
||||
|
@ -124,7 +124,7 @@ export function getPRIssueIcon({type, reasons, dark, pinned}) {
|
||||
color: ${PinnedColor};
|
||||
font-size: 18px;
|
||||
`}
|
||||
></i>
|
||||
/>
|
||||
</NotificationIconWrapper>
|
||||
);
|
||||
}
|
||||
@ -143,7 +143,7 @@ export function getPRIssueIcon({type, reasons, dark, pinned}) {
|
||||
color: ${ThemeColor(dark)};
|
||||
font-size: 18px;
|
||||
`}
|
||||
></i>
|
||||
/>
|
||||
</NotificationIconWrapper>
|
||||
);
|
||||
case 'Issue':
|
||||
@ -159,7 +159,7 @@ export function getPRIssueIcon({type, reasons, dark, pinned}) {
|
||||
color: ${ThemeColor(dark)};
|
||||
font-size: 18px;
|
||||
`}
|
||||
></i>
|
||||
/>
|
||||
</NotificationIconWrapper>
|
||||
);
|
||||
default:
|
||||
@ -213,7 +213,7 @@ export function iconsOfBadges(badges) {
|
||||
css={css`
|
||||
color: ${BadgeColors.RED};
|
||||
`}
|
||||
></i>
|
||||
/>
|
||||
);
|
||||
case Badges.COMMENTS:
|
||||
return (
|
||||
@ -222,7 +222,7 @@ export function iconsOfBadges(badges) {
|
||||
css={css`
|
||||
color: ${BadgeColors.BLUE};
|
||||
`}
|
||||
></i>
|
||||
/>
|
||||
);
|
||||
case Badges.OLD:
|
||||
return (
|
||||
@ -231,7 +231,7 @@ export function iconsOfBadges(badges) {
|
||||
css={css`
|
||||
color: ${BadgeColors.YELLOW};
|
||||
`}
|
||||
></i>
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
|
@ -44,20 +44,18 @@ const Container = styled('div')`
|
||||
${forSmallScreens(`
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
`)}
|
||||
${forMobile(`
|
||||
`)} ${forMobile(`
|
||||
margin-bottom: 2.5rem;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const LightContainer = styled(Container)`
|
||||
${forSmallScreens(`
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
`)}
|
||||
${forMobile(`
|
||||
`)} ${forMobile(`
|
||||
margin-bottom: 2.5rem;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const FlexItem = styled('div')`
|
||||
@ -146,7 +144,7 @@ const HeroTitle = styled('h1')`
|
||||
font-size: 46px;
|
||||
line-height: 54px;
|
||||
margin: 0 auto 6px;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const HeroSubtitle = styled('h1')`
|
||||
@ -163,7 +161,7 @@ const HeroSubtitle = styled('h1')`
|
||||
margin: 0 auto 32px;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const Title = styled('h1')`
|
||||
@ -209,7 +207,7 @@ const HeroLeft = styled(FlexItem)`
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const HeroRight = styled(FlexItem)`
|
||||
@ -218,7 +216,7 @@ const HeroRight = styled(FlexItem)`
|
||||
${forMobile(`
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const PricingContainer = styled('div')`
|
||||
@ -233,9 +231,7 @@ const PricingContainer = styled('div')`
|
||||
text-align: left;
|
||||
${forMobile(`
|
||||
width: 100%;
|
||||
`)}
|
||||
|
||||
h4 {
|
||||
`)} h4 {
|
||||
margin: -2px 0 0;
|
||||
font-size: 20px;
|
||||
color: #26b768;
|
||||
@ -285,9 +281,7 @@ const FloatingLinkBox = styled('div')`
|
||||
margin: 0 24px 0 0;
|
||||
${forMobile(`
|
||||
margin: 0 0 24px 0;
|
||||
`)}
|
||||
|
||||
h4 {
|
||||
`)} h4 {
|
||||
margin: 0;
|
||||
color: #47494b;
|
||||
font-size: 18px;
|
||||
@ -326,8 +320,7 @@ const FAQItem = styled('div')`
|
||||
flex: 1 0 34%;
|
||||
${forMobile(`
|
||||
flex: 1 0 51%;
|
||||
`)}
|
||||
flex-direction: column;
|
||||
`)} flex-direction: column;
|
||||
display: flex;
|
||||
align-items: left;
|
||||
justify-content: center;
|
||||
@ -368,7 +361,7 @@ export default function Scene(props) {
|
||||
css={css`
|
||||
${forMobile(`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
`)};
|
||||
`}
|
||||
>
|
||||
<HeroLeft>
|
||||
@ -399,7 +392,7 @@ export default function Scene(props) {
|
||||
|
||||
<Container
|
||||
css={css`
|
||||
${forMobile(`flex-direction: column;`)}
|
||||
${forMobile(`flex-direction: column;`)};
|
||||
`}
|
||||
>
|
||||
<FloatingLinkBox>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { compose } from 'recompose';
|
||||
import { withAuthProvider } from '../../providers/Auth';
|
||||
import { withCookiesProvider } from '../../providers/Cookies';
|
||||
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
|
||||
import {compose} from 'recompose';
|
||||
import {withAuthProvider} from '../../providers/Auth';
|
||||
import {withCookiesProvider} from '../../providers/Cookies';
|
||||
import {OAUTH_TOKEN_COOKIE} from '../../constants/cookies';
|
||||
import Scene from './Scene';
|
||||
|
||||
class PricingPage extends React.Component {
|
||||
@ -10,17 +10,14 @@ class PricingPage extends React.Component {
|
||||
// Remove cookie and invalidate token on client.
|
||||
this.props.cookiesApi.removeCookie(OAUTH_TOKEN_COOKIE);
|
||||
this.props.authApi.invalidateToken();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<Scene
|
||||
loggedIn={!!this.props.authApi.token}
|
||||
onLogout={this.onLogout}
|
||||
/>
|
||||
<Scene loggedIn={!!this.props.authApi.token} onLogout={this.onLogout} />
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const enhance = compose(
|
||||
withAuthProvider,
|
||||
|
@ -49,10 +49,9 @@ const Container = styled('div')`
|
||||
${forSmallScreens(`
|
||||
padding-left: 2.0rem;
|
||||
padding-right: 2.0rem;
|
||||
`)}
|
||||
${forMobile(`
|
||||
`)} ${forMobile(`
|
||||
margin-bottom: 2.5rem;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const FlexItem = styled('div')`
|
||||
@ -83,9 +82,7 @@ const ButtonLink = styled('a')`
|
||||
transition: all 0.15s ease-in-out;
|
||||
${forMobile(`
|
||||
padding: 0.125rem 0.25rem;
|
||||
`)}
|
||||
|
||||
&:hover {
|
||||
`)} &:hover {
|
||||
background-color: #f4f4f4;
|
||||
border-color: #f4f4f4;
|
||||
}
|
||||
@ -120,9 +117,7 @@ const Button = styled(RouterLink)`
|
||||
transition: all 0.15s ease-in-out;
|
||||
${forMobile(`
|
||||
padding: 0.125rem 0.25rem;
|
||||
`)}
|
||||
|
||||
&:hover {
|
||||
`)} &:hover {
|
||||
background-color: #f4f4f4;
|
||||
border-color: #f4f4f4;
|
||||
}
|
||||
@ -163,7 +158,7 @@ const LogoTitle = styled('span')`
|
||||
|
||||
${forMobile(`
|
||||
display: none;
|
||||
`)}
|
||||
`)};
|
||||
`;
|
||||
|
||||
const LogoSection = () => (
|
||||
@ -176,8 +171,7 @@ const LogoSection = () => (
|
||||
width: 125px;
|
||||
${forMobile(`
|
||||
width: 36px;
|
||||
`)}
|
||||
div {
|
||||
`)} div {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@ -277,7 +271,7 @@ export function BasicPageWrapper({loggedIn, onLogout, children}) {
|
||||
border-radius: 100%;
|
||||
${forMobile(`
|
||||
right: -1px;
|
||||
`)}
|
||||
`)};
|
||||
}
|
||||
`}
|
||||
>
|
||||
|
@ -1,35 +1,36 @@
|
||||
import React from 'react';
|
||||
import { withCookiesProvider } from './Cookies';
|
||||
import { OAUTH_TOKEN_COOKIE } from '../constants/cookies';
|
||||
import {withCookiesProvider} from './Cookies';
|
||||
import {OAUTH_TOKEN_COOKIE} from '../constants/cookies';
|
||||
|
||||
const {Provider, Consumer} = React.createContext();
|
||||
|
||||
class AuthProvider extends React.Component {
|
||||
state = {
|
||||
token: (
|
||||
token:
|
||||
this.props.cookiesApi.getCookie(OAUTH_TOKEN_COOKIE) ||
|
||||
(process.localEnv.NODE_ENV === 'local'
|
||||
? process.localEnv.OAUTH_TOKEN
|
||||
: undefined)
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
setToken = token => {
|
||||
this.props.cookiesApi.setCookie(OAUTH_TOKEN_COOKIE, token);
|
||||
this.setState({ token });
|
||||
}
|
||||
this.setState({token});
|
||||
};
|
||||
|
||||
invalidateToken = () => {
|
||||
this.setState({ token: null });
|
||||
}
|
||||
this.setState({token: null});
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<Provider value={{
|
||||
token: this.state.token,
|
||||
setToken: this.setToken,
|
||||
invalidateToken: this.invalidateToken
|
||||
}}>
|
||||
<Provider
|
||||
value={{
|
||||
token: this.state.token,
|
||||
setToken: this.setToken,
|
||||
invalidateToken: this.invalidateToken
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
</Provider>
|
||||
);
|
||||
|
@ -4,42 +4,46 @@ import moment from 'moment';
|
||||
class CookiesProvider extends React.Component {
|
||||
state = {
|
||||
cookies: {}
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
this.hydrateCookies();
|
||||
}
|
||||
|
||||
mapifyCookies = () => {
|
||||
const cookiesPairs = document.cookie.split(';').map(cookie => cookie.trim());
|
||||
const cookiesPairs = document.cookie
|
||||
.split(';')
|
||||
.map(cookie => cookie.trim());
|
||||
const cookies = cookiesPairs.reduce((map, cookiePair) => {
|
||||
const [key, value] = cookiePair.split('=');
|
||||
map[key] = value;
|
||||
return map;
|
||||
}, {});
|
||||
return cookies;
|
||||
}
|
||||
};
|
||||
|
||||
hydrateCookies = () => {
|
||||
const cookies = this.mapifyCookies();
|
||||
this.setState({ cookies });
|
||||
}
|
||||
this.setState({cookies});
|
||||
};
|
||||
|
||||
setCookie = (name, value) => {
|
||||
document.cookie = `${name}=${value}`;
|
||||
this.hydrateCookies()
|
||||
}
|
||||
this.hydrateCookies();
|
||||
};
|
||||
|
||||
getCookie = name => {
|
||||
return this.state.cookies[name];
|
||||
}
|
||||
};
|
||||
|
||||
removeCookie = name => {
|
||||
document.cookie = `${name}=''; expires=${moment().subtract(1, 'day').toString()}`;
|
||||
document.cookie = `${name}=''; expires=${moment()
|
||||
.subtract(1, 'day')
|
||||
.toString()}`;
|
||||
this.hydrateCookies();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return this.props.children({
|
||||
...this.state,
|
||||
setCookie: this.setCookie,
|
||||
@ -55,7 +59,4 @@ const withCookiesProvider = WrappedComponent => props => (
|
||||
</CookiesProvider>
|
||||
);
|
||||
|
||||
export {
|
||||
CookiesProvider,
|
||||
withCookiesProvider
|
||||
};
|
||||
export {CookiesProvider, withCookiesProvider};
|
||||
|
@ -7,14 +7,14 @@ import {Status} from '../constants/status';
|
||||
const BASE_GITHUB_API_URL = 'https://api.github.com';
|
||||
const PER_PAGE = 50;
|
||||
|
||||
function transformUrlFromResponse (url) {
|
||||
function transformUrlFromResponse(url) {
|
||||
return url
|
||||
.replace('api.github.com', 'github.com')
|
||||
.replace('/repos/', '/')
|
||||
.replace('/pulls/', '/pull/');
|
||||
}
|
||||
|
||||
function processHeadersAndBodyJson (response) {
|
||||
function processHeadersAndBodyJson(response) {
|
||||
const entries = response.headers.entries();
|
||||
const headers = {};
|
||||
for (let [name, value] of entries) {
|
||||
@ -25,7 +25,7 @@ function processHeadersAndBodyJson (response) {
|
||||
const links = {};
|
||||
|
||||
if (rawLinks) {
|
||||
rawLinks.split(',').forEach((p) => {
|
||||
rawLinks.split(',').forEach(p => {
|
||||
const section = p.split(';');
|
||||
if (section.length !== 2) {
|
||||
throw new Error("section could not be split on ';'");
|
||||
@ -34,7 +34,7 @@ function processHeadersAndBodyJson (response) {
|
||||
const page = section[0].match(/page=(\d)/)[1];
|
||||
const name = section[1].replace(/rel="(.*)"/, '$1').trim();
|
||||
links[name] = {url, page};
|
||||
})
|
||||
});
|
||||
}
|
||||
// links.next.page
|
||||
headers['link'] = links;
|
||||
@ -61,7 +61,7 @@ function processHeadersAndBodyJson (response) {
|
||||
}
|
||||
|
||||
class NotificationsProvider extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.last_modified = null;
|
||||
@ -73,18 +73,19 @@ class NotificationsProvider extends React.Component {
|
||||
error: null,
|
||||
newChanges: null,
|
||||
notificationsPermission:
|
||||
this.props.getUserItem('notificationsPermission') ||
|
||||
'default',
|
||||
}
|
||||
this.props.getUserItem('notificationsPermission') || 'default'
|
||||
};
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
// Don't try to rerender if we're just setting the new changes.
|
||||
if (this.state.newChanges !== nextState.newChanges) {
|
||||
return false;
|
||||
}
|
||||
// Update if our state changes.
|
||||
if ((this.state.loading !== nextState.loading) ||
|
||||
(this.state.error !== nextState.error)) {
|
||||
if (
|
||||
this.state.loading !== nextState.loading ||
|
||||
this.state.error !== nextState.error
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Update if the token changes at all (sign in & sign out).
|
||||
@ -93,9 +94,7 @@ class NotificationsProvider extends React.Component {
|
||||
}
|
||||
// Only update if our notifications prop changes.
|
||||
// All other props "changing" should NOT trigger a rerender.
|
||||
return (
|
||||
this.props.notifications !== nextProps.notifications
|
||||
);
|
||||
return this.props.notifications !== nextProps.notifications;
|
||||
}
|
||||
|
||||
// The web notificaitons API doesn't let users revoke notifications permission
|
||||
@ -106,12 +105,12 @@ class NotificationsProvider extends React.Component {
|
||||
this.setState({notificationsPermission: permission});
|
||||
this.props.setUserItem('notificationsPermission', permission);
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
request = (url, method = 'GET') => {
|
||||
const headers = {
|
||||
'Authorization': `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// @TODO probably add timestamp
|
||||
@ -123,26 +122,27 @@ class NotificationsProvider extends React.Component {
|
||||
return fetch(url, {
|
||||
method,
|
||||
headers
|
||||
}).then(processHeadersAndBodyJson)
|
||||
})
|
||||
.then(processHeadersAndBodyJson)
|
||||
.then(({json}) => {
|
||||
console.info(`Response from %c${url}`, 'font-weight: bold;')
|
||||
console.info(json)
|
||||
console.info('')
|
||||
console.info(`Response from %c${url}`, 'font-weight: bold;');
|
||||
console.info(json);
|
||||
console.info('');
|
||||
this.props.setUserItem(url, json);
|
||||
return json;
|
||||
})
|
||||
.catch(({status, text}) => {
|
||||
console.info(`Response from %c${url}`, 'font-weight: bold;')
|
||||
console.info(`${status}: ${text}`)
|
||||
console.info('')
|
||||
console.info(`Response from %c${url}`, 'font-weight: bold;');
|
||||
console.info(`${status}: ${text}`);
|
||||
console.info('');
|
||||
this.props.setUserItem(url, null);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestUser = () => {
|
||||
const headers = {
|
||||
'Authorization': `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
// @TODO probably add timestamp
|
||||
@ -154,12 +154,13 @@ class NotificationsProvider extends React.Component {
|
||||
return fetch(`${BASE_GITHUB_API_URL}/user`, {
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
}).then(processHeadersAndBodyJson)
|
||||
})
|
||||
.then(processHeadersAndBodyJson)
|
||||
.then(({json}) => {
|
||||
this.props.setUserItem('user-model', json);
|
||||
return json;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestPage = (page = 1, optimizePolling = true) => {
|
||||
// Fetch all notifications from a month ago, including ones that have been read.
|
||||
@ -169,21 +170,28 @@ class NotificationsProvider extends React.Component {
|
||||
// the middle of the things you're working on and only having part of the story.
|
||||
//
|
||||
// 1 month is pretty arbitrary, we can raise this if we want.
|
||||
const since = moment().subtract(1, 'month').toISOString().split('.')[0] + 'Z';
|
||||
const since =
|
||||
moment()
|
||||
.subtract(1, 'month')
|
||||
.toISOString()
|
||||
.split('.')[0] + 'Z';
|
||||
|
||||
const headers = {
|
||||
'Authorization': `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
if (optimizePolling && this.last_modified) {
|
||||
headers['If-Modified-Since'] = this.last_modified;
|
||||
}
|
||||
|
||||
return fetch(`${BASE_GITHUB_API_URL}/notifications?page=${page}&per_page=${PER_PAGE}&since=${since}&all=true`, {
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
})
|
||||
return fetch(
|
||||
`${BASE_GITHUB_API_URL}/notifications?page=${page}&per_page=${PER_PAGE}&since=${since}&all=true`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
}
|
||||
)
|
||||
.then(processHeadersAndBodyJson)
|
||||
.then(({headers, json}) => {
|
||||
// If there were updates, make sure we get the newest last-modified.
|
||||
@ -203,7 +211,7 @@ class NotificationsProvider extends React.Component {
|
||||
}
|
||||
return this.processNotificationsChunk(nextPage, json);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestFetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
if (this.state.syncing) {
|
||||
@ -212,9 +220,10 @@ class NotificationsProvider extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({syncing: true});
|
||||
return this.requestPage(page, optimizePolling)
|
||||
.finally(() => this.setState({syncing: false}));
|
||||
}
|
||||
return this.requestPage(page, optimizePolling).finally(() =>
|
||||
this.setState({syncing: false})
|
||||
);
|
||||
};
|
||||
|
||||
fetchNotifications = (page = 1, optimizePolling = true) => {
|
||||
if (!this.props.token) {
|
||||
@ -227,12 +236,12 @@ class NotificationsProvider extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
this.setState({loading: true});
|
||||
return this.requestFetchNotifications(page, optimizePolling)
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error =>this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({loading: false}));
|
||||
};
|
||||
|
||||
processNotificationsChunk = (nextPage, notificationsChunk) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -278,12 +287,12 @@ class NotificationsProvider extends React.Component {
|
||||
return resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestMarkAsRead = thread_id => {
|
||||
const headers = {
|
||||
'Authorization': `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `token ${this.props.token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
return fetch(`${BASE_GITHUB_API_URL}/notifications/threads/${thread_id}`, {
|
||||
@ -291,20 +300,18 @@ class NotificationsProvider extends React.Component {
|
||||
headers: headers
|
||||
})
|
||||
.then(response => {
|
||||
return response.status === 205
|
||||
? Promise.resolve()
|
||||
: Promise.reject();
|
||||
return response.status === 205 ? Promise.resolve() : Promise.reject();
|
||||
})
|
||||
.then(() => {
|
||||
this.props.removeItemFromStorage(thread_id);
|
||||
this.props.refreshNotifications();
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
markAsRead = thread_id => {
|
||||
if (!this.props.token) {
|
||||
console.error('Unauthenitcated, aborting request.')
|
||||
console.error('Unauthenitcated, aborting request.');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -313,12 +320,12 @@ class NotificationsProvider extends React.Component {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
this.setState({loading: true});
|
||||
return this.requestMarkAsRead(thread_id)
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
.finally(() => this.setState({loading: false}));
|
||||
};
|
||||
|
||||
requestClearCache = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -327,15 +334,15 @@ class NotificationsProvider extends React.Component {
|
||||
this.last_modified = null;
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
clearCache = () => {
|
||||
this.setState({ loading: true });
|
||||
this.setState({loading: true});
|
||||
return this.requestClearCache()
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
.finally(() => this.setState({loading: false}));
|
||||
};
|
||||
|
||||
requestStageThread = thread_id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -350,10 +357,12 @@ class NotificationsProvider extends React.Component {
|
||||
this.props.refreshNotifications();
|
||||
return resolve();
|
||||
} else {
|
||||
throw new Error(`Attempted to stage thread ${thread_id} that wasn't found in the cache.`);
|
||||
throw new Error(
|
||||
`Attempted to stage thread ${thread_id} that wasn't found in the cache.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestPinThread = thread_id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -368,10 +377,12 @@ class NotificationsProvider extends React.Component {
|
||||
this.props.refreshNotifications();
|
||||
return resolve();
|
||||
} else {
|
||||
throw new Error(`Attempted to stage thread ${thread_id} that wasn't found in the cache.`);
|
||||
throw new Error(
|
||||
`Attempted to stage thread ${thread_id} that wasn't found in the cache.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestReadPinThread = thread_id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -386,10 +397,12 @@ class NotificationsProvider extends React.Component {
|
||||
this.props.refreshNotifications();
|
||||
return resolve();
|
||||
} else {
|
||||
throw new Error(`Attempted to stage thread ${thread_id} that wasn't found in the cache.`);
|
||||
throw new Error(
|
||||
`Attempted to stage thread ${thread_id} that wasn't found in the cache.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestStageAll = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -410,7 +423,7 @@ class NotificationsProvider extends React.Component {
|
||||
this.props.refreshNotifications();
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
requestRestoreThread = thread_id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -425,54 +438,56 @@ class NotificationsProvider extends React.Component {
|
||||
this.props.refreshNotifications();
|
||||
return resolve();
|
||||
} else {
|
||||
throw new Error(`Attempted to restore thread ${thread_id} that wasn't found in the cache.`);
|
||||
throw new Error(
|
||||
`Attempted to restore thread ${thread_id} that wasn't found in the cache.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
markAllAsStaged = () => {
|
||||
this.setState({ loading: true });
|
||||
this.setState({loading: true});
|
||||
return this.requestStageAll()
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}))
|
||||
.finally(() => this.setState({ loading: false }));
|
||||
}
|
||||
.finally(() => this.setState({loading: false}));
|
||||
};
|
||||
|
||||
stageThread = thread_id => {
|
||||
return this.requestStageThread(thread_id)
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}));
|
||||
}
|
||||
};
|
||||
|
||||
pinThread = thread_id => {
|
||||
return this.requestPinThread(thread_id)
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}));
|
||||
}
|
||||
};
|
||||
|
||||
readPinThread = thread_id => {
|
||||
return this.requestReadPinThread(thread_id)
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}));
|
||||
}
|
||||
};
|
||||
|
||||
restoreThread = thread_id => {
|
||||
return this.requestRestoreThread(thread_id)
|
||||
.then(() => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}));
|
||||
}
|
||||
};
|
||||
|
||||
updateNotification = (n, cachedNotification = null) => {
|
||||
const prevReason = cachedNotification ? cachedNotification.reasons : null;
|
||||
const isPinned = cachedNotification ? (
|
||||
cachedNotification.status === Status.Pinned ||
|
||||
cachedNotification.status === Status.PinnedRead
|
||||
) : false;
|
||||
const isPinned = cachedNotification
|
||||
? cachedNotification.status === Status.Pinned ||
|
||||
cachedNotification.status === Status.PinnedRead
|
||||
: false;
|
||||
let reasons = [];
|
||||
const newReason = {
|
||||
reason: n.reason,
|
||||
time: n.updated_at
|
||||
}
|
||||
};
|
||||
|
||||
if (prevReason) {
|
||||
reasons = prevReason.concat(newReason);
|
||||
@ -485,7 +500,9 @@ class NotificationsProvider extends React.Component {
|
||||
: null;
|
||||
|
||||
const url = commentNumber
|
||||
? transformUrlFromResponse(n.subject.url) + '#issuecomment-' + commentNumber
|
||||
? transformUrlFromResponse(n.subject.url) +
|
||||
'#issuecomment-' +
|
||||
commentNumber
|
||||
: transformUrlFromResponse(n.subject.url);
|
||||
|
||||
let nextStatus = null;
|
||||
@ -522,9 +539,9 @@ class NotificationsProvider extends React.Component {
|
||||
};
|
||||
this.props.setItemInStorage(n.id, value);
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return this.props.children({
|
||||
...this.state,
|
||||
request: this.request,
|
||||
@ -539,14 +556,14 @@ class NotificationsProvider extends React.Component {
|
||||
restoreThread: this.restoreThread,
|
||||
pinThread: this.pinThread,
|
||||
readPinThread: this.readPinThread,
|
||||
setNotificationsPermission: this.setNotificationsPermission,
|
||||
setNotificationsPermission: this.setNotificationsPermission
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const withNotificationsProvider = WrappedComponent => props => (
|
||||
<AuthConsumer>
|
||||
{({ token }) => (
|
||||
{({token}) => (
|
||||
<StorageProvider>
|
||||
{({
|
||||
refreshNotifications,
|
||||
@ -569,8 +586,11 @@ const withNotificationsProvider = WrappedComponent => props => (
|
||||
removeItemFromStorage={removeItem}
|
||||
token={token}
|
||||
>
|
||||
{(notificationsApi) => (
|
||||
<WrappedComponent {...props} notificationsApi={notificationsApi} />
|
||||
{notificationsApi => (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
notificationsApi={notificationsApi}
|
||||
/>
|
||||
)}
|
||||
</NotificationsProvider>
|
||||
)}
|
||||
@ -579,7 +599,4 @@ const withNotificationsProvider = WrappedComponent => props => (
|
||||
</AuthConsumer>
|
||||
);
|
||||
|
||||
export {
|
||||
NotificationsProvider,
|
||||
withNotificationsProvider
|
||||
};
|
||||
export {NotificationsProvider, withNotificationsProvider};
|
||||
|
@ -17,7 +17,7 @@ export const TriageLimit = {
|
||||
};
|
||||
|
||||
class StorageProvider extends React.Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.originalTitle = document.title;
|
||||
@ -28,13 +28,13 @@ class StorageProvider extends React.Component {
|
||||
loading: false,
|
||||
error: null,
|
||||
notifications: []
|
||||
}
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
this.refreshNotifications();
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
window.onfocus = () => this.setTitle(this.originalTitle);
|
||||
}
|
||||
|
||||
@ -42,14 +42,13 @@ class StorageProvider extends React.Component {
|
||||
if (document.title.indexOf('(1)') === -1 && document.title !== title) {
|
||||
document.title = title;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads up the notifications state with the cache.
|
||||
*/
|
||||
refreshNotifications = () => {
|
||||
const notifications = Object
|
||||
.keys(window.localStorage)
|
||||
const notifications = Object.keys(window.localStorage)
|
||||
.reduce((acc, key) => {
|
||||
if (key.indexOf(LOCAL_STORAGE_PREFIX) > -1) {
|
||||
const cached_n = JSON.parse(window.localStorage.getItem(key));
|
||||
@ -62,8 +61,7 @@ class StorageProvider extends React.Component {
|
||||
// a notification, however we should fallback to `updated_at` in case
|
||||
// there is a thread that doesn't have this set yet.
|
||||
const lastUpdated = moment(
|
||||
notification.status_last_changed ||
|
||||
notification.updated_at
|
||||
notification.status_last_changed || notification.updated_at
|
||||
);
|
||||
const daysOld = moment().diff(lastUpdated, 'days');
|
||||
|
||||
@ -105,12 +103,12 @@ class StorageProvider extends React.Component {
|
||||
return false;
|
||||
});
|
||||
|
||||
this.setState({ notifications });
|
||||
this.setState({notifications});
|
||||
|
||||
// Faux notifications for sample screenshots.
|
||||
// const mockNotifications = createMockNotifications(20);
|
||||
// this.setState({ notifications: mockNotifications });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stats are broken up since they are fetched and set often, we want to avoid
|
||||
@ -134,18 +132,24 @@ class StorageProvider extends React.Component {
|
||||
// Range reflects `[start, end)`
|
||||
for (let m = startTime.clone(); m.isBefore(endTime); m.add(1, 'day')) {
|
||||
const key = m.format('YYYY-MM-DD');
|
||||
const value = window.localStorage.getItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}-${stat}`);
|
||||
const value = window.localStorage.getItem(
|
||||
`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}-${stat}`
|
||||
);
|
||||
if (value) {
|
||||
response.push(value);
|
||||
} else {
|
||||
// If the date is in the past or present, give it a value of 0. Otherwise, null.
|
||||
const fauxValue = m.clone().startOf('day').isSameOrBefore(
|
||||
currentTime.clone().startOf('day')) ? 0 : null;
|
||||
const fauxValue = m
|
||||
.clone()
|
||||
.startOf('day')
|
||||
.isSameOrBefore(currentTime.clone().startOf('day'))
|
||||
? 0
|
||||
: null;
|
||||
response.push(fauxValue);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Since our stats right now are just numbers, we can assume "setting" will always
|
||||
@ -154,13 +158,21 @@ class StorageProvider extends React.Component {
|
||||
*/
|
||||
incrStat = (stat, additionalPrefix = moment().format('YYYY-MM-DD')) => {
|
||||
const key = additionalPrefix ? `${additionalPrefix}-` : '';
|
||||
const oldValue = window.localStorage.getItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}${stat}`);
|
||||
const oldValue = window.localStorage.getItem(
|
||||
`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}${stat}`
|
||||
);
|
||||
if (oldValue !== null) {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}${stat}`, parseInt(oldValue, 10) + 1);
|
||||
window.localStorage.setItem(
|
||||
`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}${stat}`,
|
||||
parseInt(oldValue, 10) + 1
|
||||
);
|
||||
} else {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}${stat}`, 1);
|
||||
window.localStorage.setItem(
|
||||
`${LOCAL_STORAGE_STATISTIC_PREFIX}${key}${stat}`,
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getAllRepoStagedCounts = () => {
|
||||
return Object.keys(window.localStorage)
|
||||
@ -172,42 +184,55 @@ class StorageProvider extends React.Component {
|
||||
}
|
||||
|
||||
// Janky but will work.
|
||||
const repo = key.split('__REPO__-').pop().split('-stagedCount')[0];
|
||||
const repo = key
|
||||
.split('__REPO__-')
|
||||
.pop()
|
||||
.split('-stagedCount')[0];
|
||||
|
||||
repos[repo] = value;
|
||||
return repos;
|
||||
}, {});
|
||||
}
|
||||
};
|
||||
|
||||
// val value : Object
|
||||
setItem = (id, value) => {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_PREFIX}${id}`, JSON.stringify(value));
|
||||
}
|
||||
window.localStorage.setItem(
|
||||
`${LOCAL_STORAGE_PREFIX}${id}`,
|
||||
JSON.stringify(value)
|
||||
);
|
||||
};
|
||||
|
||||
getItem = id => {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`));
|
||||
return JSON.parse(
|
||||
window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`)
|
||||
);
|
||||
} catch (e) {
|
||||
return window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getUserItem = id => {
|
||||
try {
|
||||
return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`));
|
||||
return JSON.parse(
|
||||
window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`)
|
||||
);
|
||||
} catch (e) {
|
||||
return window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setUserItem = (id, value) => {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`, JSON.stringify(value));
|
||||
}
|
||||
window.localStorage.setItem(
|
||||
`${LOCAL_STORAGE_USER_PREFIX}${id}`,
|
||||
JSON.stringify(value)
|
||||
);
|
||||
};
|
||||
|
||||
// Actually does the work of deleting the item from the cache.
|
||||
deleteItem = id => {
|
||||
window.localStorage.removeItem(`${LOCAL_STORAGE_PREFIX}${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
removeItem = id => {
|
||||
// We never really want to purge anything from the cache if we can help it,
|
||||
@ -222,11 +247,10 @@ class StorageProvider extends React.Component {
|
||||
status: Status.CLOSED
|
||||
};
|
||||
this.setItem(id, closed_cached_n);
|
||||
}
|
||||
};
|
||||
|
||||
clearArchivedCache = () => {
|
||||
Object
|
||||
.keys(window.localStorage)
|
||||
Object.keys(window.localStorage)
|
||||
.reduce((acc, key) => {
|
||||
if (key.indexOf(LOCAL_STORAGE_PREFIX) > -1) {
|
||||
const cached_n = JSON.parse(window.localStorage.getItem(key));
|
||||
@ -236,13 +260,13 @@ class StorageProvider extends React.Component {
|
||||
}, [])
|
||||
.filter(notification => notification.status === Status.Archived)
|
||||
.forEach(notification => this.deleteItem(notification.id));
|
||||
}
|
||||
};
|
||||
|
||||
clearCache = () => {
|
||||
window.localStorage.clear();
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return this.props.children({
|
||||
...this.state,
|
||||
setItem: this.setItem,
|
||||
@ -255,20 +279,15 @@ class StorageProvider extends React.Component {
|
||||
refreshNotifications: this.refreshNotifications,
|
||||
getStat: this.getStat,
|
||||
getAllRepoStagedCounts: this.getAllRepoStagedCounts,
|
||||
incrStat: this.incrStat,
|
||||
incrStat: this.incrStat
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const withStorageProvider = WrappedComponent => props => (
|
||||
<StorageProvider>
|
||||
{(storageApi) => (
|
||||
<WrappedComponent {...props} storageApi={storageApi} />
|
||||
)}
|
||||
{storageApi => <WrappedComponent {...props} storageApi={storageApi} />}
|
||||
</StorageProvider>
|
||||
);
|
||||
|
||||
export {
|
||||
StorageProvider,
|
||||
withStorageProvider
|
||||
};
|
||||
export {StorageProvider, withStorageProvider};
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
const facts = [
|
||||
'Movie trailers were originally shown after the movie, which is why they were called “trailers”.',
|
||||
'The top six foods that make your fart are beans, corn, bell peppers, cauliflower, cabbage and milk!',
|
||||
@ -40,7 +39,7 @@ const facts = [
|
||||
'12+1 = 11+2, and "twelve plus one" is an anagram of "eleven plus two."'
|
||||
];
|
||||
|
||||
export function getFact () {
|
||||
export function getFact() {
|
||||
// DoUbLe TiLdE iS mOrE eLeGaNt ThAn MaTh.FlOoR
|
||||
return facts[~~(Math.random() * facts.length)];
|
||||
}
|
||||
|
@ -2,9 +2,9 @@ import moment from 'moment';
|
||||
import {Reasons} from '../constants/reasons';
|
||||
import {Status} from '../constants/status';
|
||||
|
||||
function* createCounterGenerator () {
|
||||
function* createCounterGenerator() {
|
||||
let i = 0;
|
||||
while(true) yield i++
|
||||
while (true) yield i++;
|
||||
}
|
||||
|
||||
const uidGen = createCounterGenerator();
|
||||
@ -19,7 +19,7 @@ const getMockReasons = (n, ra) => {
|
||||
}));
|
||||
};
|
||||
|
||||
function getMockName (index) {
|
||||
function getMockName(index) {
|
||||
const names = [
|
||||
'[MPQ-43] Feature - Improve native events performance',
|
||||
'Chore - Remove redundent constants file',
|
||||
@ -27,29 +27,36 @@ function getMockName (index) {
|
||||
'Feature - Update Storage bindings to be fastpipe compatible',
|
||||
'[RFC] Standard Library Interfaces',
|
||||
'[Belt] Change reduceReverse to reduceRight',
|
||||
'[Experiment][DoNotmerge] Represent OCaml records as JS objects at runtime',
|
||||
'[Experiment][DoNotmerge] Represent OCaml records as JS objects at runtime'
|
||||
];
|
||||
return names[index % names.length];
|
||||
}
|
||||
|
||||
const getMockNotification = (ra, rb, rc) => ({
|
||||
id: uidGen.next().value,
|
||||
updated_at: moment().subtract(ra * 500, 'minutes').utc().format(),
|
||||
updated_at: moment()
|
||||
.subtract(ra * 500, 'minutes')
|
||||
.utc()
|
||||
.format(),
|
||||
isAuthor: ra < 0.2,
|
||||
status: ra < 0.4 ? Status.Unread : rb < 0.6 ? Status.Read : Status.Archived,
|
||||
reasons: getMockReasons(Math.ceil(rb * 10), ra),
|
||||
type: ['PullRequest', 'Issue'][rc > .7 ? 1 : 0],
|
||||
type: ['PullRequest', 'Issue'][rc > 0.7 ? 1 : 0],
|
||||
name: getMockName(indexGen.next().value),
|
||||
url: 'https://github.com/test/repo/pull',
|
||||
repository: ['BuckleScript/bucklescript', 'nickzuber/meteorite'][Math.floor(rb * 2)],
|
||||
repository: ['BuckleScript/bucklescript', 'nickzuber/meteorite'][
|
||||
Math.floor(rb * 2)
|
||||
],
|
||||
number: Math.ceil(rc * 1000),
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
repositoryUrl: 'https://github.com/test/repo'
|
||||
});
|
||||
|
||||
export function createMockNotifications (n = 50) {
|
||||
export function createMockNotifications(n = 50) {
|
||||
const mockNotifications = new Array(n);
|
||||
for (let i = 0; i < mockNotifications.length; i++) {
|
||||
let a = Math.random(), b = Math.random(), c = Math.random();
|
||||
let a = Math.random(),
|
||||
b = Math.random(),
|
||||
c = Math.random();
|
||||
mockNotifications[i] = getMockNotification(a, b, c);
|
||||
}
|
||||
return mockNotifications;
|
||||
|
@ -1,4 +1,4 @@
|
||||
export function inlineSvg (svg) {
|
||||
export function inlineSvg(svg) {
|
||||
const b64 = btoa(svg);
|
||||
return `"data:image/svg+xml;base64,${b64}"`;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user