diff --git a/iris/index.js b/iris/index.js index 7745a9d511..f7e64573d3 100644 --- a/iris/index.js +++ b/iris/index.js @@ -25,10 +25,6 @@ const PORT = 3001; initPassport(); // API server const app = express(); -// $FlowFixMe -if (IS_PROD) { - require('newrelic'); -} import middlewares from './routes/middlewares'; app.use(middlewares); diff --git a/iris/newrelic.js b/iris/newrelic.js deleted file mode 100644 index 14be5b490c..0000000000 --- a/iris/newrelic.js +++ /dev/null @@ -1,25 +0,0 @@ -'use strict'; -/** - * New Relic agent configuration. - * - * See lib/config.defaults.js in the agent distribution for a more complete - * description of configuration variables and their potential values. - */ -exports.config = { - /** - * Array of application names. - */ - app_name: ['Spectrum'], - /** - * Your New Relic license key. - */ - license_key: 'aeeaaf8d4e70e0e859cc3a4d000fb13b7b319b01', - logging: { - /** - * Level at which to log. 'trace' is most useful to New Relic when diagnosing - * issues with the agent, 'info' and higher will impose the least overhead on - * production applications. - */ - level: 'info', - }, -}; diff --git a/iris/routes/auth/logout.js b/iris/routes/auth/logout.js index a2fc943579..1c90273585 100644 --- a/iris/routes/auth/logout.js +++ b/iris/routes/auth/logout.js @@ -1,5 +1,6 @@ // @flow import { Router } from 'express'; +const debug = require('debug')('iris:routes:auth:logout'); import { destroySession } from '../../models/session'; const IS_PROD = process.env.NODE_ENV === 'production'; @@ -7,24 +8,19 @@ const HOME = IS_PROD ? '/' : 'http://localhost:3000/'; const logoutRouter = Router(); logoutRouter.get('/', (req, res) => { + debug('started'); const sessionCookie = req.cookies['connect.sid']; if (req.isUnauthenticated() || !sessionCookie) { + debug('is unauthenticated, aborting logout'); return res.redirect(HOME); } - const sessionId = sessionCookie.split('.')[0].replace('s:', ''); - return destroySession(sessionId) - .then(() => { - // I should not have to do this manually - // but it doesn't work otherwise ¯\_(ツ)_/¯ - res.clearCookie('connect.sid'); - req.logout(); - res.redirect(HOME); - }) - .catch(err => { - res.clearCookie('connect.sid'); - console.log(err); - res.redirect(HOME); - }); + debug('logging out'); + req.logout(); + req.session.destroy(err => { + if (err) console.log(err); + debug(`destroyed session, redirecting`); + res.redirect(HOME); + }); }); export default logoutRouter; diff --git a/iris/routes/middlewares/index.js b/iris/routes/middlewares/index.js index f1a7aeaa7e..319dae43b4 100644 --- a/iris/routes/middlewares/index.js +++ b/iris/routes/middlewares/index.js @@ -3,6 +3,11 @@ import { Router } from 'express'; const middlewares = Router(); +if (process.env.NODE_ENV === 'development') { + const logging = require('./logging'); + middlewares.use(logging); +} + if (process.env.NODE_ENV === 'production' && !process.env.FORCE_DEV) { // Raven (Sentry client) needs to come before everything else const raven = require('./raven').default; diff --git a/iris/routes/middlewares/logging.js b/iris/routes/middlewares/logging.js new file mode 100644 index 0000000000..edffceb546 --- /dev/null +++ b/iris/routes/middlewares/logging.js @@ -0,0 +1,8 @@ +// @flow +// Log requests with debug +const debug = require('debug')('iris:web'); + +module.exports = (req: Request, res: Response, next: Function) => { + debug(`requesting ${req.url}`); + next(); +}; diff --git a/iris/routes/middlewares/session.js b/iris/routes/middlewares/session.js index 369e237e59..4e884da2b0 100644 --- a/iris/routes/middlewares/session.js +++ b/iris/routes/middlewares/session.js @@ -14,7 +14,7 @@ export default session({ // Forces a session that is "uninitialized" to be saved to the store // NOTE(@mxstbr): This might not be necessary or even useful, but the default example of // session-rethinkdb uses it. Ref: llambda/session-rethinkdb#12 - saveUninitialized: true, + saveUninitialized: false, // Force a session identifier cookie to be set on every response, resets the expire date of the // cookie to one year from the time of the response, meaning you'll only get logged out after a // year of inactivity. diff --git a/package.json b/package.json index d1c014218b..74c8260262 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "flow-bin": "^0.43.0", "lint-staged": "^3.3.0", "micromatch": "^3.0.4", + "newrelic": "^2.2.0", "nodemon": "^1.11.0", "prettier": "^1.0.0", "raw-loader": "^0.5.1", diff --git a/public/img/conversation.svg b/public/img/conversation.svg new file mode 100644 index 0000000000..9693b2d946 --- /dev/null +++ b/public/img/conversation.svg @@ -0,0 +1,162 @@ + +conversation +Created using Figma + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/discover.png b/public/img/discover.png new file mode 100644 index 0000000000..ccf7127be4 Binary files /dev/null and b/public/img/discover.png differ diff --git a/public/img/goopy-2.svg b/public/img/goopy-2.svg deleted file mode 100644 index 185d6f0592..0000000000 --- a/public/img/goopy-2.svg +++ /dev/null @@ -1,37 +0,0 @@ - -goopy-2 -Created using Figma - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/img/goopy-3.svg b/public/img/goopy-3.svg deleted file mode 100644 index 09cb229028..0000000000 --- a/public/img/goopy-3.svg +++ /dev/null @@ -1,21 +0,0 @@ - -goopy-3 -Created using Figma - - - - - - - - - - - - - - - - - - diff --git a/public/img/goopy-4.svg b/public/img/goopy-4.svg deleted file mode 100644 index e1fffe4687..0000000000 --- a/public/img/goopy-4.svg +++ /dev/null @@ -1,17 +0,0 @@ - -goopy-4 -Created using Figma - - - - - - - - - - - - - - diff --git a/public/img/goopy.svg b/public/img/goopy.svg deleted file mode 100755 index 98bfec8567..0000000000 --- a/public/img/goopy.svg +++ /dev/null @@ -1,52 +0,0 @@ - -goopy -Created using Figma - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/actions/authentication.js b/src/actions/authentication.js index 5a4fba152c..28fb594b48 100644 --- a/src/actions/authentication.js +++ b/src/actions/authentication.js @@ -4,7 +4,7 @@ import { clearApolloStore } from '../api'; import { removeItemFromStorage, storeItem } from '../helpers/localStorage'; import Raven from 'raven-js'; -export const logout = () => { +export const logout = dispatch => { track(`user`, `sign out`, null); // clear localStorage removeItemFromStorage('spectrum'); @@ -15,6 +15,10 @@ export const logout = () => { process.env.NODE_ENV === 'production' ? '/auth/logout' : 'http://localhost:3001/auth/logout'; + + dispatch({ + type: 'CLEAR_USER', + }); }; export const saveUserDataToLocalStorage = (user: Object) => dispatch => { diff --git a/src/api/index.js b/src/api/index.js index dd49dfbf25..41bd03b672 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -101,6 +101,11 @@ export const SERVER_URL = ? `${window.location.protocol}//${window.location.host}` : 'http://localhost:3001'; +export const CLIENT_URL = + process.env.NODE_ENV === 'production' + ? `${window.location.protocol}//${window.location.host}` + : 'http://localhost:3000'; + export const PUBLIC_STRIPE_KEY = process.env.NODE_ENV === 'production' ? 'pk_live_viV7X5XXD1sw8aN2NgQjiff6' diff --git a/src/components/buttons/style.js b/src/components/buttons/style.js index af16a4e040..bee70baae7 100644 --- a/src/components/buttons/style.js +++ b/src/components/buttons/style.js @@ -20,7 +20,7 @@ const baseButton = css` line-height: 1; position: relative; text-align: center; - padding: ${props => (props.icon ? '4px 8px 4px 4px' : '12px 16px')}; + padding: ${props => (props.icon ? '4px 8px' : '12px 16px')}; &:hover { transition: ${Transition.hover.on}; diff --git a/src/components/fullscreenView/index.js b/src/components/fullscreenView/index.js index 22f5ea1371..b5639ac4f9 100644 --- a/src/components/fullscreenView/index.js +++ b/src/components/fullscreenView/index.js @@ -5,7 +5,7 @@ import { ClusterTwo, ClusterThree, ClusterFour, -} from '../../views/homepage/style'; +} from '../../views/splash/components/illustrations'; import Icon from '../../components/icons'; import { FullscreenViewContainer, Illustrations, Close } from './style'; diff --git a/src/components/goop/index.js b/src/components/goop/index.js new file mode 100644 index 0000000000..5c3c33c9c1 --- /dev/null +++ b/src/components/goop/index.js @@ -0,0 +1,128 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import { zIndex } from '../globals'; + +/* eslint no-eval: 0 */ + +export const InlineSvg = styled.svg` + position: absolute; + top: auto; + right: 0; + bottom: 0; + left: 0; + width: 100%; + color: inherit; + fill: currentColor; + + > g { + position: absolute; + bottom: 0; + left: 0; + right: 0; + top: auto; + } +`; + +export const SvgWrapper = styled.div` + position: relative; + flex: none; + z-index: ${zIndex.base}; + height: 80px; + width: 110%; + bottom: -4px; + left: -5%; + right: -5%; + display: ${props => (props.goop === 0 ? 'none' : 'inline-block')}; + color: ${props => eval(`props.theme.${props.color}`)}; + + @media (max-width: 768px) { + width: 150%; + left: -25%; + right: -25%; + } +`; + +class Goop extends React.Component { + returnGoop() { + switch (this.props.goop) { + default: + case 0: + return null; + case 1: + return ( + + + + ); + case 2: + return ( + + + + ); + case 3: + return ( + + + + ); + case 4: + return ( + + + + ); + case 5: + return ( + + + + + ); + case 6: + return ( + + + + + + ); + case 7: + return ( + + + + + ); + } + } + + render() { + return ( + + + goop + {this.returnGoop()} + + + ); + } +} + +Goop.defaultProps = { + color: 'bg.default', +}; + +export default Goop; diff --git a/src/components/linkPreview/index.js b/src/components/linkPreview/index.js index d2b1ba5d26..61b6ef7972 100644 --- a/src/components/linkPreview/index.js +++ b/src/components/linkPreview/index.js @@ -8,7 +8,6 @@ import { LinkPreviewImage, LinkPreviewTextContainer, MetaTitle, - MetaDescription, MetaUrl, Close, LinkPreviewSkeleton, @@ -30,12 +29,7 @@ export class LinkPreview extends Component { }; render() { - let { - data: { description, image, title, url, trueUrl }, - editable, - margin, - } = this.props; - description = description ? truncate(description, 80) : ''; + let { data: { image, title, url, trueUrl }, editable, margin } = this.props; title = title ? truncate(title, 72) : ''; return ( diff --git a/src/components/logo/index.js b/src/components/logo/index.js index 17d7225503..9093ee5d2b 100644 --- a/src/components/logo/index.js +++ b/src/components/logo/index.js @@ -3,37 +3,56 @@ import React from 'react'; //$FlowFixMe import styled from 'styled-components'; -const Svg = styled.svg` - fill: ${props => (props.color ? props.color : props.theme.text.reverse)}; - max-width: 100%; +export const Svg = styled.svg` + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + height: 100%; + width: 100%; + color: inherit; + fill: currentColor; +`; + +export const SvgWrapper = styled.div` + display: inline-block; + flex: none; + width: 160px; + height: 24px; + top: 4px; + position: relative; + color: inherit; `; export const Logo = () => { return ( - + + ); }; diff --git a/src/components/upsell/index.js b/src/components/upsell/index.js index e8a1ff84e2..53bbe6b2ec 100644 --- a/src/components/upsell/index.js +++ b/src/components/upsell/index.js @@ -15,6 +15,7 @@ import { openModal } from '../../actions/modals'; import { Avatar } from '../avatar'; import Card from '../card'; import { Button, OutlineButton } from '../buttons'; +import { Login } from '../../views/login'; import { Title, MiniTitle, @@ -178,111 +179,7 @@ export class UpsellSignIn extends Component { const preferredSigninMethod = getItemFromStorage('preferred_signin_method'); if (isSigningIn) { - const title = - signinType === 'signup' ? 'Good times ahead!' : 'Welcome back!'; - const subtitle = - signinType === 'signup' - ? 'Spectrum is a place where communities can share, discuss, and grow together. Sign in below to get in on the conversation.' - : "We're happy to see you again - log in below to get back into the conversation!"; - const verb = signinType === 'signup' ? 'Sign up' : 'Log in'; - - return ( - - - - - - - {title} - - - {subtitle} - - - - {preferredSigninMethod && - - this.trackSignin('secondary cta', 'twitter')} - > - {verb} with Twitter - - - - this.trackSignin('secondary cta', 'facebook')} - > - {verb} with Facebook - - - this.trackSignin('secondary cta', 'google')} - > - {verb} with Google - - } - - {!preferredSigninMethod && - - this.trackSignin('secondary cta', 'twitter')} - > - {verb} with Twitter - - - - this.trackSignin('secondary cta', 'facebook')} - > - {verb} with Facebook - - - this.trackSignin('secondary cta', 'google')} - > - {verb} with Google - - } - - - - By using Spectrum, you agree to our{' '} - - Code of Conduct - - - - - ); + return ; } else { const subtitle = view ? view.type === 'community' diff --git a/src/helpers/signed-out-fallback.js b/src/helpers/signed-out-fallback.js new file mode 100644 index 0000000000..d3592c551e --- /dev/null +++ b/src/helpers/signed-out-fallback.js @@ -0,0 +1,32 @@ +// @flow +// Render a component depending on a users authentication status +import React from 'react'; +// @FlowFixMe +import { connect } from 'react-redux'; + +// This is the component that determines at render time what to do +const Switch = props => { + const { Component, FallbackComponent, currentUser, ...rest } = props; + + if (!currentUser || !Component) { + return ; + } else { + return ; + } +}; + +// Connect that component to the Redux state +const ConnectedSwitch = connect(state => ({ + currentUser: state.users.currentUser, +}))(Switch); + +const signedOutFallback = (Component, FallbackComponent) => { + return props => + ; +}; + +export default signedOutFallback; diff --git a/src/index.js b/src/index.js index 064509c762..045e83f53c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,14 +5,16 @@ import ReactDOM from 'react-dom'; import { ThemeProvider } from 'styled-components'; //$FlowFixMe import { ApolloProvider } from 'react-apollo'; -import queryString from 'query-string'; +//$FlowFixMe +import { Router } from 'react-router'; import { history } from './helpers/history'; +import queryString from 'query-string'; import { client } from './api'; import { initStore } from './store'; import { getItemFromStorage } from './helpers/localStorage'; import { theme } from './components/theme'; import Routes from './routes'; -import Homepage from './views/homepage'; +import Splash from './views/splash'; import { addToastWithTimeout } from './actions/toasts'; import registerServiceWorker from './registerServiceWorker'; import type { ServiceWorkerResult } from './registerServiceWorker'; @@ -41,30 +43,16 @@ if (existingUser) { } function render() { - // if user is not stored in localStorage and they visit a blacklist url - if ( - (!existingUser || existingUser === null) && - (window.location.pathname === '/' || - window.location.pathname === '/messages' || - window.location.pathname === '/messages/new' || - window.location.pathname === '/notifications') - ) { - return ReactDOM.render( - - - , - document.querySelector('#root') - ); - } else { - return ReactDOM.render( + return ReactDOM.render( + - , - document.querySelector('#root') - ); - } + + , + document.querySelector('#root') + ); } try { diff --git a/src/reducers/users.js b/src/reducers/users.js index eabee0b253..fd68176a5a 100644 --- a/src/reducers/users.js +++ b/src/reducers/users.js @@ -8,6 +8,8 @@ export default function root(state = initialState, action) { return Object.assign({}, state, { currentUser: action.user, }); + case 'CLEAR_USER': + return (state = undefined); default: return state; } diff --git a/src/routes.js b/src/routes.js index 2d7d3dfd24..a155577bef 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,9 +1,9 @@ // @flow import React, { Component } from 'react'; //$FlowFixMe -import { Router, Route, Switch, Redirect } from 'react-router'; +import { Route, Switch, Redirect } from 'react-router'; //$FlowFixMe -import styled from 'styled-components'; +import styled, { ThemeProvider } from 'styled-components'; import generateMetaInfo from 'shared/generate-meta-info'; import { FlexCol } from './components/globals'; import { history } from './helpers/history'; @@ -26,6 +26,9 @@ import UserSettings from './views/userSettings'; import communitySettings from './views/communitySettings'; import channelSettings from './views/channelSettings'; import NewCommunity from './views/newCommunity'; +import Splash from './views/splash'; +import signedOutFallback from './helpers/signed-out-fallback'; +import { Login } from './views/login'; import ThreadSlider from './views/threadSlider'; const About = () => @@ -51,75 +54,96 @@ class Routes extends Component { const { title, description } = generateMetaInfo(); return ( - - - - {/* Default meta tags, get overriden by anything further down the tree */} - - {/* Global navigation, notifications, message notifications, etc */} - - - - - + + + {/* Default meta tags, get overriden by anything further down the tree */} + + {/* Global navigation, notifications, message notifications, etc */} - {/* - Switch only renders the first match. Subrouting happens downstream - https://reacttraining.com/react-router/web/api/Switch - */} - - - + + + + + + - {/* Public Business Pages */} - - - - - + {/* + Switch only renders the first match. Subrouting happens downstream + https://reacttraining.com/react-router/web/api/Switch + */} + + + - {/* App Pages */} - - } - /> - - - - - - } /> - - - + {/* Public Business Pages */} + + + + + - {/* - We check communitySlug last to ensure none of the above routes - pass. We handle null communitySlug values downstream by either - redirecting to home or showing a 404 - */} - - - - - - - - + {/* App Pages */} + + } + /> + + + + + + + } /> + + + + + {/* + We check communitySlug last to ensure none of the above routes + pass. We handle null communitySlug values downstream by either + redirecting to home or showing a 404 + */} + + + + + + + ); } } diff --git a/src/views/explore/index.js b/src/views/explore/index.js index 38ef1ab1a9..d3a2779237 100644 --- a/src/views/explore/index.js +++ b/src/views/explore/index.js @@ -11,7 +11,7 @@ import Titlebar from '../titlebar'; import AppViewWrapper from '../../components/appViewWrapper'; import Head from '../../components/head'; import { Column } from '../../components/column'; -import { GoopyThree } from '../../views/homepage/style'; +import Goop from '../../components/goop'; import { FeaturedCommunity } from '../../components/curation'; import TopCommunityList from './components/topCommunities'; import Search from './components/search'; @@ -45,9 +45,8 @@ const ExplorePure = props => { - + -
diff --git a/src/views/explore/style.js b/src/views/explore/style.js index 48dbdbbffc..8656f3ad5a 100644 --- a/src/views/explore/style.js +++ b/src/views/explore/style.js @@ -104,7 +104,7 @@ export const SectionWrapper = styled(FlexRow)` export const ViewHeader = styled(Section)` flex: none; - padding: 120px 0 160px 0; + padding: 120px 0 0 0; justify-content: flex-end; background-color: ${({ theme }) => theme.space.dark}; background-image: ${({ theme }) => @@ -114,7 +114,7 @@ export const ViewHeader = styled(Section)` )}, ${theme.space.dark} )`}; @media (max-width: 768px) { - padding: 48px 24px 96px 24px; + padding: 48px 24px 0 24px; } `; @@ -265,6 +265,14 @@ export const SearchWrapper = styled(Card)` position: relative; margin-bottom: 48px; padding: 12px 16px; + box-shadow: ${Shadow.low} ${props => hexa(props.theme.bg.reverse, 0.15)}; + transition: ${Transition.hover.off}; + z-index: ${zIndex.search}; + + &:hover{ + box-shadow: ${Shadow.high} ${props => hexa(props.theme.bg.reverse, 0.25)}; + transition: ${Transition.hover.on}; + } `; export const SearchInputWrapper = styled(FlexRow)` @@ -307,7 +315,6 @@ export const SearchResultsDropdown = styled.ul` flex: auto; max-height: 400px; overflow-y: scroll; - z-index: ${zIndex.dropDown}; background: ${props => props.theme.bg.default}; @media (max-width: 768px) { @@ -369,11 +376,11 @@ export const SearchLink = styled(Link)` `; export const SearchResultImage = styled(Avatar)` - margin: 8px 16px 8px 8px; + margin: 4px 6px 8px 4px; `; export const SearchResultMetaWrapper = styled(FlexCol)` - margin-left: 4px; + margin-left: 16px; `; export const SearchResultName = styled.h2` diff --git a/src/views/homepage/index.js b/src/views/homepage/index.js deleted file mode 100644 index 376d5d22c8..0000000000 --- a/src/views/homepage/index.js +++ /dev/null @@ -1,262 +0,0 @@ -// @flow -import React, { Component } from 'react'; -import { track } from '../../helpers/events'; -import Icon from '../../components/icons'; -import { FlexCol, FlexRow } from '../../components/globals'; -import { SERVER_URL } from '../../api'; -import { storeItem, getItemFromStorage } from '../../helpers/localStorage'; -import { - SectionOne, - SectionTwo, - SectionThree, - SectionFour, - ClusterOne, - ClusterTwo, - ClusterThree, - ClusterFour, - GoopyOne, - GoopyTwo, - GoopyThree, - GoopyFour, - Wrapper, - Tagline, - ButtonTwitter, - ButtonFacebook, - ButtonGoogle, - LogoContainer, - LogoWhite, - SectionContent, - Copy, - Footer, - LinkBlock, - LoginCard, -} from './style'; - -class Homepage extends Component { - state: { - preferredSigninMethod: string, - }; - - constructor() { - super(); - - const preferredSigninMethod = getItemFromStorage('preferred_signin_method'); - this.state = { - preferredSigninMethod, - }; - } - - componentDidMount() { - track('homepage', 'viewed', null); - } - - trackSignin = (type, method) => { - track('homepage', 'logged in', type); - storeItem('preferred_signin_method', method); - }; - - render() { - const { preferredSigninMethod } = this.state; - - return ( - - - - - - - - Where communities are built. - - this.trackSignin('primary cta', 'twitter')} - > - Sign in with Twitter - - - this.trackSignin('primary cta', 'facebook')} - > - Sign in with Facebook - - - this.trackSignin('primary cta', 'google')} - > - Sign in with Google - - - - Where communities are built. - - - - - - - - - All your favorite communities. Only one you. - - All your favorite communities. Only one you. - - - For years people have been hacking different messaging platforms - to support growing online communities. - - - Spectrum was built from the ground up to keep you connected with - the communities you care about in one simple feed. - - - - - - - - - A better way to stay connected. - - In most apps, channels get jumbled, messages are lost, and that - really great answer to your question from way-back-when is - nowhere to be found. - - - Spectrum keeps each conversation in its own unique and shareable - place so that you can find it whenever you're ready. - - - A better way to stay connected. - - - - - - - - - - Come on in, the chatter's fine. - - Come on in, the chatter's fine. - Spectrum is free for everyone, so dive on in! - - {preferredSigninMethod && - - - this.trackSignin('secondary cta', 'twitter')} - > - Sign in with Twitter - - - - this.trackSignin('secondary cta', 'facebook')} - > - {' '} - Sign in with Facebook - - - - this.trackSignin('secondary cta', 'google')} - > - Sign in with Google - - } - - {!preferredSigninMethod && - - - this.trackSignin('secondary cta', 'twitter')} - > - Sign in with Twitter - - - - this.trackSignin('secondary cta', 'facebook')} - > - {' '} - Sign in with Facebook - - - - this.trackSignin('secondary cta', 'google')} - > - Sign in with Google - - } - - - - - -
- - - - - - - -
Code of Conduct
-
- -
Support
-
- -
Contact
-
-
-
-
- ); - } -} - -export default Homepage; diff --git a/src/views/homepage/style.js b/src/views/homepage/style.js deleted file mode 100644 index e0c984d4f8..0000000000 --- a/src/views/homepage/style.js +++ /dev/null @@ -1,412 +0,0 @@ -import styled from 'styled-components'; -import { Logo } from '../../components/logo'; -import { - Gradient, - H2, - FlexCol, - FlexRow, - P, - Transition, - Shadow, - zIndex, -} from '../../components/globals'; - -export const Wrapper = styled(FlexCol)` - flex: 1 0 auto; - width: 100%; - background-color: ${({ theme }) => theme.bg.default}; - overflow: auto; - overflow-x: hidden; -`; - -export const Section = styled(FlexCol)` - position: relative; - flex: 1 0 720px; - justify-content: center; - padding-bottom: 80px; - - @media (max-width: 768px) { - flex-basis: 480px; - } - - img { - width: 320px; - - @media (max-width: 768px) { - display: none; - } - } -`; - -export const SectionOne = styled(Section)` - background-color: ${({ theme }) => theme.space.dark}; - background-image: ${({ theme }) => - Gradient(theme.space.dark, theme.brand.alt)}; - color: ${({ theme }) => theme.text.reverse}; - - img { - margin-left: 40px; - width: 240px; - } - - @media (max-width: 768px) { - flex-basis: 600px; - } -`; - -export const SectionTwo = styled(Section)` - justify-content: space-between; - height: 640px; - background-color: ${({ theme }) => theme.bg.default}; - color: ${({ theme }) => theme.text.default}; - padding-bottom: 160px; - - img { - margin-right: 80px; - width: 360px; - } -`; - -export const SectionThree = styled(Section)` - height: 640px; - justify-content: space-around; - background-color: ${({ theme }) => theme.space.dark}; - background-image: linear-gradient(to bottom, ${({ theme }) => - `${theme.space.dark}, ${theme.brand.default}`}); - color: ${({ theme }) => theme.text.reverse}; - - img { - margin-left: 80px; - } -`; - -export const SectionFour = styled(Section)` - height: 480px; - justify-content: space-around; - background-color: ${({ theme }) => theme.bg.default}; - color: ${({ theme }) => theme.text.default}; - - img { - margin-right: 80px; - width: 420px; - } - - button { - margin-top: 24px; - color: ${({ theme }) => theme.brand.default}; - - &:hover { - box-shadow: 0 4px 16px rgba(56, 24, 229, 0.5); - } - } -`; - -export const Footer = styled(Section)` - flex-direction: row; - flex: 0 0 80px; - background-color: ${({ theme }) => theme.space.light}; - color: ${({ theme }) => theme.text.reverse}; - justify-content: space-between; - align-items: center; - padding: 0 40px; - - @media (max-width: 768px) { - flex-basis: auto; - flex-direction: column; - justify-content: flex-start; - padding: 40px; - } -`; - -export const LinkBlock = styled.a` - display: inline-block; - margin: 0 24px; - flex: 0 0 auto; - position: relative; - - div { - font-size: 16px; - font-weight: 700; - padding: 12px 16px; - top: 4px; - position: relative; - text-align: center; - transition: ${Transition.hover.off}; - border-radius: 4px; - - &:hover { - border-radius: 12px; - background-color: ${({ theme }) => theme.bg.default}; - color: #009eba; - transition: ${Transition.hover.on}; - } - } - - @media (max-width: 768px) { - flex-direction: column; - justify-content: flex-start; - padding-bottom: 16px; - - div { - border-bottom: none; - - &:hover { - border-bottom: none; - padding-bottom: 0; - text-decoration: underline; - } - } - } -`; - -export const Cluster = styled.img`position: absolute;`; - -export const ClusterOne = styled(Cluster)` - max-width: 120px; - max-height: 120px; - opacity: 0.15; - top: 10%; - left: 10%; - z-index: ${zIndex.base}; -`; - -export const ClusterTwo = styled(Cluster)` - max-width: 160px; - max-height: 160px; - opacity: 0.15; - top: 60%; - right: 10%; - z-index: ${zIndex.base}; -`; - -export const ClusterThree = styled(Cluster)` - max-width: 80px; - max-height: 80px; - opacity: 0.15; - top: 10%; - right: 40%; - z-index: ${zIndex.base}; -`; - -export const ClusterFour = styled(Cluster)` - max-width: 80px; - max-height: 80px; - opacity: 0.15; - top: 80%; - left: 40%; - z-index: ${zIndex.base}; -`; - -export const GoopyOne = styled.div` - background-color: transparent; - background: url(/img/goopy.svg) center bottom no-repeat; - position: absolute; - background-size: 100%; - z-index: ${zIndex.background}; - height: calc(100% + 4px); - width: 110%; - top: 0; - bottom: -2px; - left: -5%; - right: -5%; -`; - -export const GoopyTwo = styled.div` - background-color: transparent; - background: url(/img/goopy-2.svg) center bottom no-repeat; - position: absolute; - background-size: 100%; - z-index: ${zIndex.background}; - height: calc(100% + 2px); - top: 0; - width: 110%; - bottom: -2px; - left: -5%; - right: -5%; -`; - -export const GoopyThree = styled.div` - background-color: transparent; - background: url(/img/goopy-3.svg) center bottom no-repeat; - position: absolute; - background-size: 100%; - z-index: ${zIndex.background}; - height: calc(100% + 2px); - top: 0; - width: 110%; - bottom: -2px; - left: -5%; - right: -5%; -`; - -export const GoopyFour = styled.div` - background-color: transparent; - background: url(/img/goopy-4.svg) center bottom no-repeat; - position: absolute; - background-size: 100%; - z-index: ${zIndex.background}; - height: calc(100% + 2px); - top: 0; - width: 110%; - bottom: -2px; - left: -5%; - right: -5%; -`; - -export const Tagline = styled(H2)` - font-weight: 700; - font-size: 24px; - margin-top: 8px; - margin-bottom: 24px; - color: inherit; -`; - -export const Button = styled.a` - display: flex; - flex-shrink: 1; - z-index: ${zIndex.base + 1}; - flex-direction: flex-row; - align-self: flex-start; - align-items: center; - color: ${({ theme }) => theme.text.reverse}; - border-radius: 8px; - padding: 8px; - padding-right: 16px; - font-size: 14px; - font-weight: 700; - transition: ${Transition.hover.off}; - position: relative; - margin: 16px 0; - - ${props => - props.after && - ` - margin: 24px 0; - - &:after { - content: 'Previously signed in with'; - position: absolute; - top: -23px; - font-size: 10px; - font-weight: 500; - text-transform: uppercase; - opacity: 0.8; - left: 50%; - transform: translateX(-50%); - width: 100%; - text-align: center; - color: #fff; - } - `} span { - display: inline-block; - flex: 0 0 auto; - margin-top: -1px; - margin-left: 8px; - line-height: 2.45; - word-break: keep-all; - white-space: nowrap; - color: currentColor; - } - - svg { - fill: currentColor !important; - } - - &:hover { - cursor: pointer; - } -`; - -export const ButtonTwitter = styled(Button)` - background: ${props => - props.preferred ? props.theme.social.twitter.default : 'none'}; - color: ${props => - props.whitebg - ? props.theme.social.twitter.default - : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; - - &:hover { - color: ${props => - props.whitebg ? props.theme.social.twitter.default : '#fff'} - } -`; - -export const ButtonFacebook = styled(Button)` - background: ${props => - props.preferred ? props.theme.social.facebook.default : 'none'}; - color: ${props => - props.whitebg - ? props.theme.social.facebook.default - : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; - - - &:hover { - color: ${props => - props.whitebg ? props.theme.social.facebook.default : '#fff'} - } -`; - -export const ButtonGoogle = styled(Button)` - background: ${props => - props.preferred ? props.theme.social.google.default : 'none'}; - color: ${props => - props.whitebg - ? props.theme.social.google.default - : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; - - &:hover { - color: ${props => - props.whitebg ? props.theme.social.google.default : '#fff'} - } -`; - -export const LinkButton = styled(Button)` - margin-top: 24px; - color: ${({ theme }) => theme.brand.default}; - background: ${({ theme }) => theme.bg.default}; - - &:hover { - box-shadow: ${Shadow.high} ${({ theme }) => theme.space.soft}; - } -`; - -export const LogoWhite = styled(Logo)` - max-width: 360px; -`; - -export const LogoContainer = styled.div`max-width: 360px;`; - -export const SectionContent = styled(FlexRow)` - flex-grow: 1; - width: 100%; - align-items: center; - justify-content: center; - position: relative; - z-index: ${zIndex.base + 1}; - padding: 0 10%; - - @media (max-width: 768px) { - margin-top: 80px; - } -`; - -export const Copy = styled(P)` - max-width: 400px; - width: 100%; - font-size: 18px; - line-height: 24px; - color: inherit; - font-weight: 500; - - &:not(:first-of-type){ - margin-top: 24px; - } -`; - -export const LoginCard = styled.div` - border-radius: 12px; - padding: 16px 0; - margin-top: 16px; - align-self: flex-start; - align-items: flex-start; -`; diff --git a/src/views/login/index.js b/src/views/login/index.js new file mode 100644 index 0000000000..7ff178d12b --- /dev/null +++ b/src/views/login/index.js @@ -0,0 +1,162 @@ +// @flow +import React, { Component } from 'react'; +import Icon from '../../components/icons'; +import FullscreenView from '../../components/fullscreenView'; +import { getItemFromStorage, storeItem } from '../../helpers/localStorage'; +import { SERVER_URL, CLIENT_URL } from '../../api'; +import { + LargeTitle, + LargeSubtitle, + UpsellIconContainer, + FullscreenContent, + CodeOfConduct, + SigninButtonsContainer, + ButtonTwitter, + ButtonFacebook, + ButtonGoogle, + Col, +} from './style'; + +export class Login extends Component { + state: { + isSigningIn: Boolean, + signinType: string, + }; + + constructor(props) { + super(props); + + this.state = { + isSigningIn: false, + signinType: props.signinType || 'signup', + }; + } + + toggleSigningIn = type => { + const { isSigningIn } = this.state; + this.setState({ + isSigningIn: !isSigningIn, + signinType: type, + }); + }; + + trackSignin = (type, method) => { + storeItem('preferred_signin_method', method); + }; + + render() { + const { signinType } = this.state; + const preferredSigninMethod = getItemFromStorage('preferred_signin_method'); + const { redirectPath } = this.props; + + const viewTitle = + signinType === 'login' ? 'Welcome back!' : 'Welcome to Spectrum!'; + + const viewSubtitle = + signinType === 'login' + ? "We're happy to see you again - log in below to get back into the conversation!" + : 'Spectrum is a place where communities can share, discuss, and grow together. Sign in below to get in on the conversation.'; + + const verb = signinType === 'login' ? 'Log in ' : 'Sign up '; + + const postAuthRedirectPath = redirectPath + ? `?r=${redirectPath}` + : `?r=${CLIENT_URL}/home`; + + return ( + + + + + + + {viewTitle} + + + {viewSubtitle} + + + + {preferredSigninMethod && + + this.trackSignin('secondary cta', 'twitter')} + > + {verb} with Twitter + + + this.trackSignin('secondary cta', 'facebook')} + > + {verb} with Facebook + + + this.trackSignin('secondary cta', 'google')} + > + {verb} with Google + + } + + {!preferredSigninMethod && + + this.trackSignin('secondary cta', 'twitter')} + > + {verb} with Twitter + + + this.trackSignin('secondary cta', 'facebook')} + > + {verb} with Facebook + + + this.trackSignin('secondary cta', 'google')} + > + {verb} with Google + + } + + + + By using Spectrum, you agree to our{' '} + + Code of Conduct + + + + + ); + } +} diff --git a/src/views/login/style.js b/src/views/login/style.js new file mode 100644 index 0000000000..ada98e11a7 --- /dev/null +++ b/src/views/login/style.js @@ -0,0 +1,376 @@ +// @flow +// $FlowFixMe +import styled from 'styled-components'; +import { + FlexRow, + FlexCol, + Gradient, + Transition, + Shadow, + hexa, + zIndex, +} from '../../components/globals'; +import { Button } from '../../components/buttons'; + +export const Title = styled.h1` + color: ${props => props.theme.text.default}; + width: 100%; + font-weight: 800; + font-size: 24px; + line-height: 1.25; + margin-bottom: 8px; + padding: 0; + font-size: 24px; + text-align: center; + letter-spacing: 0.2px; +`; + +export const LargeTitle = styled(Title)` + font-size: 40px; + font-weight: 900; + letter-spacing: 0.3px; + margin-bottom: 16px; +`; + +export const SmallTitle = styled(Title)` + font-size: 18px; +`; + +export const MiniTitle = styled(Title)` + font-weight: 700; + font-size: 1rem; + line-height: 1.25; +`; + +export const Actions = styled.div` + display: flex; + justify-content: center; + align-items: center; + + button { + margin: 0 8px; + } +`; + +export const Subtitle = styled.h2` + width: 100%; + color: ${props => props.theme.text.alt}; + font-weight: 500; + font-size: 16px; + line-height: 1.4; + margin-bottom: 16px; + padding: 0 32px; + text-align: center; + + b { + font-weight: 700; + } + + a { + color: ${props => props.theme.brand.default}; + } + + li { + margin-top: 8px; + list-style-type: none; + } +`; + +export const LargeSubtitle = styled(Subtitle)` + font-size: 20px; +`; + +export const MiniSubtitle = styled(Subtitle)` + font-weight: 600; + color: ${props => props.theme.text.alt}; + font-size: 0.875rem; + line-height: 1.4; +`; + +export const SmallSubtitle = styled(Subtitle)` + font-size: 15px; +`; + +export const Cost = styled(Subtitle)` + margin-top: 8px; + font-weight: bold; +`; + +export const NullCol = styled(FlexCol)` + background-image: ${props => + props.bg ? `url('/img/fills/${props.bg}.svg')` : 'none'}; + background-color: transparent; + background-size: 110% auto; + background-repeat: ${props => (props.repeat ? 'repeat-y' : 'no-repeat')}; + background-position: ${props => + props.repeat ? 'center top' : 'center center'}; + width: 100%; + height: auto; + min-height: 160px; + flex: 0 0 auto; + padding: ${props => (props.noPadding ? '0' : '2rem')}; + justify-content: center; + align-items: center; + position: relative; + align-self: center; +`; + +export const NullRow = styled(FlexRow)` + background-image: url('/img/fills/${props => + props.bg ? `${props.bg}` : 'locked'}.svg'); + background-color: transparent; + background-size: 110% auto; + background-repeat: no-repeat; + background-attachment: center; + width: 100%; + height: auto; + padding: 1rem 15%; +`; + +export const UpgradeError = styled.p` + color: ${props => props.theme.warn.default}; + font-size: 14px; + text-align: center; + margin: 16px 0 0; +`; + +export const Profile = styled.div` + position: relative; + padding: 16px 0; + + img { + border-radius: 48px; + width: 48px; + height: 48px; + } + + span { + background-color: ${({ theme }) => theme.success.default}; + background-image: ${({ theme }) => + Gradient(theme.space.light, theme.success.default)}; + position: absolute; + left: 75%; + top: 48px; + color: ${({ theme }) => theme.text.reverse}; + font-size: 10px; + font-weight: 800; + padding: 2px 4px; + border-radius: 8px; + line-height: 1.5; + border: 2px solid #fff; + z-index: ${zIndex.avatar + 1}; + } +`; + +export const LargeEmoji = styled.div` + display: flex; + text-align: center; + flex 1; + padding: 16px 0 32px; + font-size: 48px; +`; + +export const UpsellIconContainer = styled.div` + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 16px; + margin-top: 32px; + color: ${props => props.theme.text.alt}; +`; + +export const SignupButton = styled(Button)` + font-size: 18px; + font-weight: 700; + color: ${props => props.theme.text.reverse}; + padding: 16px 88px; + max-width: 100%; + box-shadow: ${props => + `${Shadow.high} ${hexa(props.theme.bg.reverse, 0.15)}`}; + margin-top: 8px; +`; + +export const SignupFooter = styled.div` + display: flex; + justify-content: center; + padding: 16px; + font-size: 14px; + color: ${props => props.theme.text.alt}; + font-weight: 500; + border-top: 2px solid ${props => props.theme.bg.wash}; + margin-top: 40px; + width: 100%; +`; + +export const SigninLink = styled.span` + color: ${props => props.theme.brand.default}; + margin-left: 6px; + cursor: pointer; +`; + +export const FullscreenContent = styled.div` + width: 100%; + max-width: 768px; + display: flex; + align-items: center; + flex-direction: column; + padding: 32px 16px; + flex: 1 0 auto; +`; + +export const CodeOfConduct = styled.p` + display: inline-block; + font-size: 14px; + font-weight: 500; + color: ${props => props.theme.text.alt}; + border-radius: 8px; + margin-top: 64px; + margin-left: 32px; + margin-right: 32px; + text-align: center; + position: relative; + z-index: ${zIndex.card + 1}; + + a { + color: ${props => props.theme.brand.default}; + font-weight: 600; + } +`; + +export const SigninButtonsContainer = styled.div` + display: flex; + padding-top: 48px; + max-width: 100%; + + @media (max-width: 768px) { + padding-top: 24px; + } +`; + +export const Col = styled.div` + display: flex; + align-items: flex-end; + flex-wrap: wrap; + justify-content: center; + + @media (max-width: 768px) { + flex-direction: column; + align-items: center; + } +`; + +export const SigninButton = styled.a` + display: flex; + flex-shrink: 1; + z-index: ${zIndex.card + 1}; + flex-direction: flex-row; + align-self: flex-start; + align-items: center; + color: ${({ theme }) => theme.text.reverse}; + border-radius: 8px; + padding: 8px; + padding-right: 16px; + font-size: 14px; + font-weight: 700; + transition: ${Transition.hover.off}; + position: relative; + margin: 16px; + + ${props => + props.after && + ` + &:after { + content: 'Previously signed in with'; + position: absolute; + top: -32px; + font-size: 14px; + font-weight: 600; + left: 50%; + transform: translateX(-50%); + width: 100%; + text-align: center; + color: ${props.theme.text.alt}; + } + `} span { + display: inline-block; + flex: 0 0 auto; + margin-top: -1px; + margin-left: 8px; + line-height: 2.45; + word-break: keep-all; + white-space: nowrap; + color: currentColor; + } + + svg { + fill: currentColor !important; + } + + @media (max-width: 768px) { + margin: 16px 0; + + ${props => + props.after && + ` + margin: 48px 0 16px 0; + `}; + } + + &:hover { + cursor: pointer; + } +`; + +export const ButtonTwitter = styled(SigninButton)` + background: ${props => + props.preferred ? props.theme.social.twitter.default : 'none'}; + color: ${props => + props.whitebg + ? props.theme.social.twitter.default + : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; + + &:after { + color: ${props => props.theme.social.twitter.default}; + } + + &:hover { + color: ${props => + props.whitebg ? props.theme.social.twitter.default : '#fff'} + } +`; + +export const ButtonFacebook = styled(SigninButton)` + background: ${props => + props.preferred ? props.theme.social.facebook.default : 'none'}; + color: ${props => + props.whitebg + ? props.theme.social.facebook.default + : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; + + &:after { + color: ${props => props.theme.social.facebook.default}; + } + + &:hover { + color: ${props => + props.whitebg ? props.theme.social.facebook.default : '#fff'} + } +`; + +export const ButtonGoogle = styled(SigninButton)` + background: ${props => + props.preferred ? props.theme.social.google.default : 'none'}; + color: ${props => + props.whitebg + ? props.theme.social.google.default + : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; + + &:after { + color: ${props => props.theme.social.google.default}; + } + + &:hover { + color: ${props => + props.whitebg ? props.theme.social.google.default : '#fff'} + } +`; diff --git a/src/views/login/view.js b/src/views/login/view.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/views/navbar/components/profileDropdown.js b/src/views/navbar/components/profileDropdown.js index 4929a84dd8..dea159708d 100644 --- a/src/views/navbar/components/profileDropdown.js +++ b/src/views/navbar/components/profileDropdown.js @@ -50,9 +50,9 @@ export const ProfileDropdown = props => { My Settings } - + {/* Log Out - + */} ); diff --git a/src/views/navbar/index.js b/src/views/navbar/index.js index ea711fb5da..681eaab964 100644 --- a/src/views/navbar/index.js +++ b/src/views/navbar/index.js @@ -297,6 +297,9 @@ class Navbar extends Component { const isMobile = window.innerWidth < 768; const currentUserExists = loggedInUser !== null && loggedInUser !== undefined; + const isHome = + history.location.pathname === '/' || + history.location.pathname === '/home'; const { allUnseenCount, dmUnseenCount, @@ -304,6 +307,9 @@ class Navbar extends Component { showNewUserOnboarding, } = this.state; + // Bail out if the splash page is showing + if (!currentUserExists && isHome) return null; + // if the user is mobile and is viewing a thread or DM thread, don't // render a navbar - it will be replaced with a chat input const params = queryString.parse(history.location.search); @@ -517,6 +523,5 @@ export default compose( markNotificationsSeenMutation, markNotificationsReadMutation, markDirectMessageNotificationsSeenMutation, - withRouter, connect(mapStateToProps) )(Navbar); diff --git a/src/views/newCommunity/index.js b/src/views/newCommunity/index.js index 87bca03386..6e0825cae6 100644 --- a/src/views/newCommunity/index.js +++ b/src/views/newCommunity/index.js @@ -20,6 +20,7 @@ import EditCommunityForm from './components/editCommunityForm'; import Titlebar from '../titlebar'; import Stepper from './components/stepper'; import Share from './components/share'; +import { Login } from '../../views/login'; import { getCommunityByIdQuery } from '../../api/community'; import { Actions, @@ -149,84 +150,96 @@ class NewCommunity extends Component { }; render() { + const { currentUser } = this.props; const { activeStep, community, existingId, hasInvitedPeople } = this.state; const title = this.title(); const description = this.description(); - return ( - - - - - - - {title} - - - {description} - - - {// gather community meta info - activeStep === 1 && - !community && - } - - {activeStep === 1 && - community && - } - - {activeStep === 2 && - community && - community.id && - - - ; + } else { + return ( + + + + + + + + {title} + + + {description} + + + {// gather community meta info + activeStep === 1 && + !community && + } + + {activeStep === 1 && + community && + - - - } - - {// connect a slack team or invite via email - activeStep === 2 && - - this.step('previous')}> - Back - - {hasInvitedPeople - ? - : this.step('next')} - > - Skip this step - } - } - - {// share the community - activeStep === 3 && - - - } - - - - ); + />} + + {activeStep === 2 && + community && + community.id && + + + + + + } + + {// connect a slack team or invite via email + activeStep === 2 && + + this.step('previous')}> + Back + + {hasInvitedPeople + ? + : this.step('next')} + > + Skip this step + } + } + + {// share the community + activeStep === 3 && + + + } + +
+ + ); + } } } - -export default compose(pure, withApollo, connect())(NewCommunity); +const mapStateToProps = state => ({ currentUser: state.users.currentUser }); +export default compose(pure, withApollo, connect(mapStateToProps))( + NewCommunity +); diff --git a/src/views/newUserOnboarding/components/communitySearch/style.js b/src/views/newUserOnboarding/components/communitySearch/style.js index 088d13426d..7780f5227c 100644 --- a/src/views/newUserOnboarding/components/communitySearch/style.js +++ b/src/views/newUserOnboarding/components/communitySearch/style.js @@ -6,7 +6,6 @@ import { FlexRow, Shadow, hexa, - Truncate, zIndex, } from '../../../../components/globals'; import { Avatar } from '../../../../components/avatar'; diff --git a/src/views/splash/components/illustrations.js b/src/views/splash/components/illustrations.js new file mode 100644 index 0000000000..a8c8504045 --- /dev/null +++ b/src/views/splash/components/illustrations.js @@ -0,0 +1,110 @@ +// @flow +import React, { Component } from 'react'; +import styled from 'styled-components'; +import { zIndex, Shadow } from '../../../components/globals'; + +export const Cluster = styled.img` + position: absolute; + opacity: 0.05; +`; + +export const ClusterOne = styled(Cluster)` + max-width: 120px; + max-height: 120px; + top: 10%; + left: 10%; + z-index: ${zIndex.background}; +`; + +export const ClusterTwo = styled(Cluster)` + max-width: 160px; + max-height: 160px; + top: 60%; + right: 10%; + z-index: ${zIndex.background}; +`; + +export const ClusterThree = styled(Cluster)` + max-width: 80px; + max-height: 80px; + top: 10%; + right: 40%; + z-index: ${zIndex.background}; +`; + +export const ClusterFour = styled(Cluster)` + max-width: 80px; + max-height: 80px; + top: 80%; + left: 40%; + z-index: ${zIndex.background}; +`; + +export const Constellations = styled.div` + position: absolute; + background-color: transparent; + background: url('/img/constellations.svg') center top no-repeat; + background-size: cover 100%; + z-index: ${zIndex.background}; + height: calc(100% + 4px); + width: 110%; + top: -10px; + bottom: 0; + left: 0; + right: 0; + pointer-events: none; +`; + +export const ConversationWrapper = styled.div` + position: relative; + z-index: ${zIndex.background}; + + max-width: 480px; + overflow-y: hidden; + box-shadow: 0 0 32px 24px ${props => props.theme.bg.default}; + display: inline-block; + align-items: center; + justify-content: center; + + @media (max-width: 768px) { + display: none; + } + + > img { + height: 100%; + width: 100%; + object-fit: contain; + pointer-events: none; + } + + + div { + margin-left: 48px; + + @media (max-width: 768px) { + margin-left: 0; + margin-bottom: 32px; + } + } +`; + +export const Conversation = () => + + + ; + +const DiscoverImage = styled.img` + position: relative; + left: -24px; + max-width: 400px; + height: auto; + object-fit: contain; + + @media (max-width: 768px) { + left: auto; + margin-top: 32px; + max-width: 100%; + min-width: 256px; + } +`; + +export const Discover = () => ; diff --git a/src/views/splash/components/themes.js b/src/views/splash/components/themes.js new file mode 100644 index 0000000000..0b2e8915ce --- /dev/null +++ b/src/views/splash/components/themes.js @@ -0,0 +1,146 @@ +// @flow +import React from 'react'; +// @Flow Fix Me +import styled from 'styled-components'; +import Goop from '../../../components/goop'; +import { + ClusterOne, + ClusterTwo, + ClusterThree, + ClusterFour, + Constellations, +} from './illustrations'; +import { FlexCol, hexa } from '../../../components/globals'; + +export const Default = styled(FlexCol)` + display: flex; + position: relative; + flex: auto; + justify-content: center; + background-color: ${({ theme }) => theme.bg.default}; + color: ${({ theme }) => theme.text.default}; +`; + +export const Primary = styled(Default)` + background-color: ${({ theme }) => theme.space.dark}; + background-image: ${({ theme }) => + `radial-gradient(farthest-corner at 50% 100%, + ${hexa(theme.brand.alt, 0.75)}, ${theme.space.dark} + )`}; + color: ${({ theme }) => theme.text.reverse}; +`; + +export const Brand = styled(Default)` + background-color: ${({ theme }) => theme.brand.default}; + background-image: linear-gradient(to bottom, ${({ theme }) => + `${theme.brand.alt}, ${theme.brand.default}`}); + color: ${({ theme }) => theme.text.reverse}; +`; + +export const Dark = styled(Default)` + background-color: ${({ theme }) => theme.space.dark}; + background-image: linear-gradient(to bottom, ${({ theme }) => + `${theme.space.dark}, ${theme.brand.default}`}); + color: ${({ theme }) => theme.text.reverse}; +`; + +export const Space = styled(Default)` +background-color: ${({ theme }) => theme.space.dark}; +background-image: linear-gradient(to bottom, ${({ theme }) => + `${theme.space.light}, ${theme.space.dark}`}); +color: ${({ theme }) => theme.text.reverse}; +`; + +export const Light = styled(Default)` + background-color: ${({ theme }) => theme.space.light}; + color: ${({ theme }) => theme.text.reverse}; +`; + +export const Bright = styled(Default)` + background-color: ${({ theme }) => theme.brand.default}; + background-image: linear-gradient(to bottom, ${({ theme }) => + `${theme.space.light}, ${theme.brand.default}`}); + color: ${({ theme }) => theme.text.reverse}; +`; + +export const Grayscale = styled(Default)` + background-color: ${({ theme }) => theme.bg.reverse}; + background-image: linear-gradient(to bottom, ${({ theme }) => + `${theme.text.alt}, ${theme.bg.reverse}`}); + color: ${({ theme }) => theme.text.reverse}; +`; + +const Theme = props => { + switch (props.background) { + default: + return ( + + + + + + {props.children} + + + ); + case 'primary': + return ( + + {props.children} + + + ); + case 'brand': + return ( + + {props.children} + + + ); + case 'constellations': + return ( + + {props.children} + + + + ); + case 'dark': + return ( + + {props.children} + + + ); + case 'space': + return ( + + {props.children} + + + ); + case 'bright': + return ( + + {props.children} + + + ); + case 'light': + return ( + + {props.children} + + + ); + case 'grayscale': + return ( + + {props.children} + + + ); + } +}; + +export default Theme; diff --git a/src/views/splash/index.js b/src/views/splash/index.js new file mode 100644 index 0000000000..7b6ff031f1 --- /dev/null +++ b/src/views/splash/index.js @@ -0,0 +1,55 @@ +// @flow +import React, { Component } from 'react'; +import { track } from '../../helpers/events'; +import { storeItem, getItemFromStorage } from '../../helpers/localStorage'; +import { + Overview, + Centralized, + CommunitySearch, + Chat, + Sell, + Yours, + PageFooter, +} from './view'; +import { Wrapper } from './style'; + +class Splash extends Component { + state: { + preferredSigninMethod: string, + }; + + constructor() { + super(); + + const preferredSigninMethod = getItemFromStorage('preferred_signin_method'); + this.state = { + preferredSigninMethod, + }; + } + + componentDidMount() { + track('homepage', 'viewed', null); + } + + trackSignin = (type, method) => { + track('homepage', 'logged in', type); + storeItem('preferred_signin_method', method); + }; + + render() { + const { preferredSigninMethod } = this.state; + + return ( + + + + + + + + + + ); + } +} +export default Splash; diff --git a/src/views/splash/style.js b/src/views/splash/style.js new file mode 100644 index 0000000000..6899aa7451 --- /dev/null +++ b/src/views/splash/style.js @@ -0,0 +1,316 @@ +import styled from 'styled-components'; +import { Button } from '../../components/buttons'; +import { + H2, + FlexCol, + FlexRow, + P, + Transition, + Shadow, + zIndex, + hexa, +} from '../../components/globals'; + +export const Wrapper = styled(FlexCol)` + flex: 1 0 auto; + width: 100%; + background-color: ${({ theme }) => theme.bg.default}; + overflow: auto; + overflow-x: hidden; + z-index: ${zIndex.base}; +`; + +export const Flexer = styled(FlexRow)` + flex-wrap: wrap; + + @media (max-width: 768px) { + flex-direction: column; + } +`; + +export const Header = styled(FlexRow)` + padding: 32px; + justify-content: space-between; + z-index: ${zIndex.card} +`; + +export const Content = styled(FlexRow)` + flex: auto; + align-self: stretch; + align-items: center; + justify-content: center; + position: relative; + + @media (max-width: 768px) { + flex-direction: column; + } +`; + +export const Tagline = styled(H2)` + font-weight: 900; + font-size: 32px; + margin-top: 8px; + margin-bottom: 8px; + color: inherit; + + @media (max-width: 768px) { + margin-bottom: 32px; + } +`; + +export const Copy = styled(P)` + max-width: 480px; + width: 100%; + font-size: 16px; + line-height: 1.5; + color: inherit; + font-weight: 500; + + &:not(:first-of-type){ + margin-top: 16px; + } + + @media (max-width: 768px) { + text-align: left; + } +`; + +export const Bullets = styled(FlexRow)` + align-self: stretch; + flex: auto; + justify-content: center; + align-items: flex-start; + margin: 32px 16px 16px; + flex-wrap: wrap; + + @media (max-width: 768px) { + flex-direction: column; + margin-top: 0; + } +`; + +export const Bullet = styled(FlexCol)` + display: inline-block; + width: calc(33% - 64px); + min-width: 320px; + max-width: 480px; + margin: 32px; + margin-bottom: 0; + + @media (max-width: 768px) { + width: 100%; + margin: 0; + margin-top: 48px; + } +`; + +export const BulletHeading = styled(FlexRow)` + align-items: center; + white-space: nowrap; + position: relative; +`; + +export const BulletTitle = styled.h2` + font-size: 20px; + font-weight: 700; +`; + +export const BulletCopy = styled.p` + margin-top: 8px; + font-weight: 500; +`; + +export const PrimaryCTA = styled(Button)` + padding: 8px 12px; + font-weight: 700; + font-size: 16px; + border-radius: 12px; + background-color: ${props => props.theme.bg.default}; + background-image: none; + color: ${props => props.theme.brand.alt}; + transition: ${Transition.hover.off}; + z-index: ${zIndex.card}; + + &:hover { + background-color: ${props => props.theme.bg.default}; + color: ${props => props.theme.brand.default}; + box-shadow: ${Shadow.high} ${props => hexa(props.theme.bg.reverse, 0.5)}; + transition: ${Transition.hover.on}; + } +`; + +export const SecondaryCTA = styled(PrimaryCTA)` + color: ${props => props.theme.text.reverse}; + background-color: transparent; + border: 2px solid transparent; + + &:hover { + color: ${props => props.theme.text.reverse}; + background-color: transparent; + border-color: ${props => props.theme.bg.default}; + box-shadow: 0 0 8px 4px ${props => hexa(props.theme.bg.default, 0.5)}; + } +`; + +export const SignInButton = styled.a` + display: flex; + flex-shrink: 1; + z-index: ${zIndex.base + 1}; + flex-direction: flex-row; + align-self: flex-start; + align-items: center; + color: ${({ theme }) => theme.text.reverse}; + border-radius: 8px; + padding: 8px; + padding-right: 16px; + font-size: 14px; + font-weight: 700; + transition: ${Transition.hover.off}; + position: relative; + margin: 16px 0; + + ${props => + props.after && + ` + margin: 24px 0; + + &:after { + content: 'Previously signed in with'; + position: absolute; + top: -23px; + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + opacity: 0.8; + left: 50%; + transform: translateX(-50%); + width: 100%; + text-align: center; + color: #fff; + } + `} span { + display: inline-block; + flex: 0 0 auto; + margin-top: -1px; + margin-left: 8px; + line-height: 2.45; + word-break: keep-all; + white-space: nowrap; + color: currentColor; + } + + svg { + fill: currentColor !important; + } + + &:hover { + cursor: pointer; + } +`; + +export const LoginCard = styled.div` + border-radius: 12px; + padding: 16px 0; + margin-top: 16px; + align-self: flex-start; + align-items: flex-start; +`; + +export const ButtonTwitter = styled(Button)` + background: ${props => + props.preferred ? props.theme.social.twitter.default : 'none'}; + color: ${props => + props.whitebg + ? props.theme.social.twitter.default + : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; + + &:hover { + color: ${props => + props.whitebg ? props.theme.social.twitter.default : '#fff'} + } +`; + +export const ButtonFacebook = styled(Button)` + background: ${props => + props.preferred ? props.theme.social.facebook.default : 'none'}; + color: ${props => + props.whitebg + ? props.theme.social.facebook.default + : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; + + + &:hover { + color: ${props => + props.whitebg ? props.theme.social.facebook.default : '#fff'} + } +`; + +export const ButtonGoogle = styled(Button)` + background: ${props => + props.preferred ? props.theme.social.google.default : 'none'}; + color: ${props => + props.whitebg + ? props.theme.social.google.default + : props.preferred ? '#fff' : 'rgba(255,255,255,0.8)'}; + + &:hover { + color: ${props => + props.whitebg ? props.theme.social.google.default : '#fff'} + } +`; + +export const Footer = styled(FlexRow)` + position: relative; + flex: auto; + justify-content: space-between; + padding: 24px 24px 24px 40px; + background-color: ${({ theme }) => theme.bg.reverse}; + color: ${({ theme }) => theme.text.reverse}; + + @media (max-width: 768px) { + flex-direction: column; + padding: 20px; + } +`; + +export const LinkBlock = styled.a` + display: inline-block; + margin: 0 24px; + flex: 0 0 auto; + position: relative; + + &:hover { + text-decoration: none; + } + + div { + font-size: 16px; + font-weight: 700; + padding: 12px 16px; + top: 4px; + position: relative; + text-align: center; + transition: ${Transition.hover.off}; + border-radius: 12px; + + &:hover { + background-color: ${({ theme }) => theme.bg.default}; + color: ${({ theme }) => theme.text.default}; + transition: ${Transition.hover.on}; + } + } + + @media (max-width: 768px) { + flex-direction: column; + justify-content: flex-start; + padding-bottom: 16px; + + div { + border-bottom: none; + + &:hover { + border-bottom: none; + } + } + } +`; diff --git a/src/views/splash/view.js b/src/views/splash/view.js new file mode 100644 index 0000000000..2bdf6053f3 --- /dev/null +++ b/src/views/splash/view.js @@ -0,0 +1,521 @@ +// @flow +import React from 'react'; +// @Flow Fix Me +import styled from 'styled-components'; +// @Flow Fix Me +import { Link } from 'react-router-dom'; +import { SERVER_URL } from '../../api'; +import { Button } from '../../components/buttons'; +import { Logo } from '../../components/logo'; +import Icon from '../../components/icons'; +import { + Shadow, + hexa, + Gradient, + FlexCol, + FlexRow, +} from '../../components/globals'; +import Search from '../explore/components/search'; + +import Theme from './components/themes'; +import { Conversation, Discover } from './components/illustrations'; +import { + Header, + Tagline, + Copy, + Bullets, + Bullet, + BulletHeading, + BulletTitle, + BulletCopy, + LinkBlock, + Footer, + Flexer, + PrimaryCTA, + SecondaryCTA, + Content, +} from './style'; + +// const Link = styled.a``; + +const Section = props => + + {props.children} + ; + +export const Overview = props => { + const Text = styled(FlexCol)` + margin: 60px 16px 120px 16px; + text-align: center; + align-items: center; + + @media (max-width: 768px) { + margin-bottom: 16px; + } + `; + + const ThisCopy = styled(Copy)` + font-size: 20px; + line-height: 1.3; + font-weight: 500; + opacity: 0.95; + + @media (max-width: 768px) { + font-size: 20px; + text-align: center; + } + `; + + const ThisTagline = styled(Tagline)` + margin-bottom: 16px; + font-size: 40px; + `; + + const Actions = styled(Flexer)` + margin-top: 48px; + justify-content: space-between; + `; + + const ThisSecondaryCTA = styled(SecondaryCTA)` + margin-left: 16px; + font-size: 16px; + border: 2px solid ${props => props.theme.text.reverse}; + + @media (max-width: 768px) { + margin-left: 0; + margin-top: 16px; + } + `; + + const ThisPrimaryCTA = styled(PrimaryCTA)` + font-size: 16px; + `; + + const ThisButton = styled(SecondaryCTA)` + padding: 12px 16px; + border: 2px solid ${props => props.theme.text.reverse}; + `; + + return ( +
+
+ + + Log in + +
+ + + Build better communities + + Spectrum makes it easy to create and grow your online community. + + + + + Create a community + + + + + Find communities + + + + + +
+ ); +}; + +export const Centralized = props => { + const ThisContent = styled(Content)` + img { + margin: 24px 0; + } + `; + + const Text = styled(FlexCol)` + margin: 40px 16px 64px; + + @media (max-width: 768px) { + margin-top: 20px; + margin-bottom: 44px; + } + `; + + const ThisCopy = styled(Copy)` + margin-top: 16px; + `; + + const ThisPrimaryCTA = styled(PrimaryCTA)` + margin-top: 32px; + background-color: ${props => props.theme.brand.alt}; + background-image: ${props => + Gradient(props.theme.brand.alt, props.theme.brand.default)}; + color: ${props => props.theme.text.reverse}; + + &:hover { + color: ${props => props.theme.text.reverse}; + } + `; + + const Actions = styled.div` + @media (max-width: 768px) { + display: flex; + justify-content: center; + } + `; + + const ThisTagline = styled(Tagline)` + @media (max-width: 768px) { + margin-bottom: 0; + } + `; + + return ( +
+ + + + Discoverable by default + + People shouldn't have to hunt down an email invite or search through + a help center to find your community. + + + When it's built on Spectrum, people can find your community + organically with search, curation, and through other community + members. + + + + + Check out our top communities + + + + + +
+ ); +}; + +export const CommunitySearch = props => { + const ThisContent = styled(Content)` + flex-direction: column; + width: 640px; + align-content: center; + align-self: center; + margin-top: 40px; + margin-bottom: 40px; + padding: 16px; + + @media (max-width: 640px) { + margin-top: 80px; + margin-bottom: 0; + width: 100%; + } + `; + + const ThisTagline = styled(Tagline)` + margin-bottom: 16px; + `; + + const ThisCopy = styled(Copy)` + font-size: 18px; + margin-bottom: 32px; + font-weight: 500; + text-align: center; + max-width: 640px; + + @media (max-width: 768px) { + text-align: left; + } + `; + + return ( +
+ + Find a community for you! + + Try searching for topics like "crypto" or for products like "React"! + + + +
+ ); +}; + +export const Chat = props => { + const ThisContent = styled(Content)` + overflow: hidden; + margin: 40px 16px; + + @media (max-width: 768px) { + margin-bottom: 0; + } + `; + + const ThisCopy = styled(Copy)` + margin-top: 16px; + `; + + const ThisPrimaryCTA = styled(PrimaryCTA)` + background-color: ${props => props.theme.brand.alt}; + background-image: ${props => + Gradient(props.theme.brand.alt, props.theme.brand.default)}; + color: ${props => props.theme.text.reverse}; + margin-top: 32px; + + &:hover { + color: ${props => props.theme.text.reverse}; + } + `; + + const Actions = styled.div` + @media (max-width: 768px) { + display: flex; + justify-content: center; + } + `; + + const ThisTagline = styled(Tagline)` + @media (max-width: 768px) { + margin-bottom: 0; + } + `; + + return ( +
+ + + + Real-time messaging with long-term value + + Conversations on Spectrum are real-time chat, just like your + favorite messaging app. But on Spectrum, conversations continue to + provide value to more and more people over time. + + + Every conversation gets a unique link to make it easy for people to + discover, share, or save for later. + + + + + + View a live conversation + + + + + +
+ ); +}; + +export const Sell = props => { + const Text = styled(FlexCol)` + align-items: center; + margin: 40px 0; + `; + + const ThisContent = styled(Content)` + + `; + + const ThisTagline = styled(Tagline)` + margin-bottom: 0; + margin-left: 16px; + margin-right: 16px; + `; + + const Actions = styled(Flexer)` + margin-bottom: 48px; + justify-content: center; + `; + + const ThisSecondaryCTA = styled(SecondaryCTA)` + margin-left: 16px; + + @media (max-width: 768px) { + margin-left: 0; + margin-top: 16px; + } + `; + + const ThisSection = styled(Section)` + margin-bottom: 40px; + `; + + return ( + + + + Spectrum saves you time and money + + + + Supercharge support + + + Stop wasting time with endless private customer support threads + answering the same question over and over. + + + Now your team can have conversations with your community as a + whole and chat privately when a particular issue is sensitive. + + + + + Bring people together + + + Spectrum gives your top supporters and advocates a place to + share their knowledge, empower others, and foster a place of + belonging for everyone. + + + + + Tighten your feedback loop + + + There's no better feedback than the insights that come directly + from your customers. + + + Think of Spectrum as a new direct line to discovering what your + audience wants the most. + + + + + + + + + Start building your community + + + + + ); +}; + +export const Yours = props => { + const ThisCopy = styled(Copy)` + max-width: 640px; + margin-top: 16px; + `; + + const ThisContent = styled(Content)` + margin: 60px 16px 40px; + font-size: 18px; + align-items: center; + text-align: left; + `; + + const ThisPrimaryCTA = styled(PrimaryCTA)` + background-color: ${props => props.theme.brand.alt}; + background-image: ${props => + Gradient(props.theme.brand.alt, props.theme.brand.default)}; + color: ${props => props.theme.text.reverse}; + + &:hover { + color: ${props => props.theme.text.reverse}; + } + `; + + const ThisSecondaryCTA = styled(SecondaryCTA)` + margin-left: 16px; + background-color: transparent; + color: ${props => props.theme.brand.alt}; + border-color: ${props => props.theme.brand.alt}; + + &:hover { + border-color: ${props => props.theme.brand.alt}; + color: ${props => props.theme.brand.alt}; + box-shadow: 0 0 8px ${props => hexa(props.theme.brand.alt, 1)}; + } + + > div { + top: -1px; + } + + @media (max-width: 768px) { + margin-left: 0; + margin-top: 16px; + } + `; + + const Actions = styled(Flexer)` + margin-top: 32px; + justify-content: flex-start; + + > a { + display: inline-block; + } + + @media (max-width: 768px) { + justify-content: center; + } + `; + + return ( +
+ + + All your communities in one place + + Before Spectrum, participating in online communities meant joining + multiple platforms, remembering different logins, and managing + endless notifications. + + + On Spectrum you only have one account, and the conversations that + matter come to you in one simple feed — no matter which + community they're in. + + + + + Create a community + + + + + Find communities + + + + + +
+ ); +}; + +export const PageFooter = props => { + return ( + + ); +}; diff --git a/src/views/userSettings/components/emailSettings.js b/src/views/userSettings/components/emailSettings.js index 8cd8dc0051..710f4ed504 100644 --- a/src/views/userSettings/components/emailSettings.js +++ b/src/views/userSettings/components/emailSettings.js @@ -67,7 +67,6 @@ class EmailSettings extends Component { render() { const { currentUser: { settings: { notifications } } } = this.props; const settings = parseNotificationTypes(notifications); - console.log('settings', settings); return (