diff --git a/public/index.html b/public/index.html index 19a370e..038a037 100644 --- a/public/index.html +++ b/public/index.html @@ -3,7 +3,7 @@ - + Meteorite — Smarter GitHub notifications diff --git a/src/components/Icon/index.js b/src/components/Icon/index.js index 09f79d4..d48641f 100644 --- a/src/components/Icon/index.js +++ b/src/components/Icon/index.js @@ -54,6 +54,9 @@ import tagWhite from './svg/tag-white.svg'; import sync from './svg/sync.svg'; import noPhone from './svg/nophone.svg'; import noMusic from './svg/nomusic.svg'; +import leftArrow from './svg/left-arrow.svg'; +import notificationsOn from './svg/notifications-on.svg'; +import notificationsOff from './svg/notifications-off.svg'; import issue_closed from './svg/github/issue-closed.svg'; import issue_open from './svg/github/issue-open.svg'; @@ -135,6 +138,9 @@ Icon.TagWhite = createIcon(tagWhite); Icon.Sync = createIcon(sync); Icon.NoPhone = createIcon(noPhone); Icon.NoMusic = createIcon(noMusic); +Icon.LeftArrow = createIcon(leftArrow); +Icon.NotificationsOn = createIcon(notificationsOn); +Icon.NotificationsOff = createIcon(notificationsOff); Icon.IssueClosed = createIcon(issue_closed); Icon.IssueOpen = createIcon(issue_open); diff --git a/src/components/Icon/svg/left-arrow.svg b/src/components/Icon/svg/left-arrow.svg new file mode 100644 index 0000000..30e3187 --- /dev/null +++ b/src/components/Icon/svg/left-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/svg/notifications-off.svg b/src/components/Icon/svg/notifications-off.svg new file mode 100644 index 0000000..a234bdf --- /dev/null +++ b/src/components/Icon/svg/notifications-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/svg/notifications-on.svg b/src/components/Icon/svg/notifications-on.svg new file mode 100644 index 0000000..9fccbfe --- /dev/null +++ b/src/components/Icon/svg/notifications-on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/svg/right-arrow.svg b/src/components/Icon/svg/right-arrow.svg new file mode 100644 index 0000000..de05024 --- /dev/null +++ b/src/components/Icon/svg/right-arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/buttons/AuthenticationButton.js b/src/components/buttons/AuthenticationButton.js index cedb13b..79b8f2d 100644 --- a/src/components/buttons/AuthenticationButton.js +++ b/src/components/buttons/AuthenticationButton.js @@ -1,12 +1,11 @@ import React from 'react'; const CLIENT_ID = '9478c90e57ef3d546ef0'; -const REDIRECT_URI = 'https://meteorite.surge.sh/login'; const SCOPES = 'notifications'; const AuthenticationButton = props => ( Authorize with GitHub diff --git a/src/constants/reasons.js b/src/constants/reasons.js index 2349def..ee18034 100644 --- a/src/constants/reasons.js +++ b/src/constants/reasons.js @@ -6,6 +6,8 @@ export const Reasons = { AUTHOR: 'author', OTHER: 'other', COMMENT: 'comment', + STATE_CHANGE: 'state_change', + TEAM_MENTION: 'team_mention' }; export const Badges = { diff --git a/src/images/icon.png b/src/images/icon.png new file mode 100644 index 0000000..f0c89fe Binary files /dev/null and b/src/images/icon.png differ diff --git a/src/images/screenshot.png b/src/images/screenshot.png new file mode 100644 index 0000000..7590a5e Binary files /dev/null and b/src/images/screenshot.png differ diff --git a/src/pages/Home/Scene.js b/src/pages/Home/Scene.js index a879443..0a0463c 100644 --- a/src/pages/Home/Scene.js +++ b/src/pages/Home/Scene.js @@ -5,6 +5,7 @@ import { routes } from '../../constants'; import Curve from '../../components/Curve'; import Icon from '../../components/Icon'; import Logo from '../../components/Logo'; +import screenshot from '../../images/screenshot.png'; import '../../styles/gradient.css'; function createImagePlaceholder (highlight) { @@ -326,13 +327,42 @@ function createImagePlaceholder (highlight) { ); } +const ImageContainer = styled('div')({ + position: 'absolute', + height: 390, + width: 685, + top: 155, + left: '50%', + background: `url(${screenshot}) center center no-repeat`, + backgroundSize: 'cover', + backgroundColor: '#fff', + boxShadow: '0 2px 8px rgba(179, 179, 179, 0.25)', + marginLeft: 100, + borderRadius: 8, + display: 'block', + '@media (max-width: 1000px)': { + display: 'none' + } +}); + +const WidthContainer = styled('div')({ + margin: '0 auto', + maxWidth: 1500, + display: 'flex', + alignItems: 'center', + flexDirection: 'row', + '@media (max-width: 1400px)': { + flexDirection: 'column' + } +}); + const Section = styled('div')({ position: 'relative', width: '100%', minHeight: 300, display: 'flex', alignItems: 'center', - flexDirection: 'row', + flexDirection: 'column', margin: '28px auto 0', padding: '60px 0' }, ({alt}) => alt && ({ @@ -341,10 +371,12 @@ const Section = styled('div')({ color: '#fff' }, 'h2': { - color: '#fff' - }, - '@media (max-width: 1000px)': { - flexDirection: 'column' + color: '#fff', + marginTop: 0, + marginLeft: 15, + fontSize: 42, + textAlign: 'left', + fontWeight: 600 } })); @@ -393,8 +425,9 @@ const ImagePlaceholder = styled('div')({ const Header = styled('h1')({ color: '#fff', padding: '0 20px', - margin: '0 auto 48px', - letterSpacing: '-1.0px' + margin: '0 0 24px', + letterSpacing: '-1.0px', + width: '50%', }); const SubHeader = styled(Header)({ @@ -408,19 +441,32 @@ const SubHeader = styled(Header)({ const LandingHeader = styled('div')({ position: 'relative', - width: '100%', - margin: '54px 20px 78px', - maxWidth: 1000, + width: '90%', + margin: '22px 20px 54px', + maxWidth: 1500, display: 'flex', justifyContent: 'space-between', }); const LandingMessage = styled(LandingHeader)({ + marginLeft: '5%', flexDirection: 'column', - textAlign: 'center', - maxWidth: 1000, + textAlign: 'left', + maxWidth: 1500, 'h1': { display: 'block' + }, + '@media (max-width: 1000px)': { + textAlign: 'center', + 'h1': { + marginLeft: 'auto', + marginRight: 'auto', + width: 500 + }, + 'div': { + marginLeft: 'auto !important', + marginRight: 'auto !important', + }, } }); @@ -443,16 +489,24 @@ const SmallText = styled('span')({ const BottomLinkContainer = styled(LandingHeader)({ maxWidth: 350, width: '100%', - margin: '32px auto 0', + margin: '32px 20px 0', }); const LinkButton = styled('a')({}); +const U = styled('span')({ + color: 'inherit',}); +// background: '#009cfb', +// padding: '0 6px 2px', +// borderRadius: 4, +// }, ({color}) => ({ +// background: color +// })); const UnofficialReleaseTag = styled('span')({ color: 'white', position: 'absolute', left: '44px', - bottom: '7px', + bottom: '9px', fontSize: '11px', background: '#f42839', fontWeight: '800', @@ -471,27 +525,61 @@ export default function Scene ({loggedIn, onLogout, ...props}) { display: 'flex', flexDirection: 'column', alignItems: 'center', - overflow: 'hidden' + overflow: 'hidden', + paddingBottom: 50 }}> - - - beta + + + beta + {loggedIn ? ( -
- notifications - sign out +
+ notifications + sign out
) : ( -
- sign in +
+ sign in
)}
Control your GitHub notifications
Prioritize the tasks that keep you and your team most productive -
+
let's get started + { + const section = document.querySelector('#learnMore'); + const y = section.getBoundingClientRect().top + window.scrollY; + window.scroll({ + top: y, + behavior: 'smooth' + }); + }} + style={{ + marginLeft: 20, + color: '#fff', + background: 'none' + }}> + learn more + +
View and contribute on GitHub @@ -508,31 +596,34 @@ export default function Scene ({loggedIn, onLogout, ...props}) { +
-
- - - {createImagePlaceholder('badges')} - - -

Surface the things that matter the most.

- - -

The most important issues and pull requests that require your presence are called out and brought to your attention.

-
- - -

We listen for updates with your notifications and let you know why and when things change.

-
- - -

Super charge your day by focusing on getting things done, rather than sifting through notifications or emails.

-
-
- +
+ + + + {createImagePlaceholder('badges')} + + +

Surface the things that matter the most.

+ + +

The most important issues and pull requests that require your presence are called out and brought to your attention.

+
+ + +

We listen for updates with your notifications and let you know why and when things change.

+
+ + +

Super charge your day by focusing on getting things done, rather than sifting through notifications or emails.

+
+
+ +
-
+
- - -

Your time matters, so
we keep things simple.

- - -

All of the information we use to make your notifications more useful is kept offline and kept on your own computer.

-
- - -

Simply sign in and start working — no complicated or intrusive set up needed.

-
- - -

No distractions — we only show you updates on things that matter to you.

-
-
- - {createImagePlaceholder('reason')} - - + + + +

Your time matters, so
we keep things simple.

+ + +

All of the information we use to make your notifications more useful is kept offline and kept on your own computer.

+
+ + +

Simply sign in and start working — no complicated or intrusive set up needed.

+
+ + +

No distractions — we only show you updates on things that matter to you.

+
+
+ + {createImagePlaceholder('reason')} + + +
+
+
+

+ Meteorite is the assistant for your
GitHub notifications. +

+ + + + +

All of the information we use to make your notifications more useful is kept offline and kept on your own computer.

+
+ + +

Simply sign in and start working — no complicated or intrusive set up needed.

+
+ + +

No distractions — we only show you updates on things that matter to you.

+
+
+ + + +

All of the information we use to make your notifications more useful is kept offline and kept on your own computer.

+
+ + +

Simply sign in and start working — no complicated or intrusive set up needed.

+
+ + +

No distractions — we only show you updates on things that matter to you.

+
+
+
); diff --git a/src/pages/Notifications/Scene.js b/src/pages/Notifications/Scene.js index 27811ed..677c536 100644 --- a/src/pages/Notifications/Scene.js +++ b/src/pages/Notifications/Scene.js @@ -16,18 +16,29 @@ import '../../styles/gradient.css'; /* eslint-disable jsx-a11y/anchor-is-valid */ /* eslint-disable no-script-url */ -function getMessageFromReasons (reasons) { +function stringOfType (type) { + switch (type) { + case 'PullRequest': + return 'pull request'; + case 'Issue': + return 'issue'; + default: + return 'task'; + } +} + +function getMessageFromReasons (reasons, type) { switch (reasons[reasons.length - 1].reason) { case Reasons.ASSIGN: - return 'You were just assigned a task'; + return 'You were assigned this ' + stringOfType(type); case Reasons.AUTHOR: - return 'There was activity on a thread you created'; + return 'There was activity on this thread you created'; case Reasons.COMMENT: - return 'Somebody just left a comment'; + return 'Somebody left a comment'; case Reasons.MENTION: - return 'You were just @mentioned'; + return 'You were @mentioned'; case Reasons.REVIEW_REQUESTED: - return 'Your review was just requested'; + return 'Your review was requested for this ' + stringOfType(type); case Reasons.SUBSCRIBED: return 'There was an update and you\'re subscribed'; case Reasons.OTHER: @@ -86,11 +97,9 @@ const InlineBlockContainer = styled('div')({ const NotificationsContainer = styled('div')({ position: 'relative', - background: '#fff', margin: '0 auto', padding: 0, width: '100%', - height: '100%', display: 'flex', flexDirection: 'row', overflowX: 'hidden', @@ -117,7 +126,6 @@ const GeneralOptionsContainer = styled(NavigationContainer)({ minHeight: 60, width: '95%', margin: 0, - background: '#fff', padding: '8px 16px', paddingTop: 18, flex: '0 0 50px', @@ -350,7 +358,6 @@ const NotificationRow = styled('tr')({ width: '100%', borderRadius: 4, margin: '0 auto', - background: '#fff', padding: '8px 16px', transition: 'all 0.1s ease-in-out', boxSizing: 'border-box', @@ -468,6 +475,9 @@ function getPRIssueIcon (type, reasons) { } export default function Scene ({ + currentTime, + isFirstTimeUser, + notificationsPermission, queuedCount, stagedCount, closedCount, @@ -496,12 +506,23 @@ export default function Scene ({ fetchingNotificationsError, activeFilter, onSetActiveFilter, + setNotificationsPermission }) { const loading = isSearching || isFetchingNotifications; const isFirstPage = page === 1; const isLastPage = page === lastPage; + const NotificationsIcon = notificationsPermission === 'granted' + ? Icon.NotificationsOn + : Icon.NotificationsOff; + console.log('notifications', notifications) + console.log('isFirstTimeUser', isFirstTimeUser) + console.log('notificationsPermission', notificationsPermission) + + if (isFirstTimeUser && notifications.length > 5) { + // alert('hello, clear ur shit'); + } return (
@@ -578,14 +599,14 @@ export default function Scene ({ marginRight: '5px', top: '-3px', }} /> - {moment().format('h:mma')} + {currentTime.format('h:mma')} {moment().format('dddd, MMMM Do')} + }}>{currentTime.format('dddd, MMMM Do')} + + { + switch(notificationsPermission) { + case 'granted': + return setNotificationsPermission('denied'); + case 'denied': + case 'default': + default: + Notification.requestPermission().then(result => { + return setNotificationsPermission(result); + }); + } + }) : undefined} + /> + {query ? (
@@ -856,7 +894,7 @@ export default function Scene ({ }} shrink={.5} /> - {getMessageFromReasons(n.reasons)} + {getMessageFromReasons(n.reasons, n.type)} diff --git a/src/pages/Notifications/index.js b/src/pages/Notifications/index.js index 3e60932..cf68d01 100644 --- a/src/pages/Notifications/index.js +++ b/src/pages/Notifications/index.js @@ -13,7 +13,7 @@ import { Status } from '../../constants/status'; import { Reasons, Badges } from '../../constants/reasons'; import Scene from './Scene'; -const PER_PAGE = 15; +const PER_PAGE = 10; // @TODO Move these functions. @@ -64,7 +64,6 @@ function scoreOf (notification) { return score; }; -// @TODO implement this function badgesOf (notification) { const badges = []; const len = notification.reasons.length; @@ -96,13 +95,15 @@ function badgesOf (notification) { }; const scoreOfReason = { - [Reasons.ASSIGN]: 18, - [Reasons.AUTHOR]: 10, - [Reasons.MENTION]: 12, - [Reasons.OTHER]: 2, - [Reasons.REVIEW_REQUESTED]: 30, + [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 => ({ @@ -113,6 +114,9 @@ const decorateWithScore = notification => ({ class NotificationsPage extends React.Component { state = { + currentTime: moment(), + prevNotifications: [], + isFirstTimeUser: false, isSearching: false, query: null, activeFilter: Filters.PARTICIPATING, @@ -121,10 +125,17 @@ class NotificationsPage extends React.Component { } componentDidMount () { - this.props.notificationsApi.fetchNotifications(); + const isFirstTimeUser = !this.props.storageApi.getUserItem('hasOnboarded'); + if (isFirstTimeUser) { + this.setState({isFirstTimeUser: true}); + // this.props.storageApi.setUserItem('hasOnboarded', true); + } + + this.props.notificationsApi.fetchNotifications(); this.syncer = setInterval(() => { this.props.notificationsApi.fetchNotificationsSync(); + this.setState({currentTime: moment()}); }, 8 * 1000); } @@ -132,6 +143,10 @@ class NotificationsPage extends React.Component { clearInterval(this.syncer); } + diffForWebNotification (nextProps, nextState) { + this.sendWebNotification(); + } + onChangePage = page => { this.setState({ currentPage: page }); } @@ -181,6 +196,16 @@ class NotificationsPage extends React.Component { this.props.notificationsApi.restoreThread(thread_id); } + setNotificationsPermission = (...args) => { + this.props.notificationsApi.setNotificationsPermission(...args); + } + + sendWebNotification = () => { + var img = '../images/icon.png'; + var text = 'HEY! Your task "null" is now overdue.'; + var notification = new Notification('Meteorite', { body: text, icon: img }); + } + render () { if (!this.props.authApi.token) { return @@ -191,37 +216,40 @@ class NotificationsPage extends React.Component { markAsRead, markAllAsStaged, clearCache, + notificationsPermission, notifications, loading: isFetchingNotifications, error: fetchingNotificationsError, } = this.props.notificationsApi; // @TODO Move all this out of the render method. + // nick, do this ^ so you can fire off a web noti when the filtered/final + // notifications have a diff let filterMethod = () => true; switch (this.state.activeFilter) { case Filters.PARTICIPATING: filterMethod = n => ( n.reasons.some(({ reason }) => ( - reason === 'review_requested' || - reason === 'assign' || - reason === 'mention' || - reason === 'author' + reason === Reasons.REVIEW_REQUESTED || + reason === Reasons.ASSIGN || + reason === Reasons.MENTION || + reason === Reasons.AUTHOR )) ); break; case Filters.ASSIGNED: filterMethod = n => ( - n.reasons.some(({ reason }) => reason === 'assign') + n.reasons.some(({ reason }) => reason === Reasons.ASSIGN) ); break; case Filters.REVIEW_REQUESTED: filterMethod = n => ( - n.reasons.some(({ reason }) => reason === 'review_requested') + n.reasons.some(({ reason }) => reason === Reasons.REVIEW_REQUESTED) ); break; case Filters.COMMENT: filterMethod = n => ( - n.reasons.some(({ reason }) => reason === 'comment') + n.reasons.some(({ reason }) => reason === Reasons.COMMENT) ); break; default: @@ -279,6 +307,10 @@ class NotificationsPage extends React.Component { return ( { + this.setState({notificationsPermission: permission}); + this.props.setUserItem('notificationsPermission', permission); + this.forceUpdate(); + } + requestPage = (page = 1, optimizePolling = true) => { const headers = { 'Authorization': `token ${this.props.token}`, @@ -107,16 +121,23 @@ class NotificationsProvider extends React.Component { } requestFetchNotifications = (page = 1, optimizePolling = true) => { + if (this.state.syncing) { + // Don't try to send off another request if we're already trying to get one. + return Promise.reject(); + } + + this.setState({syncing: true}); return this.requestPage(page, optimizePolling) .then(({headers, json}) => { - if (json === null) return; + if (json === null) return []; let nextPage = null; const links = headers['link']; if (links && links.next && links.next.page) { nextPage = links.next.page; } return this.processNotificationsChunk(nextPage, json); - }); + }) + .finally(() => this.setState({syncing: false})); } fetchNotifications = (page = 1, optimizePolling = true) => { @@ -125,9 +146,15 @@ class NotificationsProvider extends React.Component { return false; } + if (this.state.loading) { + // Don't try to fetch if we're already fetching + return Promise.reject(); + } + this.setState({ loading: true }); return this.requestFetchNotifications(page, optimizePolling) - .catch(error => this.setState({ error })) + .then(() => this.setState({error: null})) + .catch(error =>this.setState({error})) .finally(() => this.setState({ loading: false })); } @@ -199,9 +226,15 @@ class NotificationsProvider extends React.Component { return false; } + if (this.state.loading) { + // Don't try to fetch if we're already fetching + return Promise.reject(); + } + this.setState({ loading: true }); return this.requestMarkAsRead(thread_id) - .catch(error => this.setState({ error })) + .then(() => this.setState({error: null})) + .catch(error => this.setState({error})) .finally(() => this.setState({ loading: false })); } @@ -217,7 +250,8 @@ class NotificationsProvider extends React.Component { clearCache = () => { this.setState({ loading: true }); return this.requestClearCache() - .catch(error => this.setState({ error })) + .then(() => this.setState({error: null})) + .catch(error => this.setState({error})) .finally(() => this.setState({ loading: false })); } @@ -278,21 +312,24 @@ class NotificationsProvider extends React.Component { markAllAsStaged = () => { this.setState({ loading: true }); return this.requestStageAll() - .catch(error => this.setState({ error })) + .then(() => this.setState({error: null})) + .catch(error => this.setState({error})) .finally(() => this.setState({ loading: false })); } stageThread = thread_id => { this.setState({ loading: true }); return this.requestStageThread(thread_id) - .catch(error => this.setState({ error })) + .then(() => this.setState({error: null})) + .catch(error => this.setState({error})) .finally(() => this.setState({ loading: false })); } restoreThread = thread_id => { this.setState({ loading: true }); return this.requestRestoreThread(thread_id) - .catch(error => this.setState({ error })) + .then(() => this.setState({error: null})) + .catch(error => this.setState({error})) .finally(() => this.setState({ loading: false })); } @@ -337,6 +374,7 @@ class NotificationsProvider extends React.Component { clearCache: this.clearCache, stageThread: this.stageThread, restoreThread: this.restoreThread, + setNotificationsPermission: this.setNotificationsPermission, }); } } @@ -350,6 +388,8 @@ const withNotificationsProvider = WrappedComponent => props => ( notifications, getItem, setItem, + getUserItem, + setUserItem, clearCache, removeItem }) => ( @@ -358,6 +398,8 @@ const withNotificationsProvider = WrappedComponent => props => ( notifications={notifications} getItemFromStorage={getItem} setItemInStorage={setItem} + getUserItem={getUserItem} + setUserItem={setUserItem} clearStorageCache={clearCache} removeItemFromStorage={removeItem} token={token} diff --git a/src/providers/Storage.js b/src/providers/Storage.js index f5165a0..7950c6e 100644 --- a/src/providers/Storage.js +++ b/src/providers/Storage.js @@ -1,38 +1,14 @@ import React from 'react'; import moment from 'moment'; import {Status} from '../constants/status'; -import {Reasons} from '../constants/reasons'; +import {createMockNotifications} from '../utils/mocks'; + +const mockNotifications = createMockNotifications(100); export const LOCAL_STORAGE_PREFIX = '__meteorite_noti_cache__'; +export const LOCAL_STORAGE_USER_PREFIX = '__meteorite_user_cache__'; export const LOCAL_STORAGE_STATISTIC_PREFIX = '__meteorite_statistic_cache__'; -const getMockReasons = n => { - const reasons = Object.values(Reasons); - const len = reasons.length; - return new Array(n).fill(0).map(_ => ({ - reason: reasons[Math.floor(Math.random() * len)], - time: moment().format() - })); -}; - -const getMockNotification = randomNumber => ({ - id: randomNumber, - updated_at: moment().format(), - status: (randomNumber > 0.8 ? Status.STAGED : Status.QUEUED), - reasons: getMockReasons(Math.ceil(randomNumber * 10)), - type: ['Issue', 'PullRequest'][Math.floor(randomNumber * 2)], - name: 'Mock - Fake notification name', - url: 'https://github.com/test/repo/pull', - repository: 'test/mock', - number: Math.ceil(randomNumber * 1000), - repositoryUrl: 'https://github.com/test/repo', -}); - -const mockNotifications = new Array(1000); -for (let i = 0; i < mockNotifications.length; i++) { - mockNotifications[i] = getMockNotification(Math.random()); -} - class StorageProvider extends React.Component { constructor (props) { super(props); @@ -142,10 +118,22 @@ class StorageProvider extends React.Component { try { return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`)); } catch (e) { - return null; + return window.localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${id}`); } } + getUserItem = id => { + try { + return JSON.parse(window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`)); + } catch (e) { + return window.localStorage.getItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`); + } + } + + setUserItem = (id, value) => { + window.localStorage.setItem(`${LOCAL_STORAGE_USER_PREFIX}${id}`, JSON.stringify(value)); + } + removeItem = id => { // We never really want to purge anything from the cache if we can help it, // since there's always a chance that a read notification can be resurrected. @@ -169,6 +157,8 @@ class StorageProvider extends React.Component { ...this.state, setItem: this.setItem, getItem: this.getItem, + getUserItem: this.getUserItem, + setUserItem: this.setUserItem, removeItem: this.removeItem, clearCache: this.clearCache, refreshNotifications: this.refreshNotifications, diff --git a/src/styles/gradient.css b/src/styles/gradient.css index 857a75e..74243e8 100644 --- a/src/styles/gradient.css +++ b/src/styles/gradient.css @@ -11,6 +11,7 @@ } .button-container a { + position: relative; text-align: center; box-shadow: 0 4px 6px rgba(50,50,93,.11), 0 1px 3px rgba(0,0,0,.08); margin: 0 auto; @@ -38,13 +39,25 @@ display: -ms-inline-flexbox; display: inline-flex; } -.button-container a:hover { - background: #f9f9f9; - box-shadow: 0 2px 6px #4a4a4a5c; + +.button-container a:before { + content: ""; + transition: all 150ms ease; + background: rgba(190, 197, 208, 0.25); + border-radius: 4px; + display: block; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + transform: scale(0); } -.button-container a:active { - background: #e4e4e4; - box-shadow: 0 0 0 #4a4a4a5c; +.button-container a:hover:before { + transform: scale(1); +} +.button-container a:active:before { + background: rgba(190, 197, 208, 0.5) } .button-container-alt a { @@ -114,10 +127,10 @@ } @media only screen and (max-width: 1400px) { - #section { + .section { flex-direction: column !important; } - #item-text { + .item-text { width: 600px; } }