mirror of
https://github.com/nickzuber/meteorite.git
synced 2024-07-14 23:00:32 +03:00
Deprecate /notifications-redesign to /notifications
This commit is contained in:
parent
015758fc9e
commit
9380afbeb9
10
src/App.js
10
src/App.js
@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import amplitude from 'amplitude-js';
|
||||
import {
|
||||
Redirect,
|
||||
Router,
|
||||
Location,
|
||||
LocationProvider
|
||||
@ -12,7 +13,6 @@ import {
|
||||
Login,
|
||||
Pricing,
|
||||
Guide,
|
||||
Notifications,
|
||||
NotificationsRedesign,
|
||||
} from './pages';
|
||||
|
||||
@ -41,6 +41,10 @@ function PageTracker ({location}) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function RedirectShell () {
|
||||
return <Redirect noThrow to={routes.NOTIFICATIONS} />;
|
||||
}
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
return (
|
||||
@ -54,8 +58,8 @@ class App extends Component {
|
||||
<Login path={routes.LOGIN} />
|
||||
<Pricing path={routes.PRICING} />
|
||||
<Guide path={routes.GUIDE} />
|
||||
<Notifications path={routes.NOTIFICATIONS} />
|
||||
<NotificationsRedesign path={routes.REDESIGN_NOTIFICATIONS} />
|
||||
<RedirectShell path={routes.REDESIGN_NOTIFICATIONS} />
|
||||
<NotificationsRedesign path={routes.NOTIFICATIONS} />
|
||||
</Router>
|
||||
</LocationProvider>
|
||||
</AuthProvider>
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,552 +0,0 @@
|
||||
/** @jsx jsx */
|
||||
|
||||
import React from 'react';
|
||||
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 WorkflowToggle from './WorkflowToggle';
|
||||
|
||||
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';
|
||||
import ScreenshotPng from '../../images/screenshots/new/dashboard.png';
|
||||
import ScoresPng from '../../images/screenshots/new/scores.png';
|
||||
import ReasonsPng from '../../images/screenshots/new/reasons.png';
|
||||
|
||||
import RobinLogo from '../../images/logos/robin-logo.png';
|
||||
import ForwardLogo from '../../images/logos/forward-logo.png';
|
||||
import FacebookLogo from '../../images/logos/facebook-logo.png';
|
||||
|
||||
import '../../styles/gradient.css';
|
||||
import '../../styles/font.css';
|
||||
|
||||
const themeColor = '#27B768';
|
||||
const ALT_BACKGROUND_COLOR = '#f6f2ed';
|
||||
|
||||
const ProductHuntButton = () => (
|
||||
<a href="https://www.producthunt.com/posts/meteorite?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-meteorite" target="_blank">
|
||||
<img
|
||||
src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=145651&theme=dark"
|
||||
alt="Meteorite - Smarter GitHub notifications. | Product Hunt Embed"
|
||||
style={{width: 200, height: 43}}
|
||||
width="200px"
|
||||
height="43px" />
|
||||
</a>
|
||||
);
|
||||
|
||||
const Outer = styled('div')`
|
||||
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'};
|
||||
max-width: 1080px;
|
||||
min-height: 100px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 5rem;
|
||||
${forSmallScreens(`
|
||||
padding-left: 2.5rem;
|
||||
padding-right: 2.5rem;
|
||||
`)}
|
||||
${forMobile(`
|
||||
margin-bottom: 2.5rem;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const FlexItem = styled('div')`
|
||||
flex: ${(({flex = 1}) => flex)};
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
|
||||
const Button = styled(RouterLink)`
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
color: #333333;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
background-color: transparent;
|
||||
border: 0px solid transparent;
|
||||
margin: 0rem 0.25rem;
|
||||
width: max-content;
|
||||
padding: 0.125rem 1rem;
|
||||
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;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
border-color: #f4f4f4;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #eee;
|
||||
border-color: #eee;
|
||||
}
|
||||
`;
|
||||
|
||||
const MainButton = styled(Button)`
|
||||
background-color: ${themeColor};
|
||||
border-color: ${themeColor};
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover {
|
||||
background-color: #249959;
|
||||
border-color: #249959;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: #20894f;
|
||||
border-color: #20894f;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeroButton = styled(MainButton)`
|
||||
font-size: 22px;
|
||||
margin: -4px 8px 0;
|
||||
margin-left: 0;
|
||||
`;
|
||||
|
||||
const FlexBreak = styled('div')`
|
||||
flex-basis: 100%;
|
||||
height: 0;
|
||||
min-height: ${p => p.height || 0}px;
|
||||
`;
|
||||
|
||||
const HeroTitle = styled('h1')`
|
||||
font-size: 68px;
|
||||
line-height: 68px;
|
||||
margin: 0 auto 6px;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-weight: 500;
|
||||
|
||||
${forMobile(`
|
||||
font-size: 52px;
|
||||
line-height: 58px;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const HeroSubtitle = styled('h1')`
|
||||
color: #6c757d;
|
||||
font-size: 24px;
|
||||
line-height: 28px;
|
||||
margin: 0 auto 16px;
|
||||
width: 80%;
|
||||
margin-left: 0;
|
||||
font-family: medium-content-sans-serif-font, Inter UI, system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
|
||||
${forMobile(`
|
||||
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-weight: 500;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const Subtitle = styled('h1')`
|
||||
color: #6c757d;
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
margin: 0 auto 16px;
|
||||
font-family: medium-content-sans-serif-font, Inter UI, system-ui, sans-serif;
|
||||
font-weight: 500;
|
||||
max-width: 540px;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const ItemImage = styled('img')`
|
||||
width: 847px;
|
||||
box-shadow: rgba(84, 70, 35, 0) 0px 4px 18px, rgba(84, 70, 35, 0.15) 0px 2px 8px;
|
||||
border-radius: 6px;
|
||||
margin-left: ${props => props.left || 0}px;
|
||||
margin-top: ${props => props.top || 0}px;
|
||||
transform: rotate(${props => props.deg || 0}deg) scale(${props => props.scale || 1});
|
||||
`;
|
||||
|
||||
const HeroLeft = styled(FlexItem)`
|
||||
z-index: 1;
|
||||
flex-grow: 1;
|
||||
width: 50%;
|
||||
${forMobile(`
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const HeroRight = styled(FlexItem)`
|
||||
flex-grow: 1;
|
||||
width: 50%;
|
||||
${forMobile(`
|
||||
display: none;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const DotsBackground = styled('div')`
|
||||
position: absolute;
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
margin-left: -60px;
|
||||
background: radial-gradient(transparent 50%, #fffefd), \
|
||||
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==) repeat;
|
||||
${forMobile(`
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const FooterImageContainer = styled('div')`
|
||||
position: relative;
|
||||
height: 250px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background: radial-gradient(transparent 50%, ${ALT_BACKGROUND_COLOR}), \
|
||||
url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAKElEQVQoU2NkIBIwEqmOgQ4KX715/x/mHDERQbiNGFZTXyGuUKC+rwHAcQwLu0IifQAAAABJRU5ErkJggg==) repeat;
|
||||
${forMobile(`
|
||||
display: none;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const CompanyQuotesContainer = styled('div')`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 4rem;
|
||||
${forMobile(`
|
||||
flex-direction: column;
|
||||
max-width: 300px;
|
||||
margin: 0 auto;
|
||||
`)}
|
||||
`;
|
||||
|
||||
const HorizontalFlexContainer = styled('div')`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 4rem;
|
||||
`;
|
||||
|
||||
const FeatureItem = styled('div')(({color}) => `
|
||||
flex: 1;
|
||||
${forMobile(`
|
||||
flex: 1 0 34%;
|
||||
`)}
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 4rem;
|
||||
padding: 0 12px;
|
||||
|
||||
svg {
|
||||
background: ${color}28;
|
||||
fill: ${color};
|
||||
border-radius: 100%;
|
||||
padding: 12px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
line-height: 25px;
|
||||
text-align: center;
|
||||
margin: 18px auto 8px;
|
||||
font-family: medium-marketing-display-font,Georgia,Cambria,Times New Roman,Times,serif;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 17px;
|
||||
line-height: 22px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
`);
|
||||
|
||||
const HorizontalListItem = styled('div')(({last}) => `
|
||||
flex: 1;
|
||||
border-right: ${last ? '0px' : '1px'} solid rgba(214, 212, 209, 0.3);
|
||||
padding: 0 32px;
|
||||
${forMobile(`
|
||||
border-right: 0;
|
||||
border-bottom: ${last ? '0px' : '1px'} solid rgba(214, 212, 209, 0.3);
|
||||
padding: 32px 0;
|
||||
`)}
|
||||
`);
|
||||
|
||||
const Quote = styled('p')`
|
||||
margin: 0;
|
||||
font-family: medium-content-title-font, Inter UI, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
&:before {
|
||||
content: open-quote;
|
||||
}
|
||||
&:after {
|
||||
content: close-quote;
|
||||
}
|
||||
`;
|
||||
|
||||
const CompanyPerson = styled('div')`
|
||||
transform: scale(0.95);
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
margin: 0;
|
||||
img {
|
||||
display: block;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
padding: 0 16px;
|
||||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
color: #37352f80;
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Scene (props) {
|
||||
const baseItemOffset = {
|
||||
scale: 0.1,
|
||||
top: -185,
|
||||
left: 20
|
||||
};
|
||||
|
||||
return (
|
||||
<BasicPageWrapper {...props}>
|
||||
|
||||
{/* Hero */}
|
||||
<Container css={css`
|
||||
${forMobile(`
|
||||
flex-direction: column;
|
||||
`)}
|
||||
`}>
|
||||
<HeroLeft>
|
||||
<HeroTitle>{'Manage your notifications.'}</HeroTitle>
|
||||
<HeroSubtitle>{'Meteorite helps organize, filter, and prioritize your\
|
||||
GitHub notifications to make your life easier'}</HeroSubtitle>
|
||||
<FlexBreak />
|
||||
<div css={css`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
${forMobile(`
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 300px;
|
||||
`)}
|
||||
`}>
|
||||
<HeroButton css={css`${forMobile(`margin-bottom: 12px;`)}`} to={routes.LOGIN}>{'Login / Sign up'}</HeroButton>
|
||||
<ProductHuntButton />
|
||||
</div>
|
||||
</HeroLeft>
|
||||
<HeroRight>
|
||||
<DotsBackground />
|
||||
<ItemImage
|
||||
scale={baseItemOffset.scale + 0.8}
|
||||
left={baseItemOffset.left + 8}
|
||||
top={25}
|
||||
src={ItemTwoPng} />
|
||||
<ItemImage
|
||||
deg={0}
|
||||
scale={baseItemOffset.scale + 0.95}
|
||||
left={baseItemOffset.left}
|
||||
top={baseItemOffset.top + 4}
|
||||
src={ItemPng} />
|
||||
</HeroRight>
|
||||
</Container>
|
||||
|
||||
{/* Testimonials */}
|
||||
<Container column>
|
||||
<FlexBreak height={60} />
|
||||
<Title>{'Hear what others are saying'}</Title>
|
||||
<Subtitle>{'Loved by other human beings, just like you'}</Subtitle>
|
||||
<CompanyQuotesContainer>
|
||||
<HorizontalListItem>
|
||||
<Quote>{`So good! I love the importance sorting!`}</Quote>
|
||||
<CompanyPerson>
|
||||
<img src={FacebookLogo} />
|
||||
<span>
|
||||
{'— Mike Grabowski'}<br />
|
||||
{'Software Architect, React Native'}
|
||||
</span>
|
||||
</CompanyPerson>
|
||||
</HorizontalListItem>
|
||||
<HorizontalListItem>
|
||||
<Quote>{`I've been using it for a bit and it's so useful, especially if you use GitHub for work.`}</Quote>
|
||||
<CompanyPerson>
|
||||
<img src={RobinLogo} />
|
||||
<span>
|
||||
{'— Trevor Suarez'}<br />
|
||||
{'Robin, Backend Software Engineer'}
|
||||
</span>
|
||||
</CompanyPerson>
|
||||
</HorizontalListItem>
|
||||
<HorizontalListItem last>
|
||||
<Quote>{`Awww sh*t, nice.`}</Quote>
|
||||
<CompanyPerson>
|
||||
<img src={ForwardLogo} />
|
||||
<span>
|
||||
{'— Chris Walker'}<br />
|
||||
{'Forward, Software Engineer'}
|
||||
</span>
|
||||
</CompanyPerson>
|
||||
</HorizontalListItem>
|
||||
</CompanyQuotesContainer>
|
||||
</Container>
|
||||
|
||||
{/* Lifecycle */}
|
||||
<Outer alt>
|
||||
<Container column>
|
||||
<FlexBreak height={60} />
|
||||
<Title>{'The Notification Lifecycle'}</Title>
|
||||
<Subtitle>{'GitHub notifications can actually be your friend – with a little bit of discipline.\
|
||||
All we need to do is filter out the noise & organize things in a way that makes sense.'}</Subtitle>
|
||||
<WorkflowToggle
|
||||
easeTimingMs={100}
|
||||
items={[
|
||||
{
|
||||
id: 0,
|
||||
title: 'Filter The Noise',
|
||||
description: 'Stay focused on the important things. We\'ll only show the notifications that matter to you.',
|
||||
image: ScreenshotPng
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'Highlight The Callouts',
|
||||
description: 'When things stand out, you shouldn\'t miss it. We mark notifications when there\'s something interesting going down.',
|
||||
image: ReasonsPng
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Sort By Importance',
|
||||
description: 'Don\'t get lost at sea – the most important notifications stay at the top of the list.',
|
||||
image: ScoresPng
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<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>
|
||||
<FlexBreak height={80} />
|
||||
</Container>
|
||||
</Outer>
|
||||
|
||||
{/* Features */}
|
||||
<Container column>
|
||||
<FlexBreak height={20} />
|
||||
<Title>{'Features built for getting things done.'}</Title>
|
||||
<Subtitle>{'All of the features of Meteorite are specifically designed'}<br />
|
||||
{'for optimizing your workday'}</Subtitle>
|
||||
<HorizontalFlexContainer>
|
||||
<FeatureItem color={'#9C27B0'}>
|
||||
<CloudOffSvg />
|
||||
<h3>{'Serverless'}</h3>
|
||||
<p>{'Any notification scoring and storing is done completely offline.'}</p>
|
||||
</FeatureItem>
|
||||
<FeatureItem color={'#ffc915'}>
|
||||
<NotificationsActiveSvg />
|
||||
<h3>{'Desktop Notifications'}</h3>
|
||||
<p>{'Get notified when we do – ability to turn on desktop notifications.'}</p>
|
||||
</FeatureItem>
|
||||
<FeatureItem color={'#27B768'}>
|
||||
<PriorityHighSvg />
|
||||
<h3>{'Auto Sorting'}</h3>
|
||||
<p>{'Keep your most important notifications at the top of the list.'}</p>
|
||||
</FeatureItem>
|
||||
<FeatureItem color={'#00A0F5'}>
|
||||
<TuneSvg />
|
||||
<h3>{'Filter Noise'}</h3>
|
||||
<p>{'Any notifications that don\'t directly involve you are hidden.'}</p>
|
||||
</FeatureItem>
|
||||
<FlexBreak />
|
||||
<FeatureItem color={'#EE3F46'}>
|
||||
<SpeedSvg />
|
||||
<h3>{'Dead Simple'}</h3>
|
||||
<p>{'No integrations – just log in and start working.'}</p>
|
||||
</FeatureItem>
|
||||
<FeatureItem color={'#10293c'}>
|
||||
<GpsFixedSvg />
|
||||
<h3>{'Live Updates'}</h3>
|
||||
<p>{'All of your notifications are processed in real time.'}</p>
|
||||
</FeatureItem>
|
||||
<FeatureItem color={'#fd9446'}>
|
||||
<WbIridescentSvg />
|
||||
<h3>{'Reasoning'}</h3>
|
||||
<p>{'We\'ll also tell you why you\'re getting each notification.'}</p>
|
||||
</FeatureItem>
|
||||
<FeatureItem color={'#fc46fd'}>
|
||||
<TimelineSvg />
|
||||
<h3>{'Statistics'}</h3>
|
||||
<p>{'Better understand how you work with data visualizations.'}</p>
|
||||
</FeatureItem>
|
||||
</HorizontalFlexContainer>
|
||||
</Container>
|
||||
|
||||
{/* Closer */}
|
||||
<Outer alt>
|
||||
<Container column>
|
||||
<FlexBreak height={80} />
|
||||
<Title>{'Better notifications for everyone.'}</Title>
|
||||
<FlexBreak height={20} />
|
||||
<HeroButton to={routes.LOGIN} css={css`margin: 0 auto;`}>{'Login / Sign up'}</HeroButton>
|
||||
<FlexBreak height={40} />
|
||||
<FooterImageContainer>
|
||||
<ItemImage
|
||||
scale={baseItemOffset.scale + 0.8}
|
||||
top={60}
|
||||
src={ItemPng} />
|
||||
<ItemImage
|
||||
deg={0}
|
||||
scale={baseItemOffset.scale + 0.95}
|
||||
top={-30}
|
||||
src={ItemTwoPng} />
|
||||
</FooterImageContainer>
|
||||
<FlexBreak height={40} />
|
||||
</Container>
|
||||
</Outer>
|
||||
</BasicPageWrapper>
|
||||
);
|
||||
};
|
@ -3,7 +3,7 @@ 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.new';
|
||||
import Scene from './Scene';
|
||||
|
||||
class HomePage extends React.Component {
|
||||
onLogout = () => {
|
||||
|
@ -24,7 +24,7 @@ class LoginPage extends React.Component {
|
||||
|
||||
render () {
|
||||
if (this.props.authApi.token) {
|
||||
return <Redirect noThrow to={routes.REDESIGN_NOTIFICATIONS} />
|
||||
return <Redirect noThrow to={routes.NOTIFICATIONS} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,466 +0,0 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { Redirect, navigate } from "@reach/router";
|
||||
import { compose } from 'recompose';
|
||||
import { withNotificationsProvider } from '../../providers/Notifications';
|
||||
import { withAuthProvider } from '../../providers/Auth';
|
||||
import { withCookiesProvider } from '../../providers/Cookies';
|
||||
import { withStorageProvider } from '../../providers/Storage';
|
||||
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
|
||||
import { routes } from '../../constants';
|
||||
import { Filters } from '../../constants/filters';
|
||||
import { Status } from '../../constants/status';
|
||||
import { Reasons, Badges } from '../../constants/reasons';
|
||||
import Scene, { getMessageFromReasons } from './Scene';
|
||||
import issueIcon from '../../images/issue-bg.png';
|
||||
import prIcon from '../../images/pr-bg.png';
|
||||
import tabIcon from '../../images/icon.png';
|
||||
import tabDotIcon from '../../images/iconDot.png';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
// @TODO Move these functions.
|
||||
|
||||
/**
|
||||
* Given a notification, give it a score based on its importance.
|
||||
*
|
||||
* There are some interesting workarounds that go into this algorithm to account
|
||||
* for GitHub's broken notifications API -- but we will get to that later. First,
|
||||
* let's start off with the basics of scoring.
|
||||
*
|
||||
* There are a few "reasons" that we can be getting a notification, each having
|
||||
* an initial weight of importance:
|
||||
*
|
||||
* - MENTION -> 8
|
||||
* - ASSIGN -> 14
|
||||
* - REVIEW_REQUESTED -> 30
|
||||
* - SUBSCRIBED -> 3
|
||||
* - COMMENT -> 3
|
||||
* - AUTHOR -> 10
|
||||
* - OTHER -> 2
|
||||
*
|
||||
* There are some rules that go to giving out these scores, primarily being the
|
||||
* first time we see one of these unique reasons, we award the notification with
|
||||
* the respective score, but a reason that transitions into itself will be awarded
|
||||
* a degraded score of min(ceil(n/3), 2). For example:
|
||||
*
|
||||
* - null, MENTION, MENTION -> 0, 8, 3
|
||||
* - null, ASSIGN, ASSIGN, REVIEW_REQUESTED, -> 0, 14, 5, 20
|
||||
* - null, SUBSCRIBED, SUBSCRIBED, SUBSCRIBED -> 0, 3, 2, 2
|
||||
*
|
||||
* @param {Object} notification Some notification to score.
|
||||
* @return {number} The score.
|
||||
*/
|
||||
function scoreOf (notification) {
|
||||
const {reasons} = notification;
|
||||
let score = 0;
|
||||
let prevReason = null;
|
||||
for (let i = 0; i < reasons.length; i++) {
|
||||
const reason = reasons[i].reason;
|
||||
if (prevReason && reason === prevReason) {
|
||||
const degradedScore = Math.ceil(scoreOfReason[reason] / 3);
|
||||
score += Math.max(degradedScore, 2);
|
||||
} else {
|
||||
score += scoreOfReason[reason];
|
||||
}
|
||||
prevReason = reason;
|
||||
}
|
||||
return score;
|
||||
};
|
||||
|
||||
function badgesOf (notification) {
|
||||
const badges = [];
|
||||
const len = notification.reasons.length;
|
||||
const timeSinceLastUpdate = moment().diff(moment(notification.reasons[len - 1].time), 'minutes');
|
||||
|
||||
// If there are more than 4 reasons, and the last 4 reasons have happened within
|
||||
// an hour of each other. The last update should be within the past 30 minutes.
|
||||
// The specific time frame and reasons count is subject to change.
|
||||
if (len >= 4 && timeSinceLastUpdate < 30) {
|
||||
const oldestReference = moment(notification.reasons[len - 4].time);
|
||||
const newestReference = moment(notification.reasons[len - 1].time);
|
||||
if (newestReference.diff(oldestReference, 'hours') <= 1) {
|
||||
badges.push(Badges.HOT);
|
||||
}
|
||||
}
|
||||
// If there's a lot of activity going on within the thread in general.
|
||||
// The specific nunmber should be relative to average number of thread lengths.
|
||||
// We can track a running statistic as we see notifications update.
|
||||
if (len > 6) {
|
||||
badges.push(Badges.COMMENTS);
|
||||
}
|
||||
// If you've been tagged in for review and the most recent update happened over
|
||||
// 4 hours ago, that specific time is subject to change.
|
||||
if (notification.reasons.some(r => r.reason === Reasons.REVIEW_REQUESTED) &&
|
||||
timeSinceLastUpdate > 60 * 4) {
|
||||
badges.push(Badges.OLD);
|
||||
}
|
||||
return badges;
|
||||
};
|
||||
|
||||
const scoreOfReason = {
|
||||
[Reasons.ASSIGN]: 21,
|
||||
[Reasons.AUTHOR]: 11,
|
||||
[Reasons.MENTION]: 17,
|
||||
[Reasons.TEAM_MENTION]: 11,
|
||||
[Reasons.OTHER]: 4,
|
||||
[Reasons.REVIEW_REQUESTED]: 29,
|
||||
[Reasons.SUBSCRIBED]: 3,
|
||||
[Reasons.COMMENT]: 6,
|
||||
[Reasons.STATE_CHANGE]: 5,
|
||||
};
|
||||
|
||||
const decorateWithScore = notification => ({
|
||||
...notification,
|
||||
score: scoreOf(notification),
|
||||
badges: badgesOf(notification)
|
||||
});
|
||||
|
||||
class NotificationsPage extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.notificationSent = false;
|
||||
this.isUnreadTab = false;
|
||||
}
|
||||
|
||||
state = {
|
||||
currentTime: moment(),
|
||||
error: null,
|
||||
notificationSent: false,
|
||||
isFirstTimeUser: false,
|
||||
isSearching: false,
|
||||
query: null,
|
||||
activeFilter: Filters.PARTICIPATING,
|
||||
activeStatus: Status.QUEUED,
|
||||
currentPage: 1
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const isFirstTimeUser = !this.props.storageApi.getUserItem('hasOnboarded');
|
||||
|
||||
// Harsh but fair.
|
||||
if (window.outerWidth < 1100) {
|
||||
navigate(routes.REDESIGN_NOTIFICATIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirstTimeUser) {
|
||||
this.setState({isFirstTimeUser: true});
|
||||
// this.props.storageApi.setUserItem('hasOnboarded', true);
|
||||
}
|
||||
|
||||
this.props.notificationsApi.fetchNotifications();
|
||||
|
||||
this.tabSyncer = setInterval(() => {
|
||||
if (!document.hidden && this.isUnreadTab) {
|
||||
this.updateTabIcon(false);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
this.syncer = setInterval(() => {
|
||||
this.props.notificationsApi.fetchNotificationsSync()
|
||||
.then(error => this.setState({error: null}))
|
||||
.catch(error => this.setState({error}));
|
||||
this.setState({currentTime: moment()});
|
||||
}, 8 * 1000);
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
if (this.props.notificationsApi.newChanges !== nextProps.notificationsApi.newChanges) {
|
||||
this.notificationSent = false;
|
||||
}
|
||||
// The idea here is if we've just updated the prevNotifications state, then
|
||||
// we don't want to trigger a rerender.
|
||||
return nextState.prevNotifications === this.state.prevNotifications;
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.syncer);
|
||||
clearInterval(this.tabSyncer);
|
||||
}
|
||||
|
||||
onChangePage = page => {
|
||||
this.setState({ currentPage: page });
|
||||
}
|
||||
|
||||
onSetActiveFilter = filter => {
|
||||
this.setState({ activeFilter: filter, currentPage: 1 });
|
||||
}
|
||||
|
||||
onSetActiveStatus = status => {
|
||||
this.setState({ activeStatus: status, currentPage: 1 });
|
||||
}
|
||||
|
||||
onClearQuery = () => {
|
||||
this.setState({ query: null });
|
||||
}
|
||||
|
||||
onLogout = () => {
|
||||
// Remove cookie and invalidate token on client.
|
||||
this.props.cookiesApi.removeCookie(OAUTH_TOKEN_COOKIE);
|
||||
this.props.authApi.invalidateToken();
|
||||
}
|
||||
|
||||
onSearch = event => {
|
||||
const text = event.target.value;
|
||||
|
||||
// Ignore empty queries.
|
||||
if (text.length <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ isSearching: true });
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
query: text,
|
||||
isSearching: false
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
enhancedOnStageThread = (thread_id, repository) => {
|
||||
this.props.storageApi.incrStat('stagedCount');
|
||||
this.props.storageApi.incrStat(repository + '-stagedCount');
|
||||
this.props.notificationsApi.stageThread(thread_id);
|
||||
}
|
||||
|
||||
enhancedOnMarkAsRead = (thread_id, repository) => {
|
||||
this.props.storageApi.incrStat('stagedCount');
|
||||
this.props.storageApi.incrStat(repository + '-stagedCount');
|
||||
this.props.notificationsApi.markAsRead(thread_id);
|
||||
}
|
||||
|
||||
restoreThread = thread_id => {
|
||||
this.props.notificationsApi.restoreThread(thread_id);
|
||||
}
|
||||
|
||||
setNotificationsPermission = (...args) => {
|
||||
this.props.notificationsApi.setNotificationsPermission(...args);
|
||||
}
|
||||
|
||||
updateTabIcon (hasUnread = true) {
|
||||
this.isUnreadTab = hasUnread;
|
||||
var link = document.querySelector("link[rel*='icon']") || document.createElement('link');
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = hasUnread ? tabDotIcon : tabIcon;
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
}
|
||||
|
||||
sendWebNotification = newNotifcations => {
|
||||
if (this.notificationSent || newNotifcations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only show these notifications and title change if the tab is out of focus.
|
||||
if (!document.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set this even if we don't actually send the notification due to permissions.
|
||||
this.notificationSent = true;
|
||||
this.updateTabIcon();
|
||||
|
||||
// No permission, no notification.
|
||||
if (this.props.notificationsApi.notificationsPermission !== 'granted') {
|
||||
return;
|
||||
}
|
||||
|
||||
const n = newNotifcations[0];
|
||||
const reasonByline = getMessageFromReasons(n.reasons, n.type);
|
||||
|
||||
const additionalInfo = newNotifcations.length > 1
|
||||
? ` (+${newNotifcations.length} more)`
|
||||
: '';
|
||||
|
||||
const notification = new Notification(n.name + additionalInfo, {
|
||||
body: reasonByline,
|
||||
icon: n.type === "Issue" ? issueIcon : prIcon,
|
||||
badge: n.type === "Issue" ? issueIcon : prIcon,
|
||||
requireInteraction: true,
|
||||
});
|
||||
|
||||
notification.addEventListener('click', () => {
|
||||
this.updateTabIcon(false);
|
||||
this.enhancedOnStageThread(n.id, n.repository);
|
||||
window.open(n.url);
|
||||
})
|
||||
|
||||
// Manually close for legacy browser support.
|
||||
setTimeout(notification.close.bind(notification), 10000);
|
||||
}
|
||||
|
||||
getFilteredNotifications = () => {
|
||||
const {notifications} = this.props.notificationsApi;
|
||||
|
||||
let filterMethod = () => true;
|
||||
switch (this.state.activeFilter) {
|
||||
case Filters.PARTICIPATING:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => (
|
||||
reason === Reasons.REVIEW_REQUESTED ||
|
||||
reason === Reasons.ASSIGN ||
|
||||
reason === Reasons.MENTION ||
|
||||
reason === Reasons.AUTHOR
|
||||
))
|
||||
);
|
||||
break;
|
||||
case Filters.ASSIGNED:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => reason === Reasons.ASSIGN)
|
||||
);
|
||||
break;
|
||||
case Filters.REVIEW_REQUESTED:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => reason === Reasons.REVIEW_REQUESTED)
|
||||
);
|
||||
break;
|
||||
case Filters.COMMENT:
|
||||
filterMethod = n => (
|
||||
n.reasons.some(({ reason }) => reason === Reasons.COMMENT)
|
||||
);
|
||||
break;
|
||||
default:
|
||||
filterMethod = () => true;
|
||||
}
|
||||
|
||||
const filteredNotifications = notifications.filter(filterMethod);
|
||||
|
||||
const notificationsQueued = filteredNotifications.filter(n => n.status === Status.QUEUED);
|
||||
const notificationsStaged = filteredNotifications.filter(n => n.status === Status.STAGED);
|
||||
const notificationsClosed = filteredNotifications.filter(n => n.status === Status.CLOSED);
|
||||
|
||||
let notificationsToRender = [];
|
||||
switch (this.state.activeStatus) {
|
||||
case Status.CLOSED:
|
||||
notificationsToRender = notificationsClosed;
|
||||
break;
|
||||
case Status.STAGED:
|
||||
notificationsToRender = notificationsStaged;
|
||||
break;
|
||||
case Status.QUEUED:
|
||||
default:
|
||||
notificationsToRender = notificationsQueued;
|
||||
}
|
||||
|
||||
let scoredAndSortedNotifications = notificationsToRender
|
||||
.map(decorateWithScore)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
|
||||
// We gotta make sure to search notifications before we paginate.
|
||||
// Otherwise we'd just end up searching on the current page, which is bad.
|
||||
if (this.state.query) {
|
||||
scoredAndSortedNotifications = scoredAndSortedNotifications.filter(n => (
|
||||
n.name.toLowerCase().indexOf(this.state.query.toLowerCase()) > -1)
|
||||
)
|
||||
}
|
||||
|
||||
if (this.props.notificationsApi.newChanges) {
|
||||
const filteredNewChanges = this.props.notificationsApi.newChanges.filter(n => (
|
||||
scoredAndSortedNotifications.some(fn => fn.id === n.id)
|
||||
));
|
||||
if (filteredNewChanges.length > 0) {
|
||||
this.sendWebNotification(filteredNewChanges);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
notifications: scoredAndSortedNotifications,
|
||||
queuedCount: notificationsQueued.length,
|
||||
stagedCount: notificationsStaged.length,
|
||||
closedCount: notificationsClosed.length,
|
||||
};
|
||||
}
|
||||
|
||||
render () {
|
||||
if (!this.props.authApi.token) {
|
||||
return <Redirect noThrow to={routes.HOME} />
|
||||
}
|
||||
|
||||
const {
|
||||
fetchNotifications,
|
||||
markAllAsStaged,
|
||||
clearCache,
|
||||
notificationsPermission,
|
||||
loading: isFetchingNotifications,
|
||||
error: fetchingNotificationsError,
|
||||
} = this.props.notificationsApi;
|
||||
const {
|
||||
notifications: scoredAndSortedNotifications,
|
||||
queuedCount,
|
||||
stagedCount,
|
||||
closedCount,
|
||||
} = this.getFilteredNotifications();
|
||||
|
||||
let firstIndex = (this.state.currentPage - 1) * PER_PAGE;
|
||||
let lastIndex = (this.state.currentPage * PER_PAGE);
|
||||
let notificationsOnPage = scoredAndSortedNotifications.slice(firstIndex, lastIndex);
|
||||
let lastPage = Math.ceil(scoredAndSortedNotifications.length / PER_PAGE);
|
||||
let firstNumbered = firstIndex + 1;
|
||||
let lastNumbered = Math.min(lastIndex, scoredAndSortedNotifications.length);
|
||||
|
||||
if (scoredAndSortedNotifications.length === 0) {
|
||||
firstIndex = 0;
|
||||
lastIndex = 0;
|
||||
notificationsOnPage = [];
|
||||
lastPage = 1;
|
||||
firstNumbered = 0;
|
||||
lastNumbered = 0;
|
||||
}
|
||||
|
||||
const stagedTodayCount = this.props.storageApi.getStat('stagedCount')[0];
|
||||
const stagedStatistics = this.props.storageApi.getStat(
|
||||
'stagedCount',
|
||||
this.state.currentTime.clone().startOf('week').subtract(1, 'week'),
|
||||
this.state.currentTime.clone().endOf('week')
|
||||
);
|
||||
|
||||
return (
|
||||
<Scene
|
||||
currentTime={this.state.currentTime}
|
||||
stagedStatistics={stagedStatistics}
|
||||
isFirstTimeUser={this.state.isFirstTimeUser}
|
||||
setNotificationsPermission={this.setNotificationsPermission}
|
||||
notificationsPermission={notificationsPermission}
|
||||
queuedCount={queuedCount}
|
||||
stagedCount={stagedCount}
|
||||
closedCount={closedCount}
|
||||
stagedTodayCount={stagedTodayCount || 0}
|
||||
first={firstNumbered}
|
||||
last={lastNumbered}
|
||||
lastPage={lastPage}
|
||||
allNotificationsCount={scoredAndSortedNotifications.length}
|
||||
notifications={notificationsOnPage}
|
||||
query={this.state.query}
|
||||
page={this.state.currentPage}
|
||||
activeStatus={this.state.activeStatus}
|
||||
activeFilter={this.state.activeFilter}
|
||||
onChangePage={this.onChangePage}
|
||||
onLogout={this.onLogout}
|
||||
onSetActiveStatus={this.onSetActiveStatus}
|
||||
onSearch={this.onSearch}
|
||||
onClearQuery={this.onClearQuery}
|
||||
onFetchNotifications={fetchNotifications}
|
||||
onMarkAsRead={this.enhancedOnMarkAsRead}
|
||||
onMarkAllAsStaged={markAllAsStaged}
|
||||
onClearCache={clearCache}
|
||||
onStageThread={this.enhancedOnStageThread}
|
||||
onRestoreThread={this.restoreThread}
|
||||
onRefreshNotifications={this.props.storageApi.refreshNotifications}
|
||||
isSearching={this.state.isSearching}
|
||||
isFetchingNotifications={isFetchingNotifications}
|
||||
fetchingNotificationsError={fetchingNotificationsError || this.state.error}
|
||||
onSetActiveFilter={this.onSetActiveFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const enhance = compose(
|
||||
withStorageProvider,
|
||||
withAuthProvider,
|
||||
withCookiesProvider,
|
||||
withNotificationsProvider
|
||||
);
|
||||
|
||||
export default enhance(NotificationsPage);
|
@ -255,7 +255,7 @@ export function BasicPageWrapper ({loggedIn, onLogout, children}) {
|
||||
<LoginContainer>
|
||||
<Button to={routes.GUIDE}>{'Guide'}</Button>
|
||||
<Button to={routes.PRICING}>{'Pricing'}</Button>
|
||||
<Button to={routes.REDESIGN_NOTIFICATIONS} css={css`
|
||||
<Button to={routes.NOTIFICATIONS} css={css`
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
@ -1,5 +1,4 @@
|
||||
export {default as Home} from './Home';
|
||||
export {default as Notifications} from './Notifications';
|
||||
export {default as NotificationsRedesign} from './NotificationsRedesign';
|
||||
export {default as Login} from './Login';
|
||||
export {default as Pricing} from './Pricing';
|
||||
|
Loading…
Reference in New Issue
Block a user