More UI and piping

This commit is contained in:
Nicholas Zuber 2018-10-31 00:06:58 -04:00
parent dd5a2a6323
commit 9ba0b4f04b
43 changed files with 3606 additions and 3193 deletions

3
.gitignore vendored
View File

@ -19,3 +19,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# private
/src/utils/mocks.js

6282
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,7 @@
"react-dev-utils": "^6.0.5",
"react-dom": "^16.6.0",
"react-emotion": "^9.2.12",
"react-svg-inline": "^2.1.1",
"recompose": "^0.30.0",
"resolve": "1.8.1",
"sass-loader": "7.1.0",

View File

@ -5,7 +5,7 @@ import { AuthProvider } from './providers/Auth';
import {
Home,
Login,
Inbox,
Notifications,
} from './pages';
class App extends Component {
@ -15,7 +15,7 @@ class App extends Component {
<Router>
<Home path={routes.HOME} />
<Login path={routes.LOGIN} />
<Inbox path={routes.INBOX} />
<Notifications path={routes.NOTIFICATIONS} />
</Router>
</AuthProvider>
);

View File

@ -0,0 +1,63 @@
import React from 'react';
import styled from 'react-emotion';
import allInbox from './svg/all_inbox.svg';
import back from './svg/back.svg';
import bolt from './svg/bolt.svg';
import bookmarkAlt from './svg/bookmark-alt.svg';
import bookmark from './svg/bookmark.svg';
import bookmarks from './svg/bookmarks.svg';
import check from './svg/check.svg';
import doneAll from './svg/done-all.svg';
import done from './svg/done.svg';
import hot from './svg/hot.svg';
import inbox from './svg/inbox.svg';
import locked from './svg/locked.svg';
import menu from './svg/menu.svg';
import next from './svg/next.svg';
import refresh from './svg/refresh.svg';
import search from './svg/search.svg';
import settings from './svg/settings.svg';
import starAlt from './svg/star-alt.svg';
import star from './svg/star.svg';
import unlocked from './svg/unlocked.svg';
import x from './svg/x.svg';
const SvgIcon = styled('div')({
position: 'relative',
backgroundSize: 'cover'
}, ({size, icon, opacity}) => ({
height: size || 24,
width: size || 24,
background: `url(${icon}) center center no-repeat`,
opacity
}));
export default function Icon ({src, ...props}) {
return <SvgIcon {...props} icon={src} />
}
const createIcon = src => props => <Icon {...props} src={src} />;
Icon.AllInbox = createIcon(allInbox);
Icon.Back = createIcon(back);
Icon.Bolt = createIcon(bolt);
Icon.BookmarkAlt = createIcon(bookmarkAlt);
Icon.Bookmark = createIcon(bookmark);
Icon.Bookmarks = createIcon(bookmarks);
Icon.Check = createIcon(check);
Icon.DoneAll = createIcon(doneAll);
Icon.Done = createIcon(done);
Icon.Hot = createIcon(hot);
Icon.Inbox = createIcon(inbox);
Icon.Locked = createIcon(locked);
Icon.Menu = createIcon(menu);
Icon.Next = createIcon(next);
Icon.Refresh = createIcon(refresh);
Icon.Search = createIcon(search);
Icon.Settings = createIcon(settings);
Icon.StarAlt = createIcon(starAlt);
Icon.Star = createIcon(star);
Icon.Unlocked = createIcon(unlocked);
Icon.X = createIcon(x);

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M19 3H5c-1.1 0-2 .9-2 2v7c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 6h-4c0 1.62-1.38 3-3 3s-3-1.38-3-3H5V5h14v4zm-4 7h6v3c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2v-3h6c0 1.66 1.34 3 3 3s3-1.34 3-3z"/></svg>

After

Width:  |  Height:  |  Size: 339 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>

After

Width:  |  Height:  |  Size: 198 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 2.02c-5.51 0-9.98 4.47-9.98 9.98s4.47 9.98 9.98 9.98 9.98-4.47 9.98-9.98S17.51 2.02 12 2.02zM11.48 20v-6.26H8L13 4v6.26h3.35L11.48 20z"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2zm0 15l-5-2.18L7 18V5h10v13z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 197 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M19 18l2 1V3c0-1.1-.9-2-2-2H8.99C7.89 1 7 1.9 7 3h10c1.1 0 2 .9 2 2v13zM15 5H5c-1.1 0-2 .9-2 2v16l7-3 7 3V7c0-1.1-.9-2-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>

After

Width:  |  Height:  |  Size: 187 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z"/></svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M7 10h2v2H7v-2zm2-6H7v5h2V4zm1.5 1.5l-1 1L12 9l4-4.5-1-1L12 7l-1.5-1.5zM8 13.7A5.71 5.71 0 0 1 2.3 8c0-3.14 2.56-5.7 5.7-5.7 1.83 0 3.45.88 4.5 2.2l.92-.92A6.947 6.947 0 0 0 8 1C4.14 1 1 4.14 1 8s3.14 7 7 7 7-3.14 7-7l-1.52 1.52c-.66 2.41-2.86 4.19-5.48 4.19v-.01z"></path></svg>

After

Width:  |  Height:  |  Size: 390 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 14 16" version="1.1" width="14" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M11 11.28V5c-.03-.78-.34-1.47-.94-2.06C9.46 2.35 8.78 2.03 8 2H7V0L4 3l3 3V4h1c.27.02.48.11.69.31.21.2.3.42.31.69v6.28A1.993 1.993 0 0 0 10 15a1.993 1.993 0 0 0 1-3.72zm-1 2.92c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zM4 3c0-1.11-.89-2-2-2a1.993 1.993 0 0 0-1 3.72v6.56A1.993 1.993 0 0 0 2 15a1.993 1.993 0 0 0 1-3.72V4.72c.59-.34 1-.98 1-1.72zm-.8 10c0 .66-.55 1.2-1.2 1.2-.65 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2zM2 4.2C1.34 4.2.8 3.65.8 3c0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2z"></path></svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M11 11.28V5c-.03-.78-.34-1.47-.94-2.06C9.46 2.35 8.78 2.03 8 2H7V0L4 3l3 3V4h1c.27.02.48.11.69.31.21.2.3.42.31.69v6.28A1.993 1.993 0 0 0 10 15a1.993 1.993 0 0 0 1-3.72zm-1 2.92c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zM4 3c0-1.11-.89-2-2-2a1.993 1.993 0 0 0-1 3.72v6.56A1.993 1.993 0 0 0 2 15a1.993 1.993 0 0 0 1-3.72V4.72c.59-.34 1-.98 1-1.72zm-.8 10c0 .66-.55 1.2-1.2 1.2-.65 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2zM2 4.2C1.34 4.2.8 3.65.8 3c0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2z"></path></svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M11 11.28V5c-.03-.78-.34-1.47-.94-2.06C9.46 2.35 8.78 2.03 8 2H7V0L4 3l3 3V4h1c.27.02.48.11.69.31.21.2.3.42.31.69v6.28A1.993 1.993 0 0 0 10 15a1.993 1.993 0 0 0 1-3.72zm-1 2.92c-.66 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2zM4 3c0-1.11-.89-2-2-2a1.993 1.993 0 0 0-1 3.72v6.56A1.993 1.993 0 0 0 2 15a1.993 1.993 0 0 0 1-3.72V4.72c.59-.34 1-.98 1-1.72zm-.8 10c0 .66-.55 1.2-1.2 1.2-.65 0-1.2-.55-1.2-1.2 0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2zM2 4.2C1.34 4.2.8 3.65.8 3c0-.65.55-1.2 1.2-1.2.65 0 1.2.55 1.2 1.2 0 .65-.55 1.2-1.2 1.2z"></path></svg>

After

Width:  |  Height:  |  Size: 698 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H4.99c-1.11 0-1.98.89-1.98 2L3 19c0 1.1.88 2 1.99 2H19c1.1 0 2-.9 2-2V5c0-1.11-.9-2-2-2zm0 12h-4c0 1.66-1.35 3-3 3s-3-1.34-3-3H4.99V5H19v10z"/><path fill="none" d="M0 0h24v24H0V0z"/></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/></svg>

After

Width:  |  Height:  |  Size: 363 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z"/></svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 372 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><path fill="none" d="M0 0h20v20H0V0z"/><path d="M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12 2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69 1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58 1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0 .49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z"/></svg>

After

Width:  |  Height:  |  Size: 774 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 330 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z"/></svg>

After

Width:  |  Height:  |  Size: 369 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@ -1,13 +1,13 @@
import React from 'react';
import loader from './loader.svg';
export default function LoadingIcon ({ style, ...props }) {
export default function LoadingIcon ({ style, size, ...props }) {
return (
<div style={{
background: `url(${loader}) center center no-repeat`,
position: 'relative',
height: 100,
width: 100,
height: size || 100,
width: size || 100,
margin: '0 auto',
...style
}} {...props} />

View File

@ -9,7 +9,7 @@
type="rotate"
from="0 9 9"
to="360 9 9"
dur=".75s"
dur=".5s"
repeatCount="indefinite"/>
</path>
</g>

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 713 B

View File

@ -1,5 +1,5 @@
export default {
HOME: '/',
INBOX: 'inbox',
NOTIFICATIONS: 'notifications',
LOGIN: 'login'
};

12
src/enhance/index.js Normal file
View File

@ -0,0 +1,12 @@
import React from 'react';
export const withOnEnter = WrappedComponent => ({onEnter, ...props}) => (
<WrappedComponent
{...props}
onKeyPress={event => {
if (event.key === 'Enter') {
onEnter(event);
}
}}
/>
);

View File

@ -72,15 +72,14 @@ export default function Scene ({loggedIn, onLogout, ...props}) {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
overflowX: 'hidden',
// background: 'radial-gradient(farthest-corner at -0% 100%, #9065ff 30%, #00ffbe 95%)'
overflowX: 'hidden'
}}>
<LandingHeader>
<Logo size={75} />
{loggedIn ? (
<div className="button-container">
<RouterLink style={{marginRight: 15}} to={routes.INBOX}>notifications</RouterLink>
<LinkButton style={{marginRight: 15}} href="#" onClick={onLogout}>logout</LinkButton>
<RouterLink style={{marginRight: 15}} to={routes.NOTIFICATIONS}>notifications</RouterLink>
<LinkButton style={{marginRight: 15}} href="#" onClick={onLogout}>sign out</LinkButton>
</div>
) : (
<div className="button-container">

View File

@ -1,31 +0,0 @@
import React from 'react';
import { Link, Redirect } from "@reach/router";
import { compose } from 'recompose';
import { withNotificationsProvider } from '../providers/Notifications';
import { withAuthProvider } from '../providers/Auth';
import { routes } from '../constants';
class InboxPage extends React.Component {
render () {
if (!this.props.authApi.token) {
return <Redirect noThrow to={routes.LOGIN} />
}
return (
<div>
Inbox
<Link to={routes.HOME}>home</Link>
<button onClick={() => {
this.props.notificationsApi.getNotifications()
}}>click</button>
</div>
);
}
};
const enhance = compose(
withAuthProvider,
withNotificationsProvider
);
export default enhance(InboxPage);

View File

@ -24,7 +24,7 @@ class LoginPage extends React.Component {
render () {
if (this.props.authApi.token) {
return <Redirect noThrow to={routes.INBOX} />
return <Redirect noThrow to={routes.NOTIFICATIONS} />
}
return (

View File

@ -0,0 +1,225 @@
import React from 'react';
import { Link } from "@reach/router";
import moment from 'moment';
import styled from 'react-emotion';
import Icon from '../../components/Icon';
import Logo from '../../components/Logo';
import LoadingIcon from '../../components/LoadingIcon';
import { routes } from '../../constants';
import { withOnEnter } from '../../enhance';
import '../../styles/gradient.css';
const NotificationsContainer = styled('div')({
position: 'relative',
boxSizing: 'border-box',
background: '#fff',
margin: '0 auto',
padding: 0,
width: '100%',
height: '100vh',
display: 'flex',
flexDirection: 'row'
});
const NavigationContainer = styled('div')({
position: 'relative',
boxSizing: 'border-box',
margin: '0 auto',
padding: '24px 48px',
width: '100%',
background: 'none',
height: 'initial'
});
const GeneralOptionsContainer = styled(NavigationContainer)({
background: '#fff',
padding: '8px 80px',
flex: '0 0 75px',
'button': {
display: 'inline-flex',
margin: 0,
marginTop: 12
}
});
const Sidebar = styled('div')({
flex: '0 0 75px',
marginTop: 15
});
const Notifications = styled('div')({
flex: 1,
});
const Tab = styled('button')({
cursor: 'pointer',
border: 0,
outline: 'none',
background: 'none',
display: 'block',
height: 40,
width: 40,
borderRadius: '100%',
margin: '0 auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transition: 'all 250ms ease',
':hover': {
background: 'rgba(190, 197, 208, 0.25)'
},
':active': {
background: 'rgba(190, 197, 208, 0.5)'
}
}, ({disabled}) => disabled && ({
background: 'none !important',
opacity: 0.5,
cursor: 'default',
}));
const SearchField = styled('div')({
float: 'left',
textAlign: 'left',
width: '50%',
boxShadow: '0 1px 3px #4a4a4a5c',
margin: '0 auto',
background: '#fff',
borderRadius: '4px',
alignItems: 'center',
padding: 0,
height: '48px',
fontSize: '14px',
textDecoration: 'none',
transition: 'all 0.12s ease-in-out',
display: 'inline-flex'
});
const LoaderContainer = styled('div')({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%'
});
const SearchInput = styled('input')({
flex: 1,
textAlign: 'left',
margin: '0 auto',
background: 'none',
padding: 0,
height: '48px',
fontSize: '14px',
textDecoration: 'none',
transition: 'all 0.12s ease-in-out',
display: 'inline-flex',
border: '0',
outline: 'none'
});
const EnhancedSearchInput = withOnEnter(SearchInput);
const NotificationRow = styled('div')({
display: 'block',
textAlign: 'left',
width: '100%',
margin: '0 auto',
background: '#fff',
padding: '8px 16px',
transition: 'all 0.12s ease-in-out',
});
export default function Scene ({
notifications,
onLogout,
onSearch,
onFetchNotifications,
isSearching,
isFetchingNotifications,
fetchingNotificationsError,
}) {
const isLoading = isSearching || isFetchingNotifications;
notifications = notifications
.sort((a, b) => b.repository.name.localeCompare(a.repository.name))
.filter(n => console.warn(n.reason === 'review_requested') || n.reason === 'review_requested');
return (
<div className="container-gradient" style={{
width: '100%',
position: 'relative',
flexDirection: 'column',
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
}}>
<NavigationContainer>
<div className="button-container" style={{ textAlign: 'right' }}>
<Logo size={48} style={{
float: 'left',
marginRight: 48,
cursor: 'pointer'
}} />
<SearchField>
<Icon.Search size={48} opacity={.45} />
<EnhancedSearchInput
disabled={isLoading}
type="text"
placeholder="Search for notifications"
onEnter={onSearch}
/>
{isSearching && <LoadingIcon size={48} />}
</SearchField>
<Link style={{marginRight: 15}} to={routes.HOME}>go home</Link>
<a style={{marginRight: 15}} href="#" onClick={onLogout}>sign out</a>
</div>
</NavigationContainer>
<GeneralOptionsContainer>
<Tab disabled={isLoading}>
<Icon.Refresh
opacity={0.9}
onClick={onFetchNotifications}
/>
</Tab>
<Tab disabled={isLoading}>
<Icon.DoneAll
opacity={0.9}
onClick={onFetchNotifications}
/>
</Tab>
</GeneralOptionsContainer>
<NotificationsContainer>
<Sidebar>
<Tab disabled={isLoading}>
<Icon.Refresh
opacity={0.9}
onClick={onFetchNotifications}
/>
</Tab>
</Sidebar>
<Notifications>
{isFetchingNotifications ? (
<LoaderContainer>
<LoadingIcon />
</LoaderContainer>
) : notifications.length <= 0 ? (
<div>
<p>no notifications</p>
</div>
) : (
<div>
{notifications.map(n => (
<NotificationRow key={n.id}>
<img width={16} src={n.repository.owner.avatar_url} />
<p style={{fontWeight: 'bold'}}>{n.repository.name}</p>
<p>{n.subject.title} ({n.subject.type}, {n.reason})</p>
<p>Last read at {n.last_read_at ? moment(n.last_read_at).format('dddd h:mma') : 'never'}</p>
<p>Last updated at {moment(n.last_updated).format('dddd h:mma')}</p>
</NotificationRow>
))}
</div>
)}
</Notifications>
</NotificationsContainer>
</div>
);
}

View File

@ -0,0 +1,69 @@
import React from 'react';
import { Redirect } from "@reach/router";
import { compose } from 'recompose';
import { withNotificationsProvider } from '../../providers/Notifications';
import { withAuthProvider } from '../../providers/Auth';
import { withCookiesProvider } from '../../providers/Cookies';
import { OAUTH_TOKEN_COOKIE } from '../../constants/cookies';
import { routes } from '../../constants';
import Scene from './Scene';
class NotificationsPage extends React.Component {
state = {
isSearching: false
}
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(() => {
console.warn(`searched for '${text}'`);
this.setState({ isSearching: false });
}, 2000);
}
render () {
if (!this.props.authApi.token) {
return <Redirect noThrow to={routes.LOGIN} />
}
const {
fetchNotifications,
notifications,
loading: isFetchingNotifications,
error: fetchingNotificationsError,
} = this.props.notificationsApi;
return (
<Scene
notifications={notifications}
onLogout={this.onLogout}
onSearch={this.onSearch}
onFetchNotifications={fetchNotifications}
isSearching={this.state.isSearching}
isFetchingNotifications={isFetchingNotifications}
fetchingNotificationsError={fetchingNotificationsError}
/>
);
}
};
const enhance = compose(
withAuthProvider,
withCookiesProvider,
withNotificationsProvider
);
export default enhance(NotificationsPage);

View File

@ -1,3 +1,3 @@
export {default as Home} from './Home';
export {default as Inbox} from './Inbox';
export {default as Notifications} from './Notifications';
export {default as Login} from './Login';

View File

@ -1,21 +1,35 @@
import React from 'react';
import { AuthConsumer } from './Auth';
import {AuthConsumer} from './Auth';
import {MockNotifications} from '../utils/mocks';
const BASE_GITHUB_API_URL = 'https://api.github.com';
class NotificationsProvider extends React.Component {
constructor (props) {
super(props);
this.last_modified = null;
}
state = {
loading: false,
error: null
error: null,
notifications: MockNotifications
}
requestPage = (page = 1) => {
const headers = {
'Authorization': `token ${this.props.token}`,
'Content-Type': 'application/json',
};
if (this.last_modified) {
headers['If-Modified-Since'] = this.last_modified;
}
return fetch(`${BASE_GITHUB_API_URL}/notifications?page=${page}`, {
method: 'GET',
headers: {
'Authorization': `token ${this.props.token}`,
'Content-Type': 'application/json'
}
headers: headers
})
.then(response => {
const entries = response.headers.entries();
@ -24,6 +38,11 @@ class NotificationsProvider extends React.Component {
headers[name] = value;
}
// If there were updates, make sure we get the newest last-modified.
if (headers['last-modified']) {
this.last_modified = headers['last-modified'];
}
const rawLinks = headers['link'];
const links = {};
@ -45,27 +64,37 @@ class NotificationsProvider extends React.Component {
});
}
getNotifications = () => {
// @TODO remove this mock when ready
mockRequestPage = page => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(MockNotifications), 1000)
});
}
fetchNotifications = () => {
if (!this.props.token) {
console.error('Unauthenitcated, aborting request.')
return false;
}
this.setState({ loading: true });
return this.requestPage(1)
return this.mockRequestPage(1)
.then(notifications => this.processNotificationsChunk(notifications))
.catch(error => this.setState({ error }))
.finally(() => this.setState({ loading: false }));
}
processNotificationsChunk = notifications => {
console.log(notifications);
processNotificationsChunk = notificationsChunk => {
console.warn(notificationsChunk);
this.setState({
notifications: notificationsChunk
});
}
render () {
return this.props.children({
...this.state,
getNotifications: this.getNotifications
fetchNotifications: this.fetchNotifications
});
}
}

View File

@ -5,10 +5,7 @@
background: #9065ff;
}
body {
background: #ffffff;
margin: 0;
padding: 0;
html, body, * {
font-family: 'Inter UI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
@ -16,6 +13,12 @@ body {
-moz-osx-font-smoothing: grayscale;
}
body {
background: #ffffff;
margin: 0;
padding: 0;
}
h1 {
font-weight: 500;
font-size: 48px;