More UI and piping
3
.gitignore
vendored
@ -19,3 +19,6 @@
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# private
|
||||
/src/utils/mocks.js
|
||||
|
6282
package-lock.json
generated
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
|
63
src/components/Icon/index.js
Normal 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);
|
1
src/components/Icon/svg/all_inbox.svg
Normal 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 |
1
src/components/Icon/svg/back.svg
Normal 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 |
1
src/components/Icon/svg/bolt.svg
Normal 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 |
1
src/components/Icon/svg/bookmark-alt.svg
Normal 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 |
1
src/components/Icon/svg/bookmark.svg
Normal 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 |
1
src/components/Icon/svg/bookmarks.svg
Normal 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 |
1
src/components/Icon/svg/check.svg
Normal 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 |
1
src/components/Icon/svg/done-all.svg
Normal 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 |
1
src/components/Icon/svg/done.svg
Normal 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 |
1
src/components/Icon/svg/github/issue_closed.svg
Normal 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 |
1
src/components/Icon/svg/github/issue_open.svg
Normal 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 |
1
src/components/Icon/svg/github/pr_closed.svg
Normal 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 |
1
src/components/Icon/svg/github/pr_merged.svg
Normal 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 |
1
src/components/Icon/svg/github/pr_open.svg
Normal 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 |
1
src/components/Icon/svg/hot.svg
Normal 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 |
1
src/components/Icon/svg/inbox.svg
Normal 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 |
1
src/components/Icon/svg/locked.svg
Normal 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 |
1
src/components/Icon/svg/menu.svg
Normal 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 |
1
src/components/Icon/svg/next.svg
Normal 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 |
1
src/components/Icon/svg/refresh.svg
Normal 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 |
1
src/components/Icon/svg/search.svg
Normal 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 |
1
src/components/Icon/svg/settings.svg
Normal 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 |
1
src/components/Icon/svg/star-alt.svg
Normal 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 |
1
src/components/Icon/svg/star.svg
Normal 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 |
1
src/components/Icon/svg/unlocked.svg
Normal 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 |
1
src/components/Icon/svg/x.svg
Normal 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 |
@ -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} />
|
||||
|
@ -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 |
@ -1,5 +1,5 @@
|
||||
export default {
|
||||
HOME: '/',
|
||||
INBOX: 'inbox',
|
||||
NOTIFICATIONS: 'notifications',
|
||||
LOGIN: 'login'
|
||||
};
|
||||
|
12
src/enhance/index.js
Normal 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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
@ -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">
|
||||
|
@ -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);
|
@ -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 (
|
||||
|
225
src/pages/Notifications/Scene.js
Normal 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>
|
||||
);
|
||||
}
|
69
src/pages/Notifications/index.js
Normal 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);
|
@ -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';
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|