diff --git a/packages/app-project/package.json b/packages/app-project/package.json
index edbb8adcf0..3e6510cab4 100644
--- a/packages/app-project/package.json
+++ b/packages/app-project/package.json
@@ -15,6 +15,7 @@
"test:ci": "BABEL_ENV=test mocha --reporter=min"
},
"dependencies": {
+ "@artsy/fresnel": "~1.0.7",
"@babel/plugin-proposal-decorators": "~7.4.4",
"@babel/plugin-proposal-optional-chaining": "~7.2.0",
"@sentry/browser": "^5.4.3",
@@ -42,9 +43,10 @@
"next": "~9.0.3",
"panoptes-client": "~2.12.0",
"path-match": "~1.2.4",
- "react": "~16.8.4",
- "react-dom": "~16.8.4",
+ "react": "~16.8.6",
+ "react-dom": "~16.8.6",
"styled-components": "~4.1.3",
+ "svg-loaders-react": "~2.0.1",
"url-parse": "~1.4.7",
"validator": "~11.0.0"
},
diff --git a/packages/app-project/pages/_app.js b/packages/app-project/pages/_app.js
index 127095f3ef..4acad70f0e 100644
--- a/packages/app-project/pages/_app.js
+++ b/packages/app-project/pages/_app.js
@@ -15,6 +15,7 @@ import Head from '../src/components/Head'
import ProjectHeader from '../src/components/ProjectHeader'
import ZooHeaderWrapper from '../src/components/ZooHeaderWrapper'
import { initializeLogger, logReactError } from '../src/helpers/logger'
+import { MediaContextProvider } from '../src/shared/components/Media'
import initStore from '../stores'
const GlobalStyle = createGlobalStyle`
@@ -92,19 +93,21 @@ export default class MyApp extends App {
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
)
diff --git a/packages/app-project/pages/_document.js b/packages/app-project/pages/_document.js
index a2a0e1fbf2..9f274bccad 100644
--- a/packages/app-project/pages/_document.js
+++ b/packages/app-project/pages/_document.js
@@ -2,6 +2,7 @@ import Document, { Head, Main, NextScript } from 'next/document'
import React from 'react'
import { ServerStyleSheet } from 'styled-components'
+import { mediaStyle } from '../src/shared/components/Media'
import { logNodeError } from '../src/helpers/logger'
const GA_TRACKING_ID = 'GTM-WDW6V4'
@@ -40,6 +41,7 @@ export default class MyDocument extends Document {
return (
+
{isProduction && (
)}
diff --git a/packages/app-project/src/helpers/GrommetWrapper/GrommetWrapperContainer.js b/packages/app-project/src/helpers/GrommetWrapper/GrommetWrapperContainer.js
index f2d8ba8381..46f259bb43 100644
--- a/packages/app-project/src/helpers/GrommetWrapper/GrommetWrapperContainer.js
+++ b/packages/app-project/src/helpers/GrommetWrapper/GrommetWrapperContainer.js
@@ -1,10 +1,11 @@
-import zooTheme from '@zooniverse/grommet-theme'
-import { Grommet, base as baseTheme } from 'grommet'
+import { Grommet } from 'grommet'
import merge from 'lodash/merge'
import { inject, observer } from 'mobx-react'
import { node, object, oneOf } from 'prop-types'
import React, { Component } from 'react'
+import theme from '../theme'
+
function storeMapper (stores) {
const { mode } = stores.store.ui
return {
@@ -17,7 +18,7 @@ function storeMapper (stores) {
class GrommetWrapperContainer extends Component {
mergeThemes () {
const { mode, theme } = this.props
- return merge({}, baseTheme, theme, { dark: mode === 'dark' })
+ return merge({}, theme, { dark: mode === 'dark' })
}
render () {
@@ -40,7 +41,7 @@ GrommetWrapperContainer.propTypes = {
GrommetWrapperContainer.defaultProps = {
mode: 'light',
- theme: zooTheme
+ theme
}
export default GrommetWrapperContainer
diff --git a/packages/app-project/src/helpers/GrommetWrapper/README.md b/packages/app-project/src/helpers/GrommetWrapper/README.md
index 7bb37cea33..7801aaf189 100644
--- a/packages/app-project/src/helpers/GrommetWrapper/README.md
+++ b/packages/app-project/src/helpers/GrommetWrapper/README.md
@@ -3,6 +3,6 @@
This component handles a few things related to the Grommet theme:
- Creating a Grommet context
-- Merging the Zooniverse Grommet theme with the Grommet base theme
- Observing the UI store, fetching the `mode` property, and using that to set the `dark` boolean property on the theme
- Passing the theme into the Grommet context
+- Setting a default screen size
diff --git a/packages/app-project/src/helpers/theme/README.md b/packages/app-project/src/helpers/theme/README.md
new file mode 100644
index 0000000000..ab4e92bef4
--- /dev/null
+++ b/packages/app-project/src/helpers/theme/README.md
@@ -0,0 +1,5 @@
+# Theme
+
+Creates a Grommet theme by performing a deep merge of the Grommet base theme with the Zooniverse custom Grommet theme.
+
+This was originally done in the `GrommetWrapper` theme, but was moved here so it can be reused with `@artsy/fresnel` to extract the breakpoints for responsive sizing.
diff --git a/packages/app-project/src/helpers/theme/index.js b/packages/app-project/src/helpers/theme/index.js
new file mode 100644
index 0000000000..348f44fd04
--- /dev/null
+++ b/packages/app-project/src/helpers/theme/index.js
@@ -0,0 +1 @@
+export { default } from './theme'
diff --git a/packages/app-project/src/helpers/theme/theme.js b/packages/app-project/src/helpers/theme/theme.js
new file mode 100644
index 0000000000..27b1939c0f
--- /dev/null
+++ b/packages/app-project/src/helpers/theme/theme.js
@@ -0,0 +1,7 @@
+import zooTheme from '@zooniverse/grommet-theme'
+import { base as baseTheme } from 'grommet'
+import merge from 'lodash/merge'
+
+const theme = merge({}, baseTheme, zooTheme)
+
+export default theme
diff --git a/packages/app-project/src/screens/ProjectHomePage/ProjectHomePage.js b/packages/app-project/src/screens/ProjectHomePage/ProjectHomePage.js
index f01f300250..77a02a350a 100644
--- a/packages/app-project/src/screens/ProjectHomePage/ProjectHomePage.js
+++ b/packages/app-project/src/screens/ProjectHomePage/ProjectHomePage.js
@@ -2,6 +2,7 @@ import { Grid } from 'grommet'
import React from 'react'
import { withResponsiveContext } from '@zooniverse/react-components'
+import Hero from './components/Hero'
import MessageFromResearcher from './components/MessageFromResearcher'
import AboutProject from '../../shared/components/AboutProject'
import ConnectWithProject from '../../shared/components/ConnectWithProject'
@@ -13,22 +14,25 @@ function ProjectHomePage (props) {
const { screenSize } = props
const responsiveColumns = (screenSize === 'small') ? ['auto'] : ['auto', '1em']
return (
-
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+ >
)
}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/Hero.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/Hero.js
new file mode 100644
index 0000000000..44f383f95f
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/Hero.js
@@ -0,0 +1,67 @@
+import { withResponsiveContext } from '@zooniverse/react-components'
+import { Box, Grid } from 'grommet'
+import React from 'react'
+import styled from 'styled-components'
+
+import Background from './components/Background'
+import Introduction from './components/Introduction'
+import WorkflowSelector from './components/WorkflowSelector'
+import ContentBox from '../../../../shared/components/ContentBox'
+import { Media } from '../../../../shared/components/Media'
+
+const StyledContentBox = styled(ContentBox)`
+ ${props => (props.screenSize !== 'small') && `
+ border-color: transparent;
+ max-height: 763px;
+ `}
+`
+
+function Hero (props) {
+ const { screenSize, workflows } = props
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+const DecoratedHero = withResponsiveContext(Hero)
+
+export {
+ DecoratedHero as default,
+ Hero
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/Hero.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/Hero.spec.js
new file mode 100644
index 0000000000..735c85c414
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/Hero.spec.js
@@ -0,0 +1,68 @@
+import { shallow } from 'enzyme'
+import React from 'react'
+
+import { Hero } from './Hero'
+import Background from './components/Background'
+import Introduction from './components/Introduction'
+import WorkflowSelector from './components/WorkflowSelector'
+import { Media } from '../../../../shared/components/Media'
+
+describe('Component > Hero', function () {
+ let wrapper
+
+ before(function () {
+ wrapper = shallow()
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+
+ describe('behaviour on small screens', function () {
+ let mediaWrapper
+
+ before(function () {
+ mediaWrapper = wrapper.find(Media).find({ at: 'default' })
+ })
+
+ it('should have a layout for small screens', function () {
+ expect(mediaWrapper).to.have.lengthOf(1)
+ })
+
+ it('should render the `Background` component', function () {
+ expect(mediaWrapper.find(Background)).to.have.lengthOf(1)
+ })
+
+ it('should render the `Introduction` component', function () {
+ expect(mediaWrapper.find(Introduction)).to.have.lengthOf(1)
+ })
+
+ it('should render the `WorkflowSelector` component', function () {
+ expect(mediaWrapper.find(WorkflowSelector)).to.have.lengthOf(1)
+ })
+ })
+
+ describe('behaviour on larger screens', function () {
+ let mediaWrapper
+
+ before(function () {
+ mediaWrapper = wrapper.find(Media).find({ greaterThan: 'default' })
+ })
+
+ it('should have a layout for larger screens', function () {
+ expect(mediaWrapper).to.have.lengthOf(1)
+ })
+
+ it('should render the `Background` component', function () {
+ expect(mediaWrapper.find(Background)).to.have.lengthOf(1)
+ })
+
+ it('should render the `Introduction` component', function () {
+ expect(mediaWrapper.find(Introduction)).to.have.lengthOf(1)
+ })
+
+ it('should render the `WorkflowSelector` component', function () {
+ expect(mediaWrapper.find(WorkflowSelector)).to.have.lengthOf(1)
+ })
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/HeroContainer.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/HeroContainer.js
new file mode 100644
index 0000000000..4f3c844d08
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/HeroContainer.js
@@ -0,0 +1,87 @@
+import { inject, observer } from 'mobx-react'
+import { arrayOf, string } from 'prop-types'
+import React, { Component } from 'react'
+import asyncStates from '@zooniverse/async-states'
+
+import Hero from './Hero'
+import fetchWorkflowsHelper from './helpers/fetchWorkflowsHelper'
+
+function storeMapper (stores) {
+ return {
+ activeWorkflows: stores.store.project.links['active_workflows'],
+ defaultWorkflow: stores.store.project.configuration['default_workflow']
+ }
+}
+
+class HeroContainer extends Component {
+ constructor () {
+ super()
+ this.state = {
+ workflows: {
+ loading: asyncStates.initialized,
+ data: []
+ }
+ }
+ }
+
+ componentDidMount () {
+ return this.fetchWorkflows()
+ }
+
+ async fetchWorkflows () {
+ this.setState(state => ({
+ workflows: {
+ ...state.workflows,
+ loading: asyncStates.loading
+ }
+ }))
+ try {
+ const { activeWorkflows, defaultWorkflow, language } = this.props
+ const workflows = await fetchWorkflowsHelper(language, activeWorkflows, defaultWorkflow)
+ this.setState({
+ workflows: {
+ loading: asyncStates.success,
+ data: workflows
+ }
+ })
+ } catch (error) {
+ if (process.browser) {
+ console.error(error)
+ }
+ this.setState(state => ({
+ workflows: {
+ ...state.workflows,
+ loading: asyncStates.error
+ }
+ }))
+ }
+ }
+
+ render () {
+ const { workflows } = this.state
+ return (
+
+ )
+ }
+}
+
+HeroContainer.propTypes = {
+ activeWorkflows: arrayOf(string),
+ defaultWorkflow: string,
+ language: string
+}
+
+HeroContainer.defaultProps = {
+ activeWorkflows: [],
+ defaultWorkflow: '',
+ language: 'en'
+}
+
+@inject(storeMapper)
+@observer
+class DecoratedHeroContainer extends HeroContainer { }
+
+export {
+ DecoratedHeroContainer as default,
+ HeroContainer
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/HeroContainer.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/HeroContainer.spec.js
new file mode 100644
index 0000000000..ae9a1c3e85
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/HeroContainer.spec.js
@@ -0,0 +1,166 @@
+import { shallow } from 'enzyme'
+import nock from 'nock'
+import sinon from 'sinon'
+
+import { HeroContainer } from './HeroContainer'
+import Hero from './Hero'
+
+const WORKFLOWS = [
+ {
+ id: '1',
+ completeness: 0.4
+ }
+]
+
+// `translated_id` is a number because of a bug in the translations API :(
+const TRANSLATIONS = [
+ {
+ translated_id: 1,
+ strings: {
+ display_name: 'Foo'
+ }
+ }
+]
+
+describe('Component > HeroContainer', function () {
+ describe('general behaviour', function () {
+ let scope
+ let wrapper
+ let componentWrapper
+
+ before(function () {
+ scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .reply(200, {
+ translations: TRANSLATIONS
+ })
+ .get('/workflows')
+ .query(true)
+ .reply(200, {
+ workflows: WORKFLOWS
+ })
+ wrapper = shallow()
+ componentWrapper = wrapper.find(Hero)
+ })
+
+ after(function () {
+ nock.cleanAll()
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+
+ it('should render the `Hero` component', function () {
+ expect(componentWrapper).to.have.lengthOf(1)
+ })
+ })
+
+ describe('loading state', function () {
+ let scope
+ let wrapper
+ let componentWrapper
+
+ before(function () {
+ scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .reply(200, {
+ translations: TRANSLATIONS
+ })
+ .get('/workflows')
+ .query(true)
+ .reply(200, {
+ workflows: WORKFLOWS
+ })
+ })
+
+ after(function () {
+ nock.cleanAll()
+ })
+
+ it('should pass down the correct props', function () {
+ wrapper = shallow()
+ componentWrapper = wrapper.find(Hero)
+ expect(componentWrapper.prop('workflows')).to.deep.equal({
+ loading: 'loading',
+ data: []
+ })
+ })
+ })
+
+ describe('success state', function () {
+ let scope
+ let wrapper
+ let componentWrapper
+ let fetchWorkflowsSpy
+
+ before(function () {
+ scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .reply(200, {
+ translations: TRANSLATIONS
+ })
+ .get('/workflows')
+ .query(true)
+ .reply(200, {
+ workflows: WORKFLOWS
+ })
+ fetchWorkflowsSpy = sinon.spy(HeroContainer.prototype, 'fetchWorkflows')
+ })
+
+ after(function () {
+ nock.cleanAll()
+ fetchWorkflowsSpy.restore()
+ })
+
+ it('should pass down the correct props', async function () {
+ wrapper = shallow()
+ await fetchWorkflowsSpy.returnValues[0]
+ componentWrapper = wrapper.find(Hero)
+ expect(componentWrapper.prop('workflows')).to.deep.equal({
+ loading: 'success',
+ data: [
+ { completeness: 0.4, default: true, id: '1', displayName: 'Foo' }
+ ]
+ })
+ })
+ })
+
+ describe('error state', function () {
+ let scope
+ let wrapper
+ let componentWrapper
+ let fetchWorkflowsSpy
+
+ before(function () {
+ scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .reply(200, {
+ translations: TRANSLATIONS
+ })
+ .get('/workflows')
+ .query(true)
+ .replyWithError('something awful happened')
+ fetchWorkflowsSpy = sinon.spy(HeroContainer.prototype, 'fetchWorkflows')
+ })
+
+ after(function () {
+ nock.cleanAll()
+ fetchWorkflowsSpy.restore()
+ })
+
+ it('should pass down the correct props', async function () {
+ wrapper = shallow()
+ await fetchWorkflowsSpy.returnValues[0]
+ componentWrapper = wrapper.find(Hero)
+ expect(componentWrapper.prop('workflows')).to.deep.equal({
+ loading: 'error',
+ data: []
+ })
+ })
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/README.md b/packages/app-project/src/screens/ProjectHomePage/components/Hero/README.md
new file mode 100644
index 0000000000..8a4260371f
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/README.md
@@ -0,0 +1,8 @@
+# Hero
+
+This is the main hero component at the top of the project home / landing page.
+
+## Notes
+
+- The workflows to be shown are fetched in ``, as the component that actually lists them can be unmounted depending on screen size.
+- The component defaults to a narrow view.
\ No newline at end of file
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/Background.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/Background.js
new file mode 100644
index 0000000000..ec37103e73
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/Background.js
@@ -0,0 +1,39 @@
+import { withResponsiveContext } from '@zooniverse/react-components'
+import { string } from 'prop-types'
+import React from 'react'
+import styled from 'styled-components'
+
+const Img = styled.img`
+ height: 100%;
+ object-fit: cover;
+ object-position: 0 50%;
+ width: 100%;
+
+ ${props => props.screenSize === 'small' && `
+ height: auto;
+ min-height: inherit;
+ object-fit: contain;
+ width: 100%;
+ `}
+`
+
+function Background (props) {
+ const { className, backgroundSrc, screenSize } = props
+ return (
+
+ )
+}
+
+Background.propTypes = {
+ backgroundSrc: string
+}
+
+export default withResponsiveContext(Background)
+export {
+ Background
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/Background.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/Background.spec.js
new file mode 100644
index 0000000000..6f0fdeefee
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/Background.spec.js
@@ -0,0 +1,22 @@
+import { render } from 'enzyme'
+import React from 'react'
+
+import { Background } from './Background'
+
+const BACKGROUND_SRC = '/foo/bar/baz.jpg'
+
+describe('Component > Background', function () {
+ let wrapper
+ before(function () {
+ wrapper = render()
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+
+ it('should render an `img` with the correct src', function () {
+ expect(wrapper[0].name).to.equal('img')
+ expect(wrapper.attr('src')).to.equal(BACKGROUND_SRC)
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/BackgroundContainer.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/BackgroundContainer.js
new file mode 100644
index 0000000000..2298aaa370
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/BackgroundContainer.js
@@ -0,0 +1,32 @@
+import { inject, observer } from 'mobx-react'
+import { string } from 'prop-types'
+import React, { Component } from 'react'
+
+import Background from './Background'
+
+function storeMapper (stores) {
+ return {
+ backgroundSrc: stores.store.project.background.src
+ }
+}
+
+class BackgroundContainer extends Component {
+ render () {
+ return (
+
+ )
+ }
+}
+
+BackgroundContainer.propTypes = {
+ backgroundSrc: string.isRequired
+}
+
+@inject(storeMapper)
+@observer
+class DecoratedBackgroundContainer extends BackgroundContainer { }
+
+export {
+ DecoratedBackgroundContainer as default,
+ BackgroundContainer
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/BackgroundContainer.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/BackgroundContainer.spec.js
new file mode 100644
index 0000000000..5f3802b918
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/BackgroundContainer.spec.js
@@ -0,0 +1,29 @@
+import { shallow } from 'enzyme'
+import React from 'react'
+
+import { BackgroundContainer } from './BackgroundContainer'
+import Background from './Background'
+
+const BACKGROUND_SRC = '/foo/bar/baz.jpg'
+
+let wrapper
+let componentWrapper
+
+describe('Component > BackgroundContainer', function () {
+ before(function () {
+ wrapper = shallow()
+ componentWrapper = wrapper.find(Background)
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+
+ it('should render the `Background` component', function () {
+ expect(componentWrapper).to.have.lengthOf(1)
+ })
+
+ it('should pass down the `backgroundSrc` prop', function () {
+ expect(componentWrapper.prop('backgroundSrc')).to.equal(BACKGROUND_SRC)
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/index.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/index.js
new file mode 100644
index 0000000000..837131eab6
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Background/index.js
@@ -0,0 +1 @@
+export { default } from './BackgroundContainer'
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/Introduction.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/Introduction.js
new file mode 100644
index 0000000000..65906e3f1c
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/Introduction.js
@@ -0,0 +1,55 @@
+import { SpacedText } from '@zooniverse/react-components'
+import counterpart from 'counterpart'
+import { Anchor, Box, Paragraph } from 'grommet'
+import { Next } from 'grommet-icons'
+import Link from 'next/link'
+import { object, string } from 'prop-types'
+import React from 'react'
+import styled from 'styled-components'
+
+import en from './locales/en'
+
+counterpart.registerTranslations('en', en)
+
+// Selecting the div here to resize the gap between label and icon
+const StyledAnchor = styled(Anchor)`
+ & div {
+ width: 10px
+ }
+`
+
+const StyledParagraph = styled(Paragraph)`
+ font-weight: bold;
+`
+
+function Introduction (props) {
+ const { description, link, title } = props
+ return (
+
+
+
+ {title}
+
+
+
+ {description}
+
+
+ }
+ label={{counterpart('Introduction.link')}}
+ reverse
+ />
+
+
+ )
+}
+
+Introduction.propTypes = {
+ description: string.isRequired,
+ link: object.isRequired,
+ title: string.isRequired
+}
+
+export default Introduction
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/Introduction.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/Introduction.spec.js
new file mode 100644
index 0000000000..07fccef92a
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/Introduction.spec.js
@@ -0,0 +1,38 @@
+import { render } from 'enzyme'
+import React from 'react'
+
+import Introduction from './Introduction'
+
+let wrapper
+
+const DESCRIPTION = 'Project Title!'
+const LINK = {
+ href: '/projects/foo/bar/about'
+}
+const TITLE = 'baz'
+
+describe('Component > Introduction', function () {
+ before(function () {
+ wrapper = render()
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+
+ it('should render the title', function () {
+ expect(wrapper.text().includes(TITLE)).to.be.true()
+ })
+
+ it('should render the description', function () {
+ expect(wrapper.text().includes(DESCRIPTION)).to.be.true()
+ })
+
+ it('should render a link to the about page', function () {
+ expect(wrapper.find(`a[href="${LINK.href}"]`)).to.have.lengthOf(1)
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/IntroductionContainer.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/IntroductionContainer.js
new file mode 100644
index 0000000000..549284d2d5
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/IntroductionContainer.js
@@ -0,0 +1,53 @@
+import { inject, observer } from 'mobx-react'
+import { withRouter } from 'next/router'
+import { shape, string } from 'prop-types'
+import React, { Component } from 'react'
+
+import Introduction from './Introduction'
+
+function storeMapper (stores) {
+ const { project } = stores.store
+ return {
+ description: project.description,
+ title: project.display_name
+ }
+}
+
+class IntroductionContainer extends Component {
+ getLink () {
+ const { owner, project } = this.props.router.query
+ return {
+ as: `/projects/${owner}/${project}/about`,
+ href: '/about'
+ }
+ }
+
+ render () {
+ const { description, title } = this.props
+ const link = this.getLink()
+ return (
+
+ )
+ }
+}
+
+IntroductionContainer.propTypes = {
+ description: string.isRequired,
+ router: shape({
+ query: shape({
+ owner: string.isRequired,
+ project: string.isRequired
+ }).isRequired
+ }).isRequired,
+ title: string.isRequired
+}
+
+@inject(storeMapper)
+@withRouter
+@observer
+class DecoratedIntroductionContainer extends IntroductionContainer {}
+
+export {
+ DecoratedIntroductionContainer as default,
+ IntroductionContainer
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/IntroductionContainer.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/IntroductionContainer.spec.js
new file mode 100644
index 0000000000..a804ec6f44
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/IntroductionContainer.spec.js
@@ -0,0 +1,30 @@
+import { shallow } from 'enzyme'
+import React from 'react'
+
+import { IntroductionContainer } from './IntroductionContainer'
+import Introduction from './Introduction'
+
+let wrapper
+let componentWrapper
+
+const ROUTER = {
+ query: {
+ owner: 'foo',
+ project: 'bar'
+ }
+}
+
+describe('Component > IntroductionContainer', function () {
+ before(function () {
+ wrapper = shallow()
+ componentWrapper = wrapper.find(Introduction)
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+
+ it('should render the `Introduction` component', function () {
+ expect(componentWrapper).to.have.lengthOf(1)
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/README.md b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/README.md
new file mode 100644
index 0000000000..04a1a29d10
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/README.md
@@ -0,0 +1,3 @@
+# Introduction
+
+The top section of the Hero component with the project title, description and a link to the about page.
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/index.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/index.js
new file mode 100644
index 0000000000..3ac2fc5790
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/index.js
@@ -0,0 +1 @@
+export { default } from './IntroductionContainer'
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/locales/en.json b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/locales/en.json
new file mode 100644
index 0000000000..bae1b9b092
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/Introduction/locales/en.json
@@ -0,0 +1,5 @@
+{
+ "Introduction": {
+ "link": "Learn more"
+ }
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/WorkflowSelector.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/WorkflowSelector.js
new file mode 100644
index 0000000000..f012b1805d
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/WorkflowSelector.js
@@ -0,0 +1,70 @@
+import asyncStates from '@zooniverse/async-states'
+import { SpacedText } from '@zooniverse/react-components'
+import counterpart from 'counterpart'
+import { Box, Text } from 'grommet'
+import { arrayOf, shape, string } from 'prop-types'
+import React from 'react'
+import { withTheme } from 'styled-components'
+import { Bars } from 'svg-loaders-react'
+
+import WorkflowSelectButton from './components/WorkflowSelectButton'
+import en from './locales/en'
+
+counterpart.registerTranslations('en', en)
+
+function WorkflowSelector (props) {
+ const { workflows } = props
+ const loaderColor = props.theme.global.colors.brand
+
+ return (
+
+
+ {counterpart('WorkflowSelector.classify')}
+
+
+ {counterpart('WorkflowSelector.message')}
+
+
+ {(workflows.loading === asyncStates.error) && (
+
+ {counterpart('WorkflowSelector.error')}
+
+ )}
+
+ {(workflows.loading === asyncStates.success) && (
+
+ {workflows.data.map(workflow =>
+
+ )}
+
+ )}
+
+ {(!asyncStates.values.includes(workflows.loading)) && (
+
+
+
+
+
+ )}
+
+ )
+}
+
+WorkflowSelector.propTypes = {
+ workflows: shape({
+ data: arrayOf(shape({
+ id: string.isRequired
+ }).isRequired).isRequired
+ }).isRequired
+}
+
+export default withTheme(WorkflowSelector)
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/WorkflowSelector.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/WorkflowSelector.spec.js
new file mode 100644
index 0000000000..4d7168231a
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/WorkflowSelector.spec.js
@@ -0,0 +1,16 @@
+import { shallow } from 'enzyme'
+import React from 'react'
+
+import WorkflowSelector from './WorkflowSelector'
+
+let wrapper
+
+describe('Component > WorkflowSelector', function () {
+ before(function () {
+ wrapper = shallow()
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/WorkflowSelectButton.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/WorkflowSelectButton.js
new file mode 100644
index 0000000000..ad1316a129
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/WorkflowSelectButton.js
@@ -0,0 +1,42 @@
+import { withThemeContext } from '@zooniverse/react-components'
+import { Button } from 'grommet'
+import Link from 'next/link'
+import { withRouter } from 'next/router'
+import { bool, shape, string } from 'prop-types'
+import React from 'react'
+
+import theme from './theme'
+
+function WorkflowSelectButton (props) {
+ const { router, workflow } = props
+
+ const as = workflow.default
+ ? `${router.asPath}/classify`
+ : `${router.asPath}/classify/workflow/${workflow.id}`
+
+ const href = '/Classify'
+
+ return (
+
+
+
+ )
+}
+
+WorkflowSelectButton.propTypes = {
+ router: shape({
+ asPath: string.isRequired
+ }),
+ workflow: shape({
+ default: bool.isRequired,
+ displayName: string.isRequired,
+ id: string.isRequired
+ }).isRequired
+}
+
+const DecoratedWorkflowSelectButton = withRouter(withThemeContext(WorkflowSelectButton, theme))
+
+export {
+ DecoratedWorkflowSelectButton as default,
+ WorkflowSelectButton
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/WorkflowSelectButton.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/WorkflowSelectButton.spec.js
new file mode 100644
index 0000000000..70acab5879
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/WorkflowSelectButton.spec.js
@@ -0,0 +1,45 @@
+import { render } from 'enzyme'
+import React from 'react'
+
+import { WorkflowSelectButton } from './WorkflowSelectButton'
+
+const WORKFLOW = {
+ default: false,
+ displayName: 'Workflow name',
+ id: '1'
+}
+
+const ROUTER = {
+ asPath: '/projects/foo/bar'
+}
+
+describe('Component > WorkflowSelectButton', function () {
+ let wrapper
+
+ before(function () {
+ wrapper = render()
+ console.info(wrapper.html())
+ })
+
+ it('should render without crashing', function () {
+ expect(wrapper).to.be.ok()
+ })
+
+ describe('when used with a default workflow', function () {
+ it('should be a link pointing to `/classify`', function () {
+ const wrapper = render()
+ expect(wrapper[0].name).to.equal('a')
+ expect(wrapper.prop('href')).to.equal(`${ROUTER.asPath}/classify`)
+ })
+ })
+
+ describe('when used with a non-default workflow', function () {
+ it('should be a link pointing to `/classify/workflow/:workflow_id`', function () {
+ expect(wrapper[0].name).to.equal('a')
+ expect(wrapper.prop('href')).to.equal(`${ROUTER.asPath}/classify/workflow/${WORKFLOW.id}`)
+ })
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/index.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/index.js
new file mode 100644
index 0000000000..637e38eaf3
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/index.js
@@ -0,0 +1 @@
+export { default } from './WorkflowSelectButton'
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/locales/en.json b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/locales/en.json
new file mode 100644
index 0000000000..78af8c21c5
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/locales/en.json
@@ -0,0 +1,4 @@
+{
+ "WorkflowSelectButton": {
+ }
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/theme.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/theme.js
new file mode 100644
index 0000000000..89c8578376
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/components/WorkflowSelectButton/theme.js
@@ -0,0 +1,17 @@
+const theme = {
+ button: {
+ border: {
+ width: '0'
+ },
+ color: 'black',
+ extend: props => `
+ text-align: center;
+ background: ${props.theme.global.colors['neutral-4']};
+ &:hover {
+ box-shadow: none;
+ }
+ `
+ }
+}
+
+export default theme
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/index.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/index.js
new file mode 100644
index 0000000000..efbdb9c874
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/index.js
@@ -0,0 +1 @@
+export { default } from './WorkflowSelector'
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/locales/en.json b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/locales/en.json
new file mode 100644
index 0000000000..94804439c5
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/components/WorkflowSelector/locales/en.json
@@ -0,0 +1,7 @@
+{
+ "WorkflowSelector": {
+ "classify": "Classify",
+ "error": "There was an error fetching the workflows :(",
+ "message": "Anyone can do real research!"
+ }
+}
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/README.md b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/README.md
new file mode 100644
index 0000000000..afc1a62f5e
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/README.md
@@ -0,0 +1,21 @@
+# fetchWorkflowsHelper
+
+An async helper method to fetch the data required to generate the workflow select buttons for the Hero component. Grabs the workflow and translation data for a given array of workflow IDs, and returns a collection of workflow objects.
+
+## Arguments
+
+- `language` - A two-letter language code to fetch the correct translation data from Panoptes. Defaults to `en`
+- `activeWorkflows` (required) - an array of string IDs used to fetch the required workflows from Panoptes
+- `defaultWorkflow` - the ID of the default workflow, which will be shown at `/projects/:project/:owner:/classify`
+
+## Example
+
+```js
+const workflows = await fetchWorkflowsHelper('en', ['1', '2'], '2')
+
+// returns
+// [
+// { completeness: 0.4, default: false, id: '1', displayName: 'Foo' },
+// { completeness: 0.7, default: true, id: '2', displayName: 'Bar' }
+// ]
+```
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.js
new file mode 100644
index 0000000000..827c1d8aae
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.js
@@ -0,0 +1,59 @@
+import { panoptes } from '@zooniverse/panoptes-js'
+
+function fetchWorkflowData (activeWorkflows) {
+ return panoptes
+ .get('/workflows', {
+ complete: false,
+ fields: 'completeness',
+ id: activeWorkflows.join(',')
+ })
+ .then(response => response.body.workflows)
+ .then(workflows => workflows.map(pickWorkflowFields))
+}
+
+function fetchDisplayNames (language, activeWorkflows) {
+ return panoptes
+ .get('/translations', {
+ fields: 'strings,translated_id',
+ language,
+ 'translated_id': activeWorkflows.join(','),
+ 'translated_type': 'workflow'
+ })
+ .then(response => response.body.translations)
+ .then(createDisplayNamesMap)
+}
+
+async function fetchWorkflowsHelper (language = 'en', activeWorkflows, defaultWorkflow) {
+ const workflows = await fetchWorkflowData(activeWorkflows)
+ const workflowIds = workflows.map(workflow => workflow.id)
+ const displayNames = await fetchDisplayNames(language, workflowIds)
+
+ return workflows.map(workflow => {
+ const isDefault = workflows.length === 1 || workflow.id === defaultWorkflow
+
+ return {
+ completeness: workflow.completeness || 0,
+ default: isDefault,
+ id: workflow.id,
+ displayName: displayNames[workflow.id]
+ }
+ })
+}
+
+function pickWorkflowFields (workflow) {
+ return {
+ completeness: workflow.completeness,
+ id: workflow.id
+ }
+}
+
+function createDisplayNamesMap (translations) {
+ const map = {}
+ translations.forEach(translation => {
+ const workflowId = translation.translated_id.toString()
+ map[workflowId] = translation.strings['display_name']
+ })
+ return map
+}
+
+export default fetchWorkflowsHelper
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.spec.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.spec.js
new file mode 100644
index 0000000000..737561cb33
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/fetchWorkflowsHelper.spec.js
@@ -0,0 +1,120 @@
+import nock from 'nock'
+
+import fetchWorkflowsHelper from './fetchWorkflowsHelper'
+
+const WORKFLOWS = [
+ {
+ id: '1',
+ completeness: 0.4
+ },
+ {
+ id: '2',
+ completeness: 0.7
+ }
+]
+
+// `translated_id` is a number because of a bug in the translations API :(
+const TRANSLATIONS = [
+ {
+ translated_id: 1,
+ strings: {
+ display_name: 'Foo'
+ }
+ },
+ {
+ translated_id: 2,
+ strings: {
+ display_name: 'Bar'
+ }
+ }
+]
+
+describe('Helpers > fetchWorkflowsHelper', function () {
+ it('should provide the expected result with a single workflow', async function () {
+ const scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .reply(200, {
+ translations: TRANSLATIONS.slice(0, 1)
+ })
+ .get('/workflows')
+ .query(true)
+ .reply(200, {
+ workflows: WORKFLOWS.slice(0, 1)
+ })
+
+ const result = await fetchWorkflowsHelper('en', ['1'])
+ expect(result).to.deep.equal([
+ { completeness: 0.4, default: true, id: '1', displayName: 'Foo' }
+ ])
+ })
+
+ it('should provide the expected result with multiple workflows', async function () {
+ const scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .reply(200, {
+ translations: TRANSLATIONS
+ })
+ .get('/workflows')
+ .query(true)
+ .reply(200, {
+ workflows: WORKFLOWS
+ })
+
+ const result = await fetchWorkflowsHelper('en', ['1', '2'])
+ expect(result).to.deep.equal([
+ { completeness: 0.4, default: false, id: '1', displayName: 'Foo' },
+ { completeness: 0.7, default: false, id: '2', displayName: 'Bar' }
+ ])
+ })
+
+ describe('when there is a `defaultWorkflow` provided', function () {
+ it('should provide the expected result with multiple workflows', async function () {
+ const scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .reply(200, {
+ translations: TRANSLATIONS
+ })
+ .get('/workflows')
+ .query(true)
+ .reply(200, {
+ workflows: WORKFLOWS
+ })
+
+ const result = await fetchWorkflowsHelper('en', ['1', '2'], '2')
+ expect(result).to.deep.equal([
+ { completeness: 0.4, default: false, id: '1', displayName: 'Foo' },
+ { completeness: 0.7, default: true, id: '2', displayName: 'Bar' }
+ ])
+ })
+ })
+
+ describe(`when there's an error`, function () {
+ it('should allow the error to be thrown for the consumer to handle', async function () {
+ const error = {
+ message: 'oh dear. oh dear god'
+ }
+ const scope = nock('https://panoptes-staging.zooniverse.org/api')
+ .get('/translations')
+ .query(true)
+ .replyWithError(error)
+ .get('/workflows')
+ .query(true)
+ .reply(200, {
+ workflows: WORKFLOWS
+ })
+
+ try {
+ await fetchWorkflowsHelper('en', ['1', '2'], '2')
+ expect.fail()
+ } catch (error) {
+ expect(error).to.deep.equal({
+ ...error,
+ response: undefined
+ })
+ }
+ })
+ })
+})
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/index.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/index.js
new file mode 100644
index 0000000000..d678e5c8cd
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/helpers/fetchWorkflowsHelper/index.js
@@ -0,0 +1 @@
+export { default } from './fetchWorkflowsHelper'
diff --git a/packages/app-project/src/screens/ProjectHomePage/components/Hero/index.js b/packages/app-project/src/screens/ProjectHomePage/components/Hero/index.js
new file mode 100644
index 0000000000..e99e7dd918
--- /dev/null
+++ b/packages/app-project/src/screens/ProjectHomePage/components/Hero/index.js
@@ -0,0 +1 @@
+export { default } from './HeroContainer'
diff --git a/packages/app-project/src/shared/components/Media/Media.js b/packages/app-project/src/shared/components/Media/Media.js
new file mode 100644
index 0000000000..42196fea84
--- /dev/null
+++ b/packages/app-project/src/shared/components/Media/Media.js
@@ -0,0 +1,17 @@
+import { createMedia } from '@artsy/fresnel'
+import reduce from 'lodash/reduce'
+
+import theme from '../../../helpers/theme'
+
+const breakpoints = reduce(theme.global.breakpoints, (acc, breakpoint, size) => {
+ if (breakpoint.value) {
+ acc[size] = breakpoint.value
+ }
+ return acc
+}, { default: 0 })
+
+const AppMedia = createMedia({ breakpoints })
+
+// Generate CSS to be injected into the head
+export const mediaStyle = AppMedia.createMediaStyle()
+export const { Media, MediaContextProvider } = AppMedia
diff --git a/packages/app-project/src/shared/components/Media/README.md b/packages/app-project/src/shared/components/Media/README.md
new file mode 100644
index 0000000000..70b1739848
--- /dev/null
+++ b/packages/app-project/src/shared/components/Media/README.md
@@ -0,0 +1,3 @@
+# Media
+
+Components created by [`@artsy/fresnel`](https://github.com/artsy/fresnel) for responsive SSR. Breakpoints are determined by those defined in our Grommet theme - note that since it counts _up_ from breakpoint values, `default` applies from `0px` to the start of the `small` breakpoint.
diff --git a/packages/app-project/src/shared/components/Media/index.js b/packages/app-project/src/shared/components/Media/index.js
new file mode 100644
index 0000000000..ac0d186944
--- /dev/null
+++ b/packages/app-project/src/shared/components/Media/index.js
@@ -0,0 +1 @@
+export * from './Media'
diff --git a/packages/lib-grommet-theme/src/index.js b/packages/lib-grommet-theme/src/index.js
index ebc28cdf24..1cfe96cf1d 100644
--- a/packages/lib-grommet-theme/src/index.js
+++ b/packages/lib-grommet-theme/src/index.js
@@ -110,12 +110,15 @@ const theme = deepFreeze({
breakpoints: {
small: {
edgeSize: {
- xxsmall: '5px',
- xsmall: `10px`,
small: `15px`,
medium: `20px`,
large: `25px`,
- xlarge: `30px`
+ xlarge: `30px`,
+
+ 'small-neg': `-15px`,
+ 'medium-neg': `-20px`,
+ 'large-neg': `-25px`,
+ 'xlarge-neg': `-30px`
}
}
},
@@ -132,7 +135,14 @@ const theme = deepFreeze({
small: `20px`,
medium: `30px`,
large: `50px`,
- xlarge: `90px`
+ xlarge: `90px`,
+
+ 'xxsmall-neg': '-5px',
+ 'xsmall-neg': `-10px`,
+ 'small-neg': `-20px`,
+ 'medium-neg': `-30px`,
+ 'large-neg': `-50px`,
+ 'xlarge-neg': `-90px`
},
elevation: {
light: {
diff --git a/yarn.lock b/yarn.lock
index 694054b268..d3a98de98f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,6 +2,11 @@
# yarn lockfile v1
+"@artsy/fresnel@~1.0.7":
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/@artsy/fresnel/-/fresnel-1.0.7.tgz#ba1a12610e0d0e86fa2cd124c4d823297b0d4211"
+ integrity sha512-iXPiAKoArZB5ssJW4fUFPka48XUzDUt65LOWAcEPfwQC5V/KJ7ggSvKbMbfmU9Cr4arSGufWfSX94h/2D19SJg==
+
"@babel/code-frame@7.0.0", "@babel/code-frame@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8"
@@ -15809,6 +15814,11 @@ supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0, supports-co
dependencies:
has-flag "^3.0.0"
+svg-loaders-react@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/svg-loaders-react/-/svg-loaders-react-2.0.1.tgz#67b50783b1dd0f9bb845a7248e46290ba828fc06"
+ integrity sha512-7oOA29mMLPuGNZ5UkyprH/4RysYy6F19/P8cr5mbrWJeNseH01ih5YpynNFhgpvUaQ8h6vStMS8wd3IFELcFYA==
+
svg-url-loader@^2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/svg-url-loader/-/svg-url-loader-2.3.3.tgz#4b111f6047472f815f9c1fd780c6faa413a8efab"