diff --git a/.env.development b/.env.development index f7e01b62..a01d84f8 100644 --- a/.env.development +++ b/.env.development @@ -72,4 +72,4 @@ NEXT_PUBLIC_GROWTHBOOK_ENV=dev ########## Estimated Money Per Tab ########## -EST_MONEY_RAISED_PER_TAB=0.000765 \ No newline at end of file +NEXT_PUBLIC_EST_MONEY_RAISED_PER_TAB=0.000765 \ No newline at end of file diff --git a/.env.local.info b/.env.local.info index f2c7b1ef..fb996529 100644 --- a/.env.local.info +++ b/.env.local.info @@ -33,4 +33,4 @@ NEXT_PUBLIC_GROWTHBOOK_ENV=local ########## Estimated Money Per Tab ########## -EST_MONEY_RAISED_PER_TAB=0.000765 \ No newline at end of file +NEXT_PUBLIC_EST_MONEY_RAISED_PER_TAB=0.000765 \ No newline at end of file diff --git a/.env.preview.info b/.env.preview.info index cf76523d..67eed70b 100644 --- a/.env.preview.info +++ b/.env.preview.info @@ -69,3 +69,7 @@ NEXT_PUBLIC_MEDIA_ENDPOINT=https://prod-tab2017-media.gladly.io ########## Growthbook ########## NEXT_PUBLIC_GROWTHBOOK_ENV=dev + +########## Estimated Money Per Tab ########## + +NEXT_PUBLIC_EST_MONEY_RAISED_PER_TAB=0.000765 diff --git a/.env.production.info b/.env.production.info index 7fa91a60..ab367548 100644 --- a/.env.production.info +++ b/.env.production.info @@ -77,4 +77,4 @@ NEXT_PUBLIC_GROWTHBOOK_ENV=production ########## Estimated Money Per Tab ########## -EST_MONEY_RAISED_PER_TAB=0.000765 \ No newline at end of file +NEXT_PUBLIC_EST_MONEY_RAISED_PER_TAB=0.000765 \ No newline at end of file diff --git a/package.json b/package.json index 0d4a2060..1bda927f 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "next-transpile-modules": "^8.0.0", "prop-types": "^15.7.2", "react": "^17.0.2", + "react-color": "^2.19.3", "react-dom": "^17.0.2", "react-firebaseui": "^5.0.2", "react-relay": "^12.0.0", @@ -68,7 +69,7 @@ "remark-parse": "^9.0.0", "remark-rehype": "^8.0.0", "swr": "^1.0.1", - "tab-ads": "^1.1.10", + "tab-ads": "^1.1.23", "tab-cmp": "^0.15.0-alpha.0", "unified": "^9.0.0", "uuid": "^8.3.2" diff --git a/src/__tests__/pages/account.test.js b/src/__tests__/pages/account.test.js index 63b246e4..f3322151 100644 --- a/src/__tests__/pages/account.test.js +++ b/src/__tests__/pages/account.test.js @@ -27,6 +27,7 @@ import localStorageFeaturesManager from 'src/utils/localStorageFeaturesManager' import Switch from '@material-ui/core/Switch' import localStorageMgr from 'src/utils/localstorage-mgr' import { STORAGE_NEW_USER_IS_TAB_V4_BETA } from 'src/utils/constants' +import UpdateWidgetEnabledMutation from 'src/utils/mutations/UpdateWidgetEnabledMutation' jest.mock('next-offline/runtime') jest.mock('tab-cmp') @@ -49,6 +50,7 @@ jest.mock('src/utils/localStorageFeaturesManager', () => ({ getFeatureValue: jest.fn(), })) jest.mock('src/utils/localstorage-mgr', () => ({ removeItem: jest.fn() })) +jest.mock('src/utils/mutations/UpdateWidgetEnabledMutation') const getMockDataResponse = () => ({ user: { @@ -62,6 +64,17 @@ const getMockDataResponse = () => ({ }, name: 'Cats', }, + widgets: { + edges: [ + { + node: { + id: 'fake-widget-id-bookmarks', + type: 'bookmarks', + enabled: false, + }, + }, + ], + }, }, app: { causes: { @@ -321,6 +334,54 @@ describe('account.js', () => { const content = wrapper.find(Switch) expect(content.length).toEqual(1) }) + + it('updates Bookmarks widget data and calls mutation if applicable user', async () => { + expect.assertions(3) + const AccountPage = require('src/pages/account').default + const mockProps = getMockProps() + const defaultMockData = getMockDataResponse() + useData.mockReturnValue({ data: defaultMockData }) + localStorageFeaturesManager.getFeatureValue.mockReturnValue('true') + const wrapper = mount() + + expect(wrapper.find(Switch).first().prop('checked')).toEqual(false) + + await act(async () => { + wrapper.find(Switch).first().prop('onChange')({ + target: { checked: true }, + }) + await flushAllPromises() + wrapper.update() + }) + + expect(UpdateWidgetEnabledMutation).toHaveBeenCalledWith( + defaultMockData.user, + { + id: 'fake-widget-id-bookmarks', + type: 'bookmarks', + enabled: false, + }, + true + ) + + await act(async () => { + wrapper.find(Switch).first().prop('onChange')({ + target: { checked: true }, + }) + await flushAllPromises() + wrapper.update() + }) + + expect(UpdateWidgetEnabledMutation).toHaveBeenCalledWith( + defaultMockData.user, + { + id: 'fake-widget-id-bookmarks', + type: 'bookmarks', + enabled: false, + }, + false + ) + }) }) const getRevertAccountItem = (wrapper) => diff --git a/src/__tests__/pages/index.test.js b/src/__tests__/pages/index.test.js index ee9d38ce..e16f9ea2 100644 --- a/src/__tests__/pages/index.test.js +++ b/src/__tests__/pages/index.test.js @@ -10,6 +10,7 @@ import { STORAGE_NEW_USER_CAUSE_ID, HAS_SEEN_SEARCH_V2_TOOLTIP, CAUSE_IMPACT_TYPES, + WIDGET_TYPE_BOOKMARKS, } from 'src/utils/constants' import { showMockAchievements, @@ -59,6 +60,8 @@ import { isSearchActivityComponentSupported } from 'src/utils/browserSupport' import localStorageFeaturesManager from 'src/utils/localStorageFeaturesManager' import moment from 'moment' import GroupImpactContainer from 'src/components/groupImpactComponents/GroupImpactContainer' +import AddShortcutPageContainer from 'src/components/AddShortcutPageContainer' +import FrontpageShortcutListContainer from 'src/components/FrontpageShortcutListContainer' jest.mock('uuid') uuid.mockReturnValue('some-uuid') @@ -165,6 +168,20 @@ const getMockProps = () => ({ notifications: [], searches: 10, showSfacIcon: false, + widgets: { + edges: [ + { + node: { + enabled: true, + id: 'abcde', + type: WIDGET_TYPE_BOOKMARKS, + data: JSON.stringify({ + bookmarks: [], + }), + }, + }, + ], + }, }, userImpact: { userId: 'asdf', @@ -195,6 +212,7 @@ beforeEach(() => { useDoesBrowserSupportSearchExtension.mockReturnValue(true) useBrowserName.mockReturnValue('chrome') MockDate.set(moment(mockNow)) + localStorageFeaturesManager.getFeatureValue.mockReturnValue('false') }) afterEach(() => { @@ -1544,6 +1562,33 @@ describe('index.js', () => { }) }) +it('does display shortcut components if applicable', async () => { + localStorageFeaturesManager.getFeatureValue.mockReturnValue('true') + const IndexPage = require('src/pages/index').default + const defaultMockProps = getMockProps() + useData.mockReturnValue({ data: defaultMockProps.data }) + const wrapper = mount() + + expect(wrapper.find(FrontpageShortcutListContainer).exists()).toBe(true) + + wrapper + .find(FrontpageShortcutListContainer) + .find(IconButton) + .simulate('click') + + expect(wrapper.find(AddShortcutPageContainer).exists()).toBe(true) +}) + +it('does not shortcut components if applicable', async () => { + const IndexPage = require('src/pages/index').default + const defaultMockProps = getMockProps() + useData.mockReturnValue({ data: defaultMockProps.data }) + const wrapper = mount() + + expect(wrapper.find(FrontpageShortcutListContainer).exists()).toBe(false) + expect(wrapper.find(AddShortcutPageContainer).exists()).toBe(false) +}) + /* END: core tests */ /* START: notification tests */ @@ -1556,7 +1601,7 @@ describe('index.js: hardcoded notifications', () => { const IndexPage = require('src/pages/index').default const mockProps = getMockProps() useData.mockReturnValue({ data: mockProps.data, isDataFresh: true }) - const wrapper = mount() + const wrapper = shallow() const notification = wrapper.find(Notification) expect(notification.exists()).not.toBe(true) }) diff --git a/src/assets/images/shortcut.png b/src/assets/images/shortcut.png new file mode 100644 index 00000000..88728b1f Binary files /dev/null and b/src/assets/images/shortcut.png differ diff --git a/src/assets/promos/allexpress.png b/src/assets/promos/allexpress.png new file mode 100644 index 00000000..8f8b3bbc Binary files /dev/null and b/src/assets/promos/allexpress.png differ diff --git a/src/assets/promos/bookshop.png b/src/assets/promos/bookshop.png new file mode 100644 index 00000000..477258c7 Binary files /dev/null and b/src/assets/promos/bookshop.png differ diff --git a/src/assets/promos/glossier.png b/src/assets/promos/glossier.png new file mode 100644 index 00000000..acf40a94 Binary files /dev/null and b/src/assets/promos/glossier.png differ diff --git a/src/assets/promos/horse.png b/src/assets/promos/horse.png new file mode 100644 index 00000000..2538f75d Binary files /dev/null and b/src/assets/promos/horse.png differ diff --git a/src/assets/promos/kiwico.png b/src/assets/promos/kiwico.png new file mode 100644 index 00000000..a7fb0a21 Binary files /dev/null and b/src/assets/promos/kiwico.png differ diff --git a/src/assets/promos/lego.png b/src/assets/promos/lego.png new file mode 100644 index 00000000..5c651920 Binary files /dev/null and b/src/assets/promos/lego.png differ diff --git a/src/assets/promos/lowes.png b/src/assets/promos/lowes.png new file mode 100644 index 00000000..a79a8830 Binary files /dev/null and b/src/assets/promos/lowes.png differ diff --git a/src/assets/promos/macys.png b/src/assets/promos/macys.png new file mode 100644 index 00000000..8c831cba Binary files /dev/null and b/src/assets/promos/macys.png differ diff --git a/src/assets/promos/microsoft.png b/src/assets/promos/microsoft.png new file mode 100644 index 00000000..6128ac4d Binary files /dev/null and b/src/assets/promos/microsoft.png differ diff --git a/src/assets/promos/oldnavy.png b/src/assets/promos/oldnavy.png new file mode 100644 index 00000000..dc951c40 Binary files /dev/null and b/src/assets/promos/oldnavy.png differ diff --git a/src/assets/promos/samsung.png b/src/assets/promos/samsung.png new file mode 100644 index 00000000..ead8b338 Binary files /dev/null and b/src/assets/promos/samsung.png differ diff --git a/src/assets/promos/sephora.png b/src/assets/promos/sephora.png new file mode 100644 index 00000000..db5f881b Binary files /dev/null and b/src/assets/promos/sephora.png differ diff --git a/src/assets/promos/sonos.png b/src/assets/promos/sonos.png new file mode 100644 index 00000000..b6bc4c7c Binary files /dev/null and b/src/assets/promos/sonos.png differ diff --git a/src/assets/promos/thriftbooks.png b/src/assets/promos/thriftbooks.png new file mode 100644 index 00000000..b920cfa6 Binary files /dev/null and b/src/assets/promos/thriftbooks.png differ diff --git a/src/assets/promos/ultra-beauty.png b/src/assets/promos/ultra-beauty.png new file mode 100644 index 00000000..f63e0aac Binary files /dev/null and b/src/assets/promos/ultra-beauty.png differ diff --git a/src/assets/promos/walmart.png b/src/assets/promos/walmart.png new file mode 100644 index 00000000..08ac6231 Binary files /dev/null and b/src/assets/promos/walmart.png differ diff --git a/src/assets/promos/zulily.png b/src/assets/promos/zulily.png new file mode 100644 index 00000000..07607f0b Binary files /dev/null and b/src/assets/promos/zulily.png differ diff --git a/src/components/AddShortcut.js b/src/components/AddShortcut.js index eedaeaf8..dbe80698 100644 --- a/src/components/AddShortcut.js +++ b/src/components/AddShortcut.js @@ -1,9 +1,10 @@ -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' import { makeStyles } from '@material-ui/core/styles' import Button from '@material-ui/core/Button' import PropTypes from 'prop-types' import { Typography } from '@material-ui/core' import TextField from '@material-ui/core/TextField' +import { addProtocolToURLIfNeeded } from 'src/utils/urls' import Notification from './Notification' const useStyles = makeStyles((theme) => ({ @@ -34,32 +35,80 @@ const useStyles = makeStyles((theme) => ({ marginBottom: theme.spacing(2), }, })) -const AddShortcut = ({ onCancel, onSave }) => { - const [open, setOpen] = useState(true) - const [name, setName] = useState('') - const [url, setUrl] = useState('') + +const isValidUrl = (urlString) => { + try { + return Boolean(new URL(urlString)) + } catch (e) { + return false + } +} + +const AddShortcut = ({ + onCancel, + onSave, + existingName, + existingUrl, + existingId, +}) => { + const [name, setName] = useState(existingName) + const [url, setUrl] = useState(existingUrl) + const [nameError, setNameError] = useState(null) + const [urlError, setUrlError] = useState(null) + useEffect(() => setName(existingName), [existingName]) + useEffect(() => setUrl(existingUrl), [existingUrl]) const classes = useStyles() const onCancelClick = () => { - setName('') - setUrl('') + setName(existingName) + setUrl(existingUrl) onCancel() - setOpen(false) + } + const validateName = (newName) => { + let newNameError + if (newName.length === 0) { + newNameError = 'Shortcut name cannot be empty.' + } else { + newNameError = null + } + setNameError(newNameError) + return newNameError + } + const validateUrl = (newUrl) => { + let newUrlError + if (newUrl.length === 0) { + newUrlError = 'URL cannot be empty.' + } else if (!isValidUrl(addProtocolToURLIfNeeded(newUrl))) { + newUrlError = 'URL is not valid.' + } else { + newUrlError = null + } + setUrlError(newUrlError) + return newUrlError + } + const validateForm = () => { + const newNameError = validateName(name) + const newUrlError = validateUrl(url) + return newNameError || newUrlError } const onSaveClick = () => { - onSave(name, url) - setName('') - setUrl('') - setOpen(false) + const newUrl = addProtocolToURLIfNeeded(url) + if (validateForm()) { + return + } + onSave(existingId, name, newUrl) + setName(name) + setUrl(newUrl) } const changeName = (e) => { setName(e.target.value) + validateName(e.target.value) } const changeUrl = (e) => { setUrl(e.target.value) + validateUrl(e.target.value) } return ( { gutterBottom color="primary" > - Add Shortcut + {existingName === '' && existingUrl === '' ? 'Add' : 'Edit'}{' '} + Shortcut @@ -78,6 +128,8 @@ const AddShortcut = ({ onCancel, onSave }) => { label="Name" value={name} onChange={changeName} + error={!!nameError} + helperText={nameError} className={classes.textField} /> { label="URL" value={url} onChange={changeUrl} + error={!!urlError} + helperText={urlError} className={classes.textField} /> @@ -105,6 +159,7 @@ const AddShortcut = ({ onCancel, onSave }) => { className={classes.yesButton} variant="contained" disableElevation + disabled={!!(nameError || urlError)} > Save @@ -117,11 +172,17 @@ const AddShortcut = ({ onCancel, onSave }) => { AddShortcut.propTypes = { onCancel: PropTypes.func, onSave: PropTypes.func, + existingName: PropTypes.string, + existingUrl: PropTypes.string, + existingId: PropTypes.string, } AddShortcut.defaultProps = { onCancel: () => {}, onSave: () => {}, + existingName: '', + existingUrl: '', + existingId: null, } export default AddShortcut diff --git a/src/components/AddShortcut.stories.jsx b/src/components/AddShortcut.stories.jsx index 50fb7e82..21181396 100644 --- a/src/components/AddShortcut.stories.jsx +++ b/src/components/AddShortcut.stories.jsx @@ -33,3 +33,9 @@ const Template = (args) => { export const basic = Template.bind({}) basic.args = {} + +export const existingValues = Template.bind({}) +existingValues.args = { + existingName: 'Google', + existingUrl: 'http://www.google.com', +} diff --git a/src/components/AddShortcutPage.js b/src/components/AddShortcutPage.js new file mode 100644 index 00000000..a329c999 --- /dev/null +++ b/src/components/AddShortcutPage.js @@ -0,0 +1,323 @@ +import React, { useState } from 'react' +import { makeStyles } from '@material-ui/core/styles' +import Button from '@material-ui/core/Button' +import PropTypes from 'prop-types' +import { Backdrop, Typography } from '@material-ui/core' +import shortcutImage from 'src/assets/images/shortcut.png' +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline' +import SearchInput from 'src/components/SearchInput' +import Alert from '@material-ui/lab/Alert' +import InfoIcon from '@material-ui/icons/InfoOutlined' +import Logo from 'src/components/Logo' +import IconButton from '@material-ui/core/IconButton' +import CloseIcon from '@material-ui/icons/Close' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' +import { goTo } from 'src/utils/navigation' +import { accountURL } from 'src/utils/urls' +import UpdateWidgetDataMutation from 'src/utils/mutations/UpdateWidgetDataMutation' +import { WIDGET_TYPE_BOOKMARKS } from 'src/utils/constants' +import { v4 as uuid } from 'uuid' +import AddShortcut from './AddShortcut' +import ShortcutIcon from './ShortcutIcon' + +const useStyles = makeStyles((theme) => ({ + content: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'center', + color: 'white', + height: '100%', + width: '100%', + }, + shortcut: { + width: '300px', + }, + addIcon: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + margin: 'auto', + height: '150px', + width: '120px', + borderRadius: '10px', + color: 'white', + }, + circle: { + height: '48px', + width: '48px', + }, + white: { + color: 'white', + borderColor: 'white', + }, + topBar: { + padding: theme.spacing(2), + marginTop: theme.spacing(3), + display: 'flex', + flexDirection: 'row', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + }, + closeButton: { + color: 'white', + }, + center: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + shortcutText: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + topContent: { + width: '100%', + }, + backdrop: { + width: '100vw', + backgroundColor: 'rgba(0, 0, 0, 0.8)', + position: 'absolute', + }, + buttonRoot: { + padding: '0px', + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1), + marginLeft: theme.spacing(1), + }, + shortcutWrapper: { + maxWidth: '600px', + maxHeight: '300px', + overflowY: 'auto', + marginBottom: theme.spacing(6), + }, + shortcutIcons: { + margin: 'auto', + display: 'flex', + flexDirection: 'row', + flexWrap: 'wrap', + }, + addShortcutModal: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + zIndex: 1, + }, + addShortcutWrapper: { + width: '400px', + }, + label: { + flexDirection: 'column', + }, +})) +const AddShortcutPage = ({ app, user, userId, closeHandler }) => { + const { widgets: { edges: widgetNodes = [] } = {} } = user + const bookmarkWidget = widgetNodes.find( + (widgetNode) => widgetNode.node.type === WIDGET_TYPE_BOOKMARKS + ) + const bookmarksData = + bookmarkWidget && bookmarkWidget.node.data + ? JSON.parse(bookmarkWidget.node.data).bookmarks || [] + : [] + const [addShortcutWidgetOpen, setAddShortcutWidgetOpen] = useState(false) + const [bookmarks, setBookmarks] = useState(bookmarksData) + const [currentId, setCurrentId] = useState('') + const [currentName, setCurrentName] = useState('') + const [currentUrl, setCurrentUrl] = useState('') + const shortcutWidgetCancel = () => { + setAddShortcutWidgetOpen(false) + } + const saveBookmark = async (id, name, link) => { + const existingIndex = bookmarks.findIndex((bookmark) => bookmark.id === id) + let newBookmarks + if (existingIndex === -1) { + newBookmarks = [ + ...bookmarks, + { + id, + name, + link, + }, + ] + } else { + newBookmarks = [...bookmarks] + newBookmarks[existingIndex] = { + id, + name, + link, + } + } + setBookmarks(newBookmarks) + setAddShortcutWidgetOpen(false) + setCurrentId(id) + setCurrentName(name) + setCurrentUrl(link) + await UpdateWidgetDataMutation( + user, + bookmarkWidget.node, + JSON.stringify({ bookmarks: newBookmarks }) + ) + } + + const deleteBookmark = async (id) => { + const newBookmarks = bookmarks.filter((b) => b.id !== id) + setBookmarks(newBookmarks) + await UpdateWidgetDataMutation( + user, + bookmarkWidget.node, + JSON.stringify({ bookmarks: newBookmarks }) + ) + } + + const classes = useStyles() + const onSettingsClick = () => { + goTo(accountURL) + } + + const onShortcutEdit = (id, text, url) => { + setCurrentId(id) + setCurrentName(text) + setCurrentUrl(url) + setAddShortcutWidgetOpen(true) + } + + const onNewShortcut = () => { + setCurrentId(uuid()) + setCurrentName('') + setCurrentUrl('') + setAddShortcutWidgetOpen(true) + } + + const shortcutIcons = bookmarks.map((bookmark) => ( + + )) + + return ( + + + + + + + + + + + + + Add Shortcut + + + + {bookmarks.length > 0 ? ( + + {shortcutIcons} + + ) : ( + + + + Add more shortcuts and they’ll appear here! + + + )} + + } + severity="info" + variant="outlined" + className={classes.white} + > + Don't want shortcuts? You can hide them from your settings page. + + Settings + + + + + + + + + + + + + + ) +} + +AddShortcutPage.propTypes = { + userId: PropTypes.string.isRequired, + app: PropTypes.shape({ + searchEngines: PropTypes.shape({ + edges: PropTypes.arrayOf( + PropTypes.shape({ + node: PropTypes.shape({ + engineId: PropTypes.string, + name: PropTypes.string, + rank: PropTypes.number, + isCharitable: PropTypes.bool, + inputPrompt: PropTypes.string, + }), + }) + ), + }), + }).isRequired, + user: PropTypes.shape({ + searchEngine: PropTypes.shape({ + engineId: PropTypes.string, + inputPrompt: PropTypes.string, + searchUrlPersonalized: PropTypes.string, + }), + widgets: PropTypes.shape({ + edges: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + data: PropTypes.string, + type: PropTypes.string, + }) + ), + }).isRequired, + yahooPaidSearchRewardOptIn: PropTypes.bool, + }).isRequired, + closeHandler: PropTypes.func.isRequired, +} + +export default AddShortcutPage diff --git a/src/components/AddShortcutPage.stories.jsx b/src/components/AddShortcutPage.stories.jsx new file mode 100644 index 00000000..a5b42983 --- /dev/null +++ b/src/components/AddShortcutPage.stories.jsx @@ -0,0 +1,201 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import { WIDGET_TYPE_BOOKMARKS } from 'src/utils/constants' +import AddShortcutPage from './AddShortcutPage' + +export default { + title: 'Components/AddShortcutPage', + component: AddShortcutPage, + parameters: { + backgrounds: { + default: 'grey', + values: [ + { name: 'grey', value: '#F2F2F2' }, + { name: 'black', value: '#000000' }, + ], + }, + }, +} + +const useStyles = makeStyles((theme) => ({ + widthDiv: { + width: theme.spacing(200), + }, +})) + +const Template = (args) => { + const classes = useStyles() + return ( + + + + ) +} + +export const standard = Template.bind({}) +standard.args = { + tooltip: 'Great! You can always switch your search engine here later on.', + app: { + searchEngines: { + edges: [ + { + node: { + name: 'DuckDuckGo', + engineId: 'DuckDuckGo', + rank: 3, + isCharitable: false, + inputPrompt: 'Search DuckDuckGo', + }, + }, + { + node: { + name: 'Google', + engineId: 'Google', + rank: 1, + isCharitable: false, + inputPrompt: 'Search Google', + }, + }, + { + node: { + name: 'Ecosia', + engineId: 'Ecosia', + rank: 2, + isCharitable: false, + inputPrompt: 'Search Ecosia', + }, + }, + ], + }, + }, + userId: 'abcd', + user: { + searchEngine: { + name: 'Google', + engineId: 'Google', + inputPrompt: 'Search Google', + }, + widgets: { + edges: [ + { + node: { + id: 'abcde', + data: JSON.stringify({ + bookmarks: [ + { + id: 'abcd', + name: 'google', + link: 'https://www.google.com', + }, + { + id: 'bcde', + name: 'espn', + link: 'https://www.espn.com', + }, + { + id: 'cdef', + name: 'google2', + link: 'https://www.google2.com', + }, + { + id: 'defg', + name: 'espn2', + link: 'https://www.espn2.com', + }, + { + id: 'efgh', + name: 'google3', + link: 'https://www.google.com', + }, + { + id: 'fghi', + name: 'espn3', + link: 'https://www.espn.com', + }, + { + id: 'ghij', + name: 'google4', + link: 'https://www.google2.com', + }, + { + id: 'hijk', + name: 'espn4', + link: 'https://www.espn2.com', + }, + { + id: 'lmno', + name: 'espn5', + link: 'https://www.espn2.com', + }, + { + id: 'pqrs', + name: 'espn5', + link: 'https://www.espn2.com', + }, + { + id: 'qrst', + name: 'espn6', + link: 'https://www.espn2.com', + }, + ], + }), + type: WIDGET_TYPE_BOOKMARKS, + }, + }, + ], + }, + yahooPaidSearchRewardOptIn: false, + }, + closeHandler: () => {}, +} + +export const noIcons = Template.bind({}) +noIcons.args = { + tooltip: 'Great! You can always switch your search engine here later on.', + app: { + searchEngines: { + edges: [ + { + node: { + name: 'DuckDuckGo', + engineId: 'DuckDuckGo', + rank: 3, + isCharitable: false, + inputPrompt: 'Search DuckDuckGo', + }, + }, + { + node: { + name: 'Google', + engineId: 'Google', + rank: 1, + isCharitable: false, + inputPrompt: 'Search Google', + }, + }, + { + node: { + name: 'Ecosia', + engineId: 'Ecosia', + rank: 2, + isCharitable: false, + inputPrompt: 'Search Ecosia', + }, + }, + ], + }, + }, + userId: 'abcd', + user: { + searchEngine: { + name: 'Google', + engineId: 'Google', + inputPrompt: 'Search Google', + }, + widgets: { + edges: [], + }, + yahooPaidSearchRewardOptIn: false, + }, + closeHandler: () => {}, +} diff --git a/src/components/AddShortcutPageContainer.js b/src/components/AddShortcutPageContainer.js new file mode 100644 index 00000000..677fed30 --- /dev/null +++ b/src/components/AddShortcutPageContainer.js @@ -0,0 +1,42 @@ +import { createFragmentContainer, graphql } from 'react-relay' +import AddShortcutPage from 'src/components/AddShortcutPage' + +export default createFragmentContainer(AddShortcutPage, { + app: graphql` + fragment AddShortcutPageContainer_app on App { + searchEngines { + edges { + node { + engineId + name + rank + isCharitable + inputPrompt + } + } + } + } + `, + user: graphql` + fragment AddShortcutPageContainer_user on User { + id + searchEngine { + engineId + inputPrompt + searchUrlPersonalized + } + widgets { + edges { + node { + id + data + name + type + enabled + } + } + } + yahooPaidSearchRewardOptIn + } + `, +}) diff --git a/src/components/BackgroundColorPicker.js b/src/components/BackgroundColorPicker.js new file mode 100644 index 00000000..884da4f9 --- /dev/null +++ b/src/components/BackgroundColorPicker.js @@ -0,0 +1,71 @@ +import React, { useCallback, useState } from 'react' +import { makeStyles } from '@material-ui/core/styles' +import { SketchPicker } from 'react-color' +import { Typography } from '@material-ui/core' +import PropTypes from 'prop-types' + +const useStyles = makeStyles((theme) => ({ + root: { + display: 'flex', + flexWrap: 'wrap', + }, + gridList: { + display: 'flex', + flexDirection: 'row', + width: '100%', + }, + previewContainer: { + marginTop: 0, + margin: theme.spacing(2), + width: '100%', + height: '60%', + }, + header: { + paddingLeft: 0, + }, + divider: { + marginBottom: 10, + }, +})) +const BackgroundColorPicker = ({ user, onBackgroundColorSelection }) => { + const classes = useStyles() + const [selectedColor, setSelectedColor] = useState( + user.backgroundColor || '#000' + ) + + const onColorChanged = useCallback( + (color) => { + setSelectedColor(color.hex) + onBackgroundColorSelection(color.hex) + }, + [onBackgroundColorSelection] + ) + + return ( + + Select your color + + + + + + ) +} + +BackgroundColorPicker.propTypes = { + user: PropTypes.shape({ + backgroundColor: PropTypes.string.isRequired, + }).isRequired, + onBackgroundColorSelection: PropTypes.func.isRequired, +} + +export default BackgroundColorPicker diff --git a/src/components/BackgroundColorPicker.stories.jsx b/src/components/BackgroundColorPicker.stories.jsx new file mode 100644 index 00000000..ea98bcef --- /dev/null +++ b/src/components/BackgroundColorPicker.stories.jsx @@ -0,0 +1,37 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import blue from '@material-ui/core/colors/blue' +import BackgroundColorPicker from './BackgroundColorPicker' + +export default { + title: 'Components/BackgroundColorPicker', + component: BackgroundColorPicker, +} + +const useStyles = makeStyles(() => ({ + templateContainer: { + background: blue['200'], + height: 700, + }, +})) + +// eslint-disable-next-line react/jsx-props-no-spreading +const Template = (args) => { + const classes = useStyles() + return ( + + + + ) +} + +export const normal = Template.bind({}) +normal.args = { + user: { + backgroundColor: '#138', + }, + onBackgroundColorSelection: () => { + // eslint-disable-next-line no-console + console.log('onBackgroundColorSelection') + }, +} diff --git a/src/components/SfacActivityButton.js b/src/components/BadgeButton.js similarity index 85% rename from src/components/SfacActivityButton.js rename to src/components/BadgeButton.js index 9be0c77b..4309af29 100644 --- a/src/components/SfacActivityButton.js +++ b/src/components/BadgeButton.js @@ -1,9 +1,8 @@ -import React, { forwardRef } from 'react' +import React, { cloneElement, forwardRef } from 'react' import PropTypes from 'prop-types' import { makeStyles } from '@material-ui/core/styles' import clsx from 'clsx' import Badge from '@material-ui/core/Badge' -import SearchIcon from '@material-ui/icons/Search' import CheckCircleIcon from '@material-ui/icons/CheckCircle' import IconButton from '@material-ui/core/IconButton' import DoNotDisturbOnIcon from '@mui/icons-material/DoNotDisturbOn' @@ -47,7 +46,7 @@ const useStyles = makeStyles((theme) => ({ }, })) -const SfacActivityButton = forwardRef(({ active, onClick }, ref) => { +const BadgeButton = forwardRef(({ active, onClick, icon }, ref) => { const classes = useStyles() return ( @@ -70,17 +69,20 @@ const SfacActivityButton = forwardRef(({ active, onClick }, ref) => { } > - + {cloneElement(icon, { + className: classes.searchIcon, + })} ) }) -SfacActivityButton.propTypes = { +BadgeButton.propTypes = { onClick: PropTypes.func.isRequired, active: PropTypes.bool.isRequired, + icon: PropTypes.node.isRequired, } -SfacActivityButton.defaultProps = {} +BadgeButton.defaultProps = {} -export default SfacActivityButton +export default BadgeButton diff --git a/src/components/BadgeButton.stories.jsx b/src/components/BadgeButton.stories.jsx new file mode 100644 index 00000000..cf7085be --- /dev/null +++ b/src/components/BadgeButton.stories.jsx @@ -0,0 +1,51 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import blue from '@material-ui/core/colors/blue' +import SearchIcon from '@material-ui/icons/Search' +import { ShoppingCart } from '@material-ui/icons' +import BadgeButton from './BadgeButton' + +export default { + title: 'Components/BadgeButton', + component: BadgeButton, +} + +const useStyles = makeStyles(() => ({ + templateContainer: { + background: blue['200'], + padding: 24, + }, +})) + +const Template = (args) => { + const classes = useStyles() + return ( + + + + ) +} + +export const sfacActive = Template.bind({}) +sfacActive.args = { + active: true, + icon: , +} + +export const sfacInactive = Template.bind({}) +sfacInactive.args = { + active: false, + icon: , +} + +export const shacActive = Template.bind({}) +shacActive.args = { + active: true, + icon: , +} + +export const shacInactive = Template.bind({}) +shacInactive.args = { + active: false, + icon: , +} diff --git a/src/components/CauseIcon.js b/src/components/CauseIcon.js index 77450146..ca0b5a1b 100644 --- a/src/components/CauseIcon.js +++ b/src/components/CauseIcon.js @@ -4,6 +4,7 @@ import PetsIcon from '@material-ui/icons/Pets' import FavoriteIcon from '@material-ui/icons/Favorite' import SvgIcon from '@material-ui/core/SvgIcon' import TransgenderIcon from '@mui/icons-material/Transgender' +import AttachMoneyIcon from '@mui/icons-material/AttachMoney' import { mdiJellyfish, mdiHandshake, @@ -22,6 +23,7 @@ const MEDICAL_BAG = 'medical-bag' const FOOD_APPLE = 'food-apple' const WATER = 'water' const PERSON_HEART = 'person-heart' +const MONEY = 'money' const TRANSGENDER = 'transgender' const iconOptions = [ @@ -34,6 +36,7 @@ const iconOptions = [ WATER, PERSON_HEART, TRANSGENDER, + MONEY, ] const CauseIcon = ({ icon, className }) => { @@ -126,6 +129,9 @@ const CauseIcon = ({ icon, className }) => { ) break + case MONEY: + iconComp = + break case TRANSGENDER: iconComp = break diff --git a/src/components/FrontpageShortcutList.js b/src/components/FrontpageShortcutList.js new file mode 100644 index 00000000..ce327f7f --- /dev/null +++ b/src/components/FrontpageShortcutList.js @@ -0,0 +1,215 @@ +import React, { useEffect, useState } from 'react' +import PropTypes from 'prop-types' +import { AddCircleOutline } from '@mui/icons-material' +import { makeStyles } from '@material-ui/core/styles' +import Typography from '@material-ui/core/Typography' +import Button from '@material-ui/core/Button' +import IconButton from '@material-ui/core/IconButton' +import { KeyboardArrowDown } from '@material-ui/icons' +import UpdateWidgetDataMutation from 'src/utils/mutations/UpdateWidgetDataMutation' +import { WIDGET_TYPE_BOOKMARKS } from 'src/utils/constants' +import { v4 as uuid } from 'uuid' +import { Backdrop } from '@material-ui/core' +import ShortcutIcon from './ShortcutIcon' +import AddShortcut from './AddShortcut' + +const useStyles = makeStyles((theme) => ({ + wrapper: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }, + button: { + height: '150px', + width: '110px', + maxWidth: '110px', + borderRadius: '10px', + padding: theme.spacing(1), + color: 'white', + '&:hover': { + backgroundColor: 'rgba(0,0,0,0.5)', + }, + textTransform: 'none', + }, + shortcutList: { + display: 'flex', + flexDirection: 'row', + maxWidth: '550px', + flexWrap: 'wrap', + zIndex: 1.4e3, + }, + addCircle: { + marginTop: '24px', + width: '86px', + height: '86px', + }, + label: { + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + openButton: { + color: 'white', + }, + addShortcutModal: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + zIndex: 10001, + }, + addShortcutWrapper: { + width: '400px', + position: 'relative', + }, +})) +const FrontpageShortcutList = ({ openHandler, user }) => { + const { widgets: { edges: widgetNodes = [] } = {} } = user + const [bookmarks, setBookmarks] = useState([]) + const [bookmarkWidget, setBookmarkWidget] = useState(null) + + useEffect(() => { + setBookmarkWidget( + widgetNodes.find( + (widgetNode) => widgetNode.node.type === WIDGET_TYPE_BOOKMARKS + ) + ) + const bookmarksData = + bookmarkWidget && bookmarkWidget.node.data + ? JSON.parse(bookmarkWidget.node.data).bookmarks || [] + : [] + setBookmarks(bookmarksData) + }, [widgetNodes, bookmarkWidget]) + + const [addShortcutWidgetOpen, setAddShortcutWidgetOpen] = useState(false) + const [currentId, setCurrentId] = useState('') + const [currentName, setCurrentName] = useState('') + const [currentUrl, setCurrentUrl] = useState('') + + const saveBookmark = async (id, name, link) => { + const existingIndex = bookmarks.findIndex((bookmark) => bookmark.id === id) + let newBookmarks + if (existingIndex === -1) { + newBookmarks = [ + ...bookmarks, + { + id, + name, + link, + }, + ] + } else { + newBookmarks = [...bookmarks] + newBookmarks[existingIndex] = { + id, + name, + link, + } + } + setBookmarks(newBookmarks) + setAddShortcutWidgetOpen(false) + setCurrentId(id) + setCurrentName(name) + setCurrentUrl(link) + await UpdateWidgetDataMutation( + user, + bookmarkWidget.node, + JSON.stringify({ bookmarks: newBookmarks }) + ) + } + + const deleteBookmark = async (id) => { + const newBookmarks = bookmarks.filter((b) => b.id !== id) + setBookmarks(newBookmarks) + await UpdateWidgetDataMutation( + user, + bookmarkWidget.node, + JSON.stringify({ bookmarks: newBookmarks }) + ) + } + + const onShortcutEdit = (id, text, url) => { + setCurrentId(id) + setCurrentName(text) + setCurrentUrl(url) + setAddShortcutWidgetOpen(true) + } + + const onNewShortcut = () => { + setCurrentId(uuid()) + setCurrentName('') + setCurrentUrl('') + setAddShortcutWidgetOpen(true) + } + + const shortcutWidgetCancel = () => { + setAddShortcutWidgetOpen(false) + } + + const classes = useStyles() + const shortcutIcons = bookmarks + .slice(-9) + .map((bookmark) => ( + + )) + return ( + + + {shortcutIcons} + + + Add Shortcut + + + + + + + + + + + + ) +} +FrontpageShortcutList.propTypes = { + user: PropTypes.shape({ + widgets: PropTypes.shape({ + edges: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + data: PropTypes.string, + type: PropTypes.string, + }) + ), + }).isRequired, + yahooPaidSearchRewardOptIn: PropTypes.bool, + }).isRequired, + openHandler: PropTypes.func.isRequired, +} + +export default FrontpageShortcutList diff --git a/src/components/FrontpageShortcutList.stories.jsx b/src/components/FrontpageShortcutList.stories.jsx new file mode 100644 index 00000000..66919e2e --- /dev/null +++ b/src/components/FrontpageShortcutList.stories.jsx @@ -0,0 +1,109 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import blue from '@material-ui/core/colors/blue' +import { WIDGET_TYPE_BOOKMARKS } from 'src/utils/constants' +import FrontpageShortcutList from './FrontpageShortcutList' + +export default { + title: 'Components/FrontpageShortcutList', + component: FrontpageShortcutList, +} + +const useStyles = makeStyles(() => ({ + templateContainer: { + background: blue['200'], + height: 700, + width: 700, + }, +})) + +// eslint-disable-next-line react/jsx-props-no-spreading +const Template = (args) => { + const classes = useStyles() + return ( + + + + ) +} +export const standard = Template.bind({}) +standard.args = { + userId: 'userId', + user: { + widgets: { + edges: [ + { + node: { + id: 'abcde', + data: JSON.stringify({ + bookmarks: [ + { + id: 'abcd', + name: 'google', + link: 'https://www.google.com', + }, + { + id: 'bcde', + name: 'espn', + link: 'https://www.espn.com', + }, + { + id: 'cdef', + name: 'google2', + link: 'https://www.google2.com', + }, + { + id: 'defg', + name: 'espn2', + link: 'https://www.espn2.com', + }, + { + id: 'efgh', + name: 'google3', + link: 'https://www.google.com', + }, + { + id: 'fghi', + name: 'espn3', + link: 'https://www.espn.com', + }, + { + id: 'ghij', + name: 'google4', + link: 'https://www.google2.com', + }, + { + id: 'hijk', + name: 'google2', + link: 'https://www.google2.com', + }, + { + id: 'ijkl', + name: 'espn2', + link: 'https://www.espn2.com', + }, + { + id: 'jklm', + name: 'google3', + link: 'https://www.google.com', + }, + { + id: 'klmn', + name: 'espn3', + link: 'https://www.espn.com', + }, + { + id: 'lmno', + name: 'google4', + link: 'https://www.google2.com', + }, + ], + }), + type: WIDGET_TYPE_BOOKMARKS, + }, + }, + ], + }, + }, + openHandler: () => {}, +} diff --git a/src/components/FrontpageShortcutListContainer.js b/src/components/FrontpageShortcutListContainer.js new file mode 100644 index 00000000..b6c16f60 --- /dev/null +++ b/src/components/FrontpageShortcutListContainer.js @@ -0,0 +1,21 @@ +import { createFragmentContainer, graphql } from 'react-relay' +import FrontpageShortcutList from 'src/components/FrontpageShortcutList' + +export default createFragmentContainer(FrontpageShortcutList, { + user: graphql` + fragment FrontpageShortcutListContainer_user on User { + id + widgets { + edges { + node { + id + data + name + type + enabled + } + } + } + } + `, +}) diff --git a/src/components/Link.js b/src/components/Link.js index 5ee90548..bbbf0ef3 100644 --- a/src/components/Link.js +++ b/src/components/Link.js @@ -1,3 +1,4 @@ +/* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ import React, { useEffect, useState } from 'react' import PropTypes from 'prop-types' import clsx from 'clsx' @@ -13,7 +14,7 @@ const useStyles = makeStyles(() => ({ })) const Link = (props) => { - const { children, className, target, to = '', style } = props + const { children, className, target, to = '', style, stopPropagation } = props const classes = useStyles() const [destInternal, setDestInternal] = useState(true) @@ -41,9 +42,14 @@ const Link = (props) => { anchorTarget = destInternal ? undefined : '_top' } + const stopPropagationHandler = (e) => { + e.stopPropagation() + } + return ( +export const demo = Template.bind({}) +demo.args = { + to: groupImpactLeaderboardFAQ, + children: 'Hello', + target: '_blank', +} + +const TemplateWithHandler = (args) => { + const clickHandler = () => { + // eslint-disable-next-line no-console + console.log('click handler') + } + return ( + + + + ) +} +export const demoStopPropagation = TemplateWithHandler.bind({}) +demoStopPropagation.args = { + to: groupImpactLeaderboardFAQ, + children: 'Hello', + stopPropagation: true, + target: '_blank', +} diff --git a/src/components/SfacActivity.js b/src/components/SfacActivity.js index bb964624..db415c72 100644 --- a/src/components/SfacActivity.js +++ b/src/components/SfacActivity.js @@ -2,9 +2,10 @@ import React, { useRef, useState } from 'react' import PropTypes from 'prop-types' import { makeStyles } from '@material-ui/core/styles' import DashboardPopover from 'src/components/DashboardPopover' -import SfacActivityButton from 'src/components/SfacActivityButton' +import BadgeButton from 'src/components/BadgeButton' import SfacActivityNotification from 'src/components/SfacActivityNotification' import { SFAC_ACTIVITY_STATES } from 'src/utils/constants' +import SearchIcon from '@material-ui/icons/Search' const useStyles = makeStyles(() => ({ popover: { @@ -28,12 +29,13 @@ const SfacActivity = ({ const [isPopoverOpen, setIsPopoverOpen] = useState(false) return ( - { setIsPopoverOpen(true) }} ref={buttonRef} + icon={} /> ({ - templateContainer: { - background: blue['200'], - padding: 24, - }, -})) - -const Template = (args) => { - const classes = useStyles() - return ( - - - - ) -} - -export const active = Template.bind({}) -active.args = { - active: true, -} - -export const inactive = Template.bind({}) -inactive.args = { - active: false, -} diff --git a/src/components/ShacActivity.js b/src/components/ShacActivity.js new file mode 100644 index 00000000..bf7f8299 --- /dev/null +++ b/src/components/ShacActivity.js @@ -0,0 +1,76 @@ +import React, { useRef, useState } from 'react' +import PropTypes from 'prop-types' +import { makeStyles } from '@material-ui/core/styles' +import DashboardPopover from 'src/components/DashboardPopover' +import BadgeButton from 'src/components/BadgeButton' +import ShacActivityNotification from 'src/components/ShacActivityNotification' +import { SHAC_ACTIVITY_STATES } from 'src/utils/constants' +import { ShoppingCart } from '@mui/icons-material' + +const useStyles = makeStyles(() => ({ + popover: { + // Match other popovers in the user menu + marginTop: 9, + }, + popoverPaper: { + borderRadius: 15, + }, + popoverContent: { + width: 400, + }, +})) + +const ShacActivity = ({ + user: { cause = {}, shacTotalEarned, shacActivityState }, +}) => { + const classes = useStyles() + const { name: causeName } = cause + const buttonRef = useRef(undefined) + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + return ( + + { + setIsPopoverOpen(true) + }} + ref={buttonRef} + icon={} + /> + { + setIsPopoverOpen(false) + }} + className={classes.popover} + PaperProps={{ + classes: { + root: classes.popoverPaper, + }, + }} + > + + + + ) +} + +ShacActivity.propTypes = { + user: PropTypes.shape({ + cause: PropTypes.shape({ + name: PropTypes.string.isRequired, + }).isRequired, + shacTotalEarned: PropTypes.number.isRequired, + shacActivityState: PropTypes.string.isRequired, + }).isRequired, +} + +ShacActivity.defaultProps = {} + +export default ShacActivity diff --git a/src/components/ShacActivity.stories.jsx b/src/components/ShacActivity.stories.jsx new file mode 100644 index 00000000..da990e14 --- /dev/null +++ b/src/components/ShacActivity.stories.jsx @@ -0,0 +1,61 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import blue from '@material-ui/core/colors/blue' +import ShacActivity from './ShacActivity' + +export default { + title: 'Components/ShacActivity', + component: ShacActivity, +} + +const useStyles = makeStyles(() => ({ + templateContainer: { + background: blue['200'], + padding: 24, + paddingLeft: 96, + width: '100%', + minHeight: 400, + }, +})) + +const Template = (args) => { + const classes = useStyles() + return ( + + + + ) +} + +export const active = Template.bind({}) +active.args = { + user: { + cause: { + name: 'Trees', + }, + shacTotalEarned: 200.3, + shacActivityState: 'active', + }, +} + +export const inactive = Template.bind({}) +inactive.args = { + user: { + cause: { + name: 'Reproductive Health', + }, + shacTotalEarned: 150.25, + shacActivityState: 'inactive', + }, +} + +export const newStatus = Template.bind({}) +newStatus.args = { + user: { + cause: { + name: 'Cats', + }, + totalEarned: 0, + shacActivityState: 'new', + }, +} diff --git a/src/components/ShacActivityNotification.js b/src/components/ShacActivityNotification.js new file mode 100644 index 00000000..fab6346c --- /dev/null +++ b/src/components/ShacActivityNotification.js @@ -0,0 +1,194 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import Button from '@material-ui/core/Button' +import PropTypes from 'prop-types' +import { Typography } from '@material-ui/core' +import CheckCircleIcon from '@material-ui/icons/CheckCircle' +import DoNotDisturbOnIcon from '@mui/icons-material/DoNotDisturbOn' +import { shopLandingURL } from 'src/utils/urls' +import Link from 'src/components/Link' +import { currencyFormatUSD } from 'src/utils/formatting' +import Notification from './Notification' + +const statusIconSize = 18 + +const useStyles = makeStyles((theme) => ({ + notification: { + paddingLeft: theme.spacing(3), + paddingRight: theme.spacing(3), + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + borderRadius: 15, + }, + noButton: { + fontWeight: '500', + }, + yesButton: { + fontWeight: '900', + marginLeft: theme.spacing(1), + }, + title: { + fontWeight: '700', + fontSize: '24px', + }, + text: { + paddingBottom: theme.spacing(3.5), + }, + buttonsWrapper: { + display: 'flex', + justifyContent: 'flex-end', + }, + active: { + color: '#219653', + }, + inactive: { + color: '#E3720E', + }, + status: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + paddingBottom: theme.spacing(2.5), + }, + statusWrapper: { + paddingBottom: theme.spacing(2.5), + }, + shopContainer: { + display: 'flex', + flexDirection: 'row', + }, + shopSubContainer: { + flexGrow: 1, + paddingRight: theme.spacing(2), + }, + statusText: { + paddingRight: theme.spacing(0.5), + fontWeight: 700, + }, + shop: { + fontWeight: 900, + }, + statusIcon: { + height: statusIconSize, + width: statusIconSize, + }, +})) +const ShacActivityNotification = ({ + className, + activityState, + totalEarned, + impactName, +}) => { + const classes = useStyles() + let buttons = null + if (activityState === 'inactive') { + buttons = ( + + + + Activate Extension + + + + ) + } else if (activityState === 'new') { + buttons = ( + + + + Get it Now + + + + ) + } + return ( + + + + Shop for a Cause + + {activityState === 'active' ? ( + + + + Active + + + + + You've been increasing your impact for causes by using Shop + for a Cause. Great job! + + + ) : ( + + + + Inactive + + + + {activityState === 'inactive' ? ( + + You haven’t used Shop for a Cause in a while! Shopping + raises up to 4x more for {impactName} than just opening + tabs. + + ) : ( + + You can do even more good with our Shop for a Cause + extension. Shopping raises up to 4x more for {impactName}{' '} + than just opening tabs. + + )} + + )} + + + + {currencyFormatUSD(totalEarned)} + + + Raised for Charities via Shopping + + + + } + includeButton={activityState !== 'active'} + buttons={buttons} + className={classes.notification} + /> + + ) +} + +ShacActivityNotification.propTypes = { + className: PropTypes.string, + activityState: PropTypes.string.isRequired, + totalEarned: PropTypes.number.isRequired, + impactName: PropTypes.string.isRequired, +} + +ShacActivityNotification.defaultProps = { + className: '', +} + +export default ShacActivityNotification diff --git a/src/components/ShacActivityNotification.stories.jsx b/src/components/ShacActivityNotification.stories.jsx new file mode 100644 index 00000000..2b533c1e --- /dev/null +++ b/src/components/ShacActivityNotification.stories.jsx @@ -0,0 +1,39 @@ +import React from 'react' +import { makeStyles } from '@material-ui/core/styles' +import ShacActivityNotification from './ShacActivityNotification' + +export default { + title: 'Components/ShacActivityNotification', + component: ShacActivityNotification, +} + +const useStyles = makeStyles((theme) => ({ + widthDiv: { + width: theme.spacing(50), + }, +})) + +const Template = (args) => { + const classes = useStyles() + return +} +export const active = Template.bind({}) +active.args = { + activityState: 'active', + totalEarned: 350.5, + impactName: 'Trees', +} + +export const inactive = Template.bind({}) +inactive.args = { + activityState: 'inactive', + totalEarned: 150.25, + impactName: 'Reproductive Health', +} + +export const newStatus = Template.bind({}) +newStatus.args = { + activityState: 'new', + totalEarned: 13.25, + impactName: 'Cats', +} diff --git a/src/components/ShortcutIcon.js b/src/components/ShortcutIcon.js index 012cd3c9..1aead3a0 100644 --- a/src/components/ShortcutIcon.js +++ b/src/components/ShortcutIcon.js @@ -9,17 +9,16 @@ import IconButton from '@material-ui/core/IconButton' import Fade from '@material-ui/core/Fade' import DeleteIcon from '@material-ui/icons/Delete' import EditIcon from '@material-ui/icons/Edit' +import CheckIcon from '@material-ui/icons/Check' import Link from 'src/components/Link' +import CloseIcon from '@material-ui/icons/Close' +import { addProtocolToURLIfNeeded } from 'src/utils/urls' const useStyles = makeStyles((theme) => ({ button: { height: '150px', - width: '96px', - maxWidth: '96px', - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'center', + width: '110px', + maxWidth: '110px', borderRadius: '10px', padding: theme.spacing(1), }, @@ -39,6 +38,7 @@ const useStyles = makeStyles((theme) => ({ '&:hover': { color: 'white', }, + color: '#cccccc', }, letterIcon: { width: '70px', @@ -61,39 +61,105 @@ const useStyles = makeStyles((theme) => ({ paddingRight: theme.spacing(1), color: 'white', }, + link: { + height: '100%', + width: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + }, + deleteText: { + color: 'white', + textAlign: 'center', + }, + confirmDialog: { + height: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, })) -const ShortcutIcon = ({ onEdit, onDelete, text, url }) => { +const ShortcutIcon = ({ onEdit, onDelete, text, url, id }) => { const getFirstTwoLetters = (str) => { const words = str.split(' ') const firstLetters = words.map((word) => word.charAt(0)) return firstLetters.slice(0, 2).join('') } const [hover, setHover] = useState(false) + const [confirmDelete, setConfirmDelete] = useState(false) const classes = useStyles() const firstLettersText = getFirstTwoLetters(text) + const onDeleteConfirmHandler = (event) => { + onDelete(id) + event.preventDefault() + } + const onDeleteRejectHandler = (event) => { + setConfirmDelete(false) + event.preventDefault() + } + const onDeleteHandler = (event) => { + setConfirmDelete(true) + event.preventDefault() + } + const onEditHandler = (event) => { + onEdit(id, text, url) + event.preventDefault() + } return ( - - setHover(true)} - onMouseOut={() => setHover(false)} - > - + setHover(true)} + onMouseOut={() => setHover(false)} + > + {confirmDelete ? ( + - - + + - - + + - - - {firstLettersText} + Confirm Delete - {text} - - + ) : ( + + + + + + + + + + + + + {firstLettersText} + + {text} + + )} + ) } @@ -102,6 +168,7 @@ ShortcutIcon.propTypes = { onDelete: PropTypes.func, text: PropTypes.string.isRequired, url: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, } ShortcutIcon.defaultProps = { diff --git a/src/components/ShortcutIcon.stories.jsx b/src/components/ShortcutIcon.stories.jsx index 761a694c..b3e50728 100644 --- a/src/components/ShortcutIcon.stories.jsx +++ b/src/components/ShortcutIcon.stories.jsx @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import React from 'react' import ShortcutIcon from './ShortcutIcon' @@ -19,6 +20,18 @@ const Template = (args) => export const basic = Template.bind({}) basic.args = { + id: 'abcd', text: 'Google Googledy', url: 'https://www.google.com', + onEdit: () => console.log('onEdit'), + onDelete: () => console.log('onDelete'), +} + +export const withoutHttps = Template.bind({}) +withoutHttps.args = { + id: 'abcd', + text: 'Google Googledy', + url: 'www.google.com', + onEdit: () => console.log('onEdit'), + onDelete: () => console.log('onDelete'), } diff --git a/src/components/TabCMPHeadElements.js b/src/components/TabCMPHeadElements.js index c5170a40..00a25a1f 100644 --- a/src/components/TabCMPHeadElements.js +++ b/src/components/TabCMPHeadElements.js @@ -6,10 +6,71 @@ import React from 'react' // https://github.com/gladly-team/tab-cmp/blob/master/src/tagModified.html const TabCMPHeadElements = () => ( <> + + + + + + + +