├── .nvmrc ├── .dockerignore ├── .github └── mockup.png ├── public ├── favicon.png ├── emailAssets │ ├── logo.png │ ├── appStore.png │ ├── moutains.png │ ├── playStore.png │ ├── resortLogos │ │ ├── boreal.png │ │ ├── donner.png │ │ ├── sierra.png │ │ ├── squaw.png │ │ ├── diamond.png │ │ ├── heavenly.png │ │ ├── hevenly.png │ │ ├── homewood.png │ │ ├── kirkwood.png │ │ ├── mt-rose.png │ │ ├── northstar.png │ │ ├── palisades.png │ │ └── sugarbowl.png │ └── weatherIcons │ │ ├── clear.png │ │ ├── rain.png │ │ ├── sleet.png │ │ ├── snow.png │ │ ├── sunny.png │ │ ├── cloudy.png │ │ ├── v2 │ │ ├── fog.png │ │ ├── hail.png │ │ ├── rain.png │ │ ├── snow.png │ │ ├── wind.png │ │ ├── cloudy.png │ │ ├── sleet.png │ │ ├── tornado.png │ │ ├── clear-day.png │ │ ├── clear-night.png │ │ ├── thunderstorm.png │ │ ├── partly-cloudy-day.png │ │ └── partly-cloudy-night.png │ │ └── thunderstorm.png ├── index.html └── images │ ├── twitterLogo.svg │ └── resorts │ ├── palisades.svg │ ├── squaw.svg │ ├── homewood.svg │ ├── boreal.svg │ ├── donner.svg │ ├── heavenly.svg │ ├── northstar.svg │ ├── sierra.svg │ ├── diamond.svg │ └── kirkwood.svg ├── static.json ├── src ├── components │ ├── Main │ │ ├── Snow.js │ │ ├── Map.css │ │ ├── Snow.css │ │ ├── Main.css │ │ ├── MapPin.js │ │ ├── Main.js │ │ └── Map.js │ ├── SideNav │ │ ├── Snow.js │ │ ├── Snow.css │ │ ├── SideNav.css │ │ ├── ResortNavCard.css │ │ ├── ResortNavCard.js │ │ └── SideNav.js │ ├── HomeButton │ │ ├── HomeButton.js │ │ └── HomeButton.css │ ├── ResortInfoCard │ │ ├── ProgressBar.css │ │ ├── images │ │ │ ├── back.svg │ │ │ ├── pin.svg │ │ │ ├── tornado.svg │ │ │ ├── fog.svg │ │ │ ├── wind.svg │ │ │ ├── cloudy.svg │ │ │ ├── partly-cloudy-night.svg │ │ │ ├── clear-night.svg │ │ │ ├── snow.svg │ │ │ ├── rain.svg │ │ │ ├── thunderstorm.svg │ │ │ ├── snow-white.svg │ │ │ └── hail.svg │ │ ├── ProgressBar.js │ │ ├── ResortInfoCard.css │ │ └── ResortInfoCard.js │ ├── OpenSourceBanner │ │ ├── OpenSourceBanner.css │ │ ├── OpenSourceBanner.js │ │ └── githubLogo.svg │ ├── WeatherBanner │ │ ├── WeatherBanner.css │ │ ├── WeatherBanner.js │ │ ├── snowflake.svg │ │ └── snowman.svg │ ├── FourOhFour │ │ ├── FourOhFour.js │ │ └── FourOhFour.css │ ├── HighwayIcon │ │ ├── IncidentIcon.js │ │ ├── AmbiguousIcon.js │ │ └── HighwayIcon.js │ ├── Footer │ │ ├── Footer.css │ │ ├── getSmartLogo.js │ │ ├── heart.svg │ │ ├── Footer.js │ │ ├── logo.svg │ │ └── holidayLogo.svg │ ├── TwitterCard │ │ ├── TwitterCard.js │ │ └── TwitterCard.css │ ├── ResortInfoCardContent │ │ ├── ResortInfoCardTextInfo.css │ │ ├── Chains.css │ │ ├── ResortInfoCardTextInfo.js │ │ ├── ResortInfoCardIconInfo.css │ │ ├── Chains.js │ │ └── ResortInfoCardIconInfo.js │ ├── App │ │ ├── App.css │ │ └── App.js │ ├── Privacy │ │ └── Privacy.css │ ├── FlippableCard │ │ ├── FlippableCard.css │ │ └── FlippableCard.js │ ├── AppDownloadCard │ │ ├── AppDownloadCard.css │ │ └── AppDownloadCard.js │ └── EmailSignup │ │ └── EmailSignup.js ├── forms │ └── newsletterSubscription.js ├── util │ └── validators.js ├── actions │ ├── userSession.js │ ├── resorts.js │ └── newsletterSubscription.js ├── ScrollToTop.js ├── index.css ├── reducers │ ├── userSession.js │ ├── resorts.js │ └── newsletterSubscription.js ├── api.js ├── withTracker.js └── index.js ├── .gitignore ├── .eslintrc ├── Dockerfile ├── fly.toml ├── LICENSE.md ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 14.2.0 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .env 3 | .env.prod 4 | *Dockerfile* 5 | *docker-compose* 6 | node_modules -------------------------------------------------------------------------------- /.github/mockup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/.github/mockup.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/emailAssets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/logo.png -------------------------------------------------------------------------------- /public/emailAssets/appStore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/appStore.png -------------------------------------------------------------------------------- /public/emailAssets/moutains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/moutains.png -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "build/", 3 | "clean_urls": false, 4 | "routes": { 5 | "/**": "index.html" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /public/emailAssets/playStore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/playStore.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/boreal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/boreal.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/donner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/donner.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/sierra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/sierra.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/squaw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/squaw.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/clear.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/rain.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/sleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/sleet.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/snow.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/sunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/sunny.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/diamond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/diamond.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/heavenly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/heavenly.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/hevenly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/hevenly.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/homewood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/homewood.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/kirkwood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/kirkwood.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/mt-rose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/mt-rose.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/cloudy.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/fog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/fog.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/hail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/hail.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/rain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/rain.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/snow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/snow.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/wind.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/wind.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/northstar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/northstar.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/palisades.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/palisades.png -------------------------------------------------------------------------------- /public/emailAssets/resortLogos/sugarbowl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/resortLogos/sugarbowl.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/cloudy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/cloudy.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/sleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/sleet.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/tornado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/tornado.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/thunderstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/thunderstorm.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/clear-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/clear-day.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/clear-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/clear-night.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/thunderstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/thunderstorm.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/partly-cloudy-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/partly-cloudy-day.png -------------------------------------------------------------------------------- /public/emailAssets/weatherIcons/v2/partly-cloudy-night.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slopeninja/slopeninja-frontend/HEAD/public/emailAssets/weatherIcons/v2/partly-cloudy-night.png -------------------------------------------------------------------------------- /src/components/Main/Snow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Snow.css'; 3 | 4 | const Snow = () => ( 5 |
6 | ); 7 | export default Snow; 8 | -------------------------------------------------------------------------------- /src/components/SideNav/Snow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Snow.css'; 3 | 4 | const Snow = () => ( 5 |
6 | ); 7 | export default Snow; 8 | -------------------------------------------------------------------------------- /src/forms/newsletterSubscription.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | email: '', 3 | }; 4 | 5 | const newsletterSubscription = { 6 | ...initialState, 7 | }; 8 | 9 | export default newsletterSubscription; 10 | -------------------------------------------------------------------------------- /src/util/validators.js: -------------------------------------------------------------------------------- 1 | import emailValidator from 'email-validator'; 2 | 3 | export const isNotEmpty = (value) => value && value.length > 0; 4 | export const isValidEmail = (value) => isNotEmpty(value) && emailValidator.validate(value); 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /src/components/HomeButton/HomeButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import './HomeButton.css'; 4 | 5 | const BackButton = () => ( 6 | 7 | HOME 8 | 9 | ); 10 | export default BackButton; 11 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/ProgressBar.css: -------------------------------------------------------------------------------- 1 | .ProgressBar-box { 2 | width: 122px; 3 | height: 22px; 4 | border: 1px solid #4A4A4A; 5 | padding: 1px; 6 | box-sizing: border-box; 7 | } 8 | 9 | .ProgressBar-progress { 10 | display: block; 11 | height: 100%; 12 | transition: width 0.5s; 13 | background-color: #4A4A4A; 14 | } 15 | -------------------------------------------------------------------------------- /src/actions/userSession.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_SHOW_NEWSLETTER_SUBSCRIPTION, 3 | } from '../reducers/userSession'; 4 | 5 | export const setShowNewsletterSubscription = (subscriberEmail) => (dispatch) => { 6 | dispatch({ 7 | type: SET_SHOW_NEWSLETTER_SUBSCRIPTION, 8 | payload: { 9 | subscriberEmail, 10 | }, 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 5 | "react/prop-types": "off", 6 | "import/prefer-default-export": "off", 7 | "react/jsx-props-no-spreading": "off", 8 | "react/destructuring-assignment": "off" 9 | }, 10 | "env": { 11 | "jasmine": true, 12 | "browser": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ScrollToTop.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { withRouter } from 'react-router'; 3 | 4 | class ScrollToTop extends Component { 5 | componentDidUpdate(prevProps) { 6 | if (this.props.location !== prevProps.location) { 7 | window.scrollTo(0, 0); 8 | } 9 | } 10 | 11 | render() { 12 | return this.props.children; 13 | } 14 | } 15 | 16 | export default withRouter(ScrollToTop); 17 | -------------------------------------------------------------------------------- /src/components/HomeButton/HomeButton.css: -------------------------------------------------------------------------------- 1 | .HomeButton-wrapper { 2 | background-color: #4A4A4A; 3 | width: 100%; 4 | min-height: 40px; 5 | display: flex; 6 | align-items: center; 7 | } 8 | .HomeButton-text { 9 | color: #ffffff; 10 | font-weight: 300; 11 | margin-left: 8px; 12 | } 13 | 14 | @media only screen and (min-width: 768px) { 15 | .HomeButton-wrapper { 16 | } 17 | } 18 | 19 | @media only screen and (min-width: 1200px) { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/components/OpenSourceBanner/OpenSourceBanner.css: -------------------------------------------------------------------------------- 1 | .OpenSourceBanner-wrapper { 2 | display: none; 3 | } 4 | 5 | @media only screen and (min-width: 575px) { 6 | .OpenSourceBanner-wrapper { 7 | background-color: #4CB950; 8 | width: 100%; 9 | min-height: 40px; 10 | display: flex; 11 | align-items: center; 12 | /*margin-bottom: 0.5pc;*/ 13 | } 14 | .OpenSourceBanner-text { 15 | color: #fff; 16 | font-weight: 400; 17 | text-align: center; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :focus:not(.focus-ring) { 2 | outline: none; 3 | } 4 | 5 | .focus-ring { 6 | outline: 2px solid #1ed2ff; 7 | } 8 | 9 | body { 10 | font-family: 'Lato', sans-serif; 11 | font-weight: 400; 12 | margin: 0; 13 | padding: 0; 14 | } 15 | 16 | figure { 17 | padding: 0; 18 | margin: 0; 19 | } 20 | 21 | h1, h2, h3, h4, h5 { 22 | margin: 0; 23 | padding: 0; 24 | color: #4A4A4A; 25 | } 26 | 27 | a, span, p { 28 | text-decoration: none; 29 | color: #4A4A4A; 30 | } 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.2.0-alpine as builder 2 | 3 | WORKDIR /app 4 | 5 | # install all dependencies 6 | COPY package.json . 7 | COPY yarn.lock . 8 | RUN yarn install --frozen-lockfile 9 | 10 | COPY . . 11 | RUN yarn build 12 | 13 | FROM node:14.2.0-alpine 14 | 15 | WORKDIR /app 16 | 17 | # install prod dependencies 18 | COPY package.json . 19 | COPY yarn.lock . 20 | RUN yarn install --production --frozen-lockfile 21 | 22 | COPY --from=builder /app/build ./build 23 | 24 | EXPOSE 3001 25 | 26 | CMD [ "yarn", "start" ] 27 | -------------------------------------------------------------------------------- /src/components/WeatherBanner/WeatherBanner.css: -------------------------------------------------------------------------------- 1 | .WeatherBanner-wrapper { 2 | background-color: #1ed2ff; 3 | width: 100%; 4 | min-height: 40px; 5 | display: flex; 6 | align-items: center; 7 | /*margin-bottom: 0.5pc;*/ 8 | } 9 | .WeatherBanner-text { 10 | color: #ffffff; 11 | font-weight: 400; 12 | padding: 0.5pc 0.5pc 0.5pc 0; 13 | } 14 | 15 | @media only screen and (min-width: 768px) { 16 | .WeatherBanner-wrapper { 17 | /*margin-bottom: 0;*/ 18 | } 19 | } 20 | 21 | @media only screen and (min-width: 1200px) { 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/reducers/userSession.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | showEmailSignup: true, 3 | }; 4 | 5 | export const SET_SHOW_NEWSLETTER_SUBSCRIPTION = 'SET_SHOW_NEWSLETTER_SUBSCRIPTION'; 6 | 7 | const userSession = (state = initialState, action) => { 8 | if (action.type === SET_SHOW_NEWSLETTER_SUBSCRIPTION) { 9 | const { subscriberEmail } = action.payload; 10 | 11 | const newState = { 12 | ...state, 13 | showEmailSignup: false, 14 | subscriberEmail, 15 | }; 16 | return newState; 17 | } 18 | return state; 19 | }; 20 | 21 | export default userSession; 22 | -------------------------------------------------------------------------------- /src/components/FourOhFour/FourOhFour.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unescaped-entities */ 2 | import React from 'react'; 3 | import snowboarders from './snowboarders.svg'; 4 | import './FourOhFour.css'; 5 | 6 | const FourOhFour = () => ( 7 |
8 |
9 | 404 14 |
15 | Got lost, ski bum? 16 | We don't know of such a resort. 17 |
18 | ); 19 | 20 | export default FourOhFour; 21 | -------------------------------------------------------------------------------- /src/actions/resorts.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_RESORTS, 3 | FETCH_RESORTS_SUCCESS, 4 | FETCH_RESORTS_FAIL, 5 | } from '../reducers/resorts'; 6 | 7 | import { 8 | getResorts, 9 | } from '../api'; 10 | 11 | export const fetchResorts = async (dispatch) => { 12 | dispatch({ 13 | type: FETCH_RESORTS, 14 | }); 15 | try { 16 | const resorts = await getResorts(); 17 | dispatch({ 18 | type: FETCH_RESORTS_SUCCESS, 19 | payload: { 20 | resorts, 21 | }, 22 | }); 23 | } catch (error) { 24 | dispatch({ 25 | type: FETCH_RESORTS_FAIL, 26 | payload: { 27 | error, 28 | }, 29 | }); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/HighwayIcon/IncidentIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const IncidentIcon = () => ( 4 | 5 | 6 | 14 | 18 | 19 | 20 | ); 21 | 22 | export default IncidentIcon; 23 | -------------------------------------------------------------------------------- /src/components/Main/Map.css: -------------------------------------------------------------------------------- 1 | .Map { 2 | display: none; 3 | align-self: stretch; 4 | margin-top: 5px; 5 | border: 1px solid; 6 | border-color: #EDEDED; 7 | flex: 1; 8 | min-height: 320px; 9 | background-color: #ffffff; 10 | position: relative; 11 | } 12 | 13 | /* 14 | @custom-media --sm-viewport only screen and (min-width: 48em); // 768px 15 | @custom-media --md-viewport only screen and (min-width: 64em); // 994px 16 | @custom-media --lg-viewport only screen and (min-width: 75em); / 1200px 17 | */ 18 | @media only screen and (min-width: 768px) { 19 | .Map { 20 | display: flex; 21 | } 22 | } 23 | 24 | @media only screen and (min-width: 1200px) { 25 | .Map { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/SideNav/Snow.css: -------------------------------------------------------------------------------- 1 | .snow { 2 | width: 100%; 3 | height: 140px; 4 | position: absolute; 5 | z-index: 999; 6 | overflow: hidden; 7 | } 8 | 9 | .snow:before { 10 | content: ''; 11 | background-image: 12 | url(./snowflake.svg), 13 | url(./snowflake-lg.svg); 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | right: 0; 18 | bottom: 0; 19 | z-index: 1; 20 | animation: snow 20s linear 1s infinite; 21 | } 22 | 23 | @keyframes snow{ 24 | 0%{ 25 | background-position: 0 0, -0 50px; 26 | } 27 | 50%{ 28 | background-position: 0 200px, -0 150px; 29 | } 30 | 100%{ 31 | background-position: 0 600px, -0 300px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Main/Snow.css: -------------------------------------------------------------------------------- 1 | .icon-snow { 2 | width: 40px; 3 | height: 40px; 4 | position: absolute; 5 | z-index: 999; 6 | overflow: hidden; 7 | } 8 | 9 | .icon-snow:before { 10 | content: ''; 11 | background-image: 12 | url(./snowflake.svg), 13 | url(./snowflake-sm.svg); 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | right: 0; 18 | bottom: 0; 19 | z-index: 1; 20 | animation: snow 20s linear 1s infinite; 21 | } 22 | 23 | @keyframes snow{ 24 | 0%{ 25 | background-position: 0 0, -0 50px; 26 | } 27 | 50%{ 28 | background-position: 0 200px, -0 150px; 29 | } 30 | 100%{ 31 | background-position: 0 600px, -0 300px; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.css: -------------------------------------------------------------------------------- 1 | .Footer-wrapper { 2 | display: flex; 3 | justify-content: space-between; 4 | /*align-items: center;*/ 5 | min-height: 60px; 6 | flex-direction: row; 7 | border-top: 1px solid; 8 | border-color: #EDEDED; 9 | margin-top: 5px; 10 | } 11 | 12 | .Footer-text-container { 13 | display: flex; 14 | flex: 1; 15 | justify-content: center; 16 | align-items: center; 17 | flex-direction: column; 18 | margin-left: 4pc; /* compensate for logo width+margin */ 19 | } 20 | 21 | .Footer-text { 22 | color: #9B9B9B; 23 | font-size: 14px; 24 | text-align: center; 25 | } 26 | 27 | .Footer-logo { 28 | align-self: center; 29 | margin-right: 1pc; 30 | margin-left: 1pc; 31 | } 32 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const API_URL = process.env.REACT_APP_API_URL; 2 | 3 | export const getResorts = async () => { 4 | const response = await fetch(`${API_URL}/resorts`); 5 | const data = await response.json(); 6 | return data.resorts; 7 | }; 8 | 9 | export const sendNewsletterSubscription = async (email) => { 10 | const response = await fetch(`${API_URL}/subscribers`, { 11 | method: 'POST', 12 | headers: 13 | new Headers({ 14 | 'content-type': 'application/json', 15 | }), 16 | body: JSON.stringify({ 17 | email, 18 | }), 19 | }); 20 | const data = await response.json(); 21 | 22 | if (response.status !== 200) { 23 | throw new Error(data.error); 24 | } 25 | 26 | return data; 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/TwitterCard/TwitterCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import twitterLogo from '../../../public/images/twitterLogo.svg'; 3 | 4 | import './TwitterCard.css'; 5 | 6 | const TwitterCard = () => ( 7 |
8 | 9 | download from Google Play 10 | 11 | {`${'Follow '}`} 12 | @slopeninja 13 | {`${' on Twitter for snow updates.'}`} 14 | 15 | 16 |
17 | ); 18 | 19 | export default TwitterCard; 20 | -------------------------------------------------------------------------------- /src/components/ResortInfoCardContent/ResortInfoCardTextInfo.css: -------------------------------------------------------------------------------- 1 | .ResortInfoBox { 2 | min-height: 160px; 3 | height: 100%; 4 | width: 100%; 5 | border: 1px solid #EDEDED; 6 | background-color: #FFFFFF; 7 | padding: 1pc; 8 | box-sizing: border-box; 9 | z-index: 100; 10 | } 11 | .ResortInfoBody-title { 12 | font-size: 24px; 13 | } 14 | 15 | .ResortInfoBody-content { 16 | font-weight: 300; 17 | font-size: 24px; 18 | display: flex; 19 | flex-direction: column; 20 | margin-top: 1pc; 21 | } 22 | @media only screen and (min-width: 1200px) { 23 | 24 | .ResortInfoBox { 25 | border: none; 26 | padding: 0.5pc; 27 | } 28 | .ResortInfoBody-title { 29 | font-size: 24px; 30 | } 31 | .ResortInfoBody-content { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for slope-ninja-web on 2021-10-16T13:07:17-07:00 2 | 3 | app = "slope-ninja-web" 4 | 5 | kill_signal = "SIGINT" 6 | kill_timeout = 5 7 | processes = [] 8 | 9 | [env] 10 | 11 | [experimental] 12 | allowed_public_ports = [] 13 | cmd = [] 14 | entrypoint = [] 15 | exec = [] 16 | 17 | [[services]] 18 | internal_port = 3001 19 | processes = [] 20 | protocol = "tcp" 21 | script_checks = [] 22 | 23 | [services.concurrency] 24 | hard_limit = 1000 25 | soft_limit = 500 26 | type = "connections" 27 | 28 | [[services.ports]] 29 | handlers = ["http"] 30 | port = 80 31 | 32 | [[services.ports]] 33 | handlers = ["tls", "http"] 34 | port = 443 35 | 36 | [[services.tcp_checks]] 37 | grace_period = "1s" 38 | interval = "10s" 39 | restart_limit = 0 40 | timeout = "2s" 41 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Footer/getSmartLogo.js: -------------------------------------------------------------------------------- 1 | import logoHoliday from './holidayLogo.svg'; 2 | import logo from './logo.svg'; 3 | 4 | const isHolidays = () => { 5 | const now = new Date(); 6 | const currentMonth = now.getMonth(); 7 | let holidayStart; 8 | let holidayEnd; 9 | 10 | if (currentMonth < 10) { 11 | holidayStart = new Date(now.getFullYear() - 1, 10, 15); 12 | holidayEnd = new Date(now.getFullYear(), 0, 5); 13 | } else { 14 | holidayStart = new Date(now.getFullYear(), 10, 15); 15 | holidayEnd = new Date(now.getFullYear() + 1, 0, 5); 16 | } 17 | 18 | if (holidayStart < now && now < holidayEnd) { 19 | return true; 20 | } 21 | 22 | return false; 23 | }; 24 | 25 | const getSmartLogo = () => { 26 | if (isHolidays()) { 27 | return logoHoliday; 28 | } 29 | 30 | return logo; 31 | }; 32 | 33 | export default getSmartLogo; 34 | -------------------------------------------------------------------------------- /src/components/Footer/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 3 Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/Main/Main.css: -------------------------------------------------------------------------------- 1 | .Main-wrapper { 2 | display: flex; 3 | align-self: stretch; 4 | flex-direction: column; 5 | overflow-y: scroll; 6 | overflow-x: hidden; 7 | flex: 1; 8 | /* resetting z-index here fixes a bug where 9 | * scrollbar renders behind content 10 | */ 11 | z-index: 0; 12 | } 13 | 14 | .Main-hideOnMobile { 15 | display: none; 16 | } 17 | 18 | /* 19 | @custom-media --sm-viewport only screen and (min-width: 48em); // 768px 20 | @custom-media --md-viewport only screen and (min-width: 64em); // 994px 21 | @custom-media --lg-viewport only screen and (min-width: 75em); / 1200px 22 | */ 23 | @media only screen and (min-width: 768px) { 24 | .Main-wrapper { 25 | display: flex; 26 | margin-left: 6px; 27 | padding-top: 6px; 28 | } 29 | } 30 | 31 | @media only screen and (min-width: 1200px) { 32 | .Main-wrapper { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Main/MapPin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/prefer-stateless-function */ 2 | import React, { Component } from 'react'; 3 | import { 4 | Link, 5 | } from 'react-router-dom'; 6 | import Snow from './Snow'; 7 | 8 | class MapPin extends Component { 9 | render() { 10 | let snow; 11 | if (this.props.resort.weather.condition === 'snow') { 12 | snow = (); 13 | } 14 | return ( 15 | 19 | {snow} 20 | Map Pin 29 | 30 | ); 31 | } 32 | } 33 | 34 | export default MapPin; 35 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/ProgressBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import './ProgressBar.css'; 4 | 5 | const ProgressBar = ({ small, progress }) => { 6 | let progressBarStyle; 7 | let progressBar = null; 8 | if (small) { 9 | progressBarStyle = { 10 | height: '14px', 11 | width: '78px', 12 | }; 13 | } 14 | if (small && progress === undefined) { 15 | return -; 16 | } 17 | 18 | const width = progress >= 0 ? Math.min(progress, 100) : 0; 19 | 20 | if (progress !== undefined) { 21 | progressBar = ( 22 | 23 | 29 | 30 | ); 31 | } 32 | return progressBar; 33 | }; 34 | 35 | export default ProgressBar; 36 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | .App-wrapper { 2 | height: 100%; 3 | min-height: 100vh; 4 | display: flex; 5 | flex-direction: column; 6 | -webkit-overflow-scrolling: touch; 7 | } 8 | 9 | .App-content { 10 | display: flex; 11 | align-items: center; 12 | justify-content: center; 13 | min-height: 0; 14 | flex: 1; 15 | } 16 | 17 | /* 18 | @custom-media --sm-viewport only screen and (min-width: 48em); // 768px 19 | @custom-media --md-viewport only screen and (min-width: 64em); // 994px 20 | @custom-media --lg-viewport only screen and (min-width: 75em); / 1200px 21 | */ 22 | @media only screen and (min-width: 768px) { 23 | .App-wrapper { 24 | height: 100vh; 25 | } 26 | 27 | .App-content { 28 | align-items: flex-start; 29 | justify-content: flex-start; 30 | } 31 | } 32 | 33 | @media only screen and (min-width: 1200px) { 34 | .App-wrapper { 35 | } 36 | 37 | .App-content { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/components/Privacy/Privacy.css: -------------------------------------------------------------------------------- 1 | .Privacy-container { 2 | flex: 1; 3 | } 4 | 5 | .Privacy-content h1 { 6 | font-size: 36px; 7 | margin-top: 2pc; 8 | margin-bottom: 2pc; 9 | font-weight: 400; 10 | } 11 | 12 | .Privacy-content h2 { 13 | font-size: 30px; 14 | margin-top: 2pc; 15 | margin-bottom: 2pc; 16 | font-weight: 300; 17 | } 18 | 19 | .Privacy-content h3 { 20 | font-size: 24px; 21 | margin-top: 2pc; 22 | margin-bottom: 2pc; 23 | font-weight: 300; 24 | } 25 | 26 | .Privacy-content p { 27 | font-size: 18px; 28 | margin-top: 1pc; 29 | margin-bottom: 1pc; 30 | line-height: 26px; 31 | font-weight: 300; 32 | } 33 | 34 | .Privacy-content a { 35 | color: #1ed2ff; 36 | } 37 | 38 | .Privacy-content-subtitle { 39 | display: block; 40 | padding-bottom: 1pc; 41 | font-weight: 400; 42 | } 43 | 44 | .Privacy-content ul { 45 | margin: 0; 46 | } 47 | 48 | .Privacy-content ul li { 49 | padding-bottom: 1pc; 50 | } 51 | -------------------------------------------------------------------------------- /src/components/SideNav/SideNav.css: -------------------------------------------------------------------------------- 1 | .SideNav-wrapper { 2 | width: 366px; 3 | overflow-y: scroll; 4 | flex: 1; 5 | display: flex; 6 | align-self: stretch; 7 | flex-direction: column; 8 | margin-top: 1pc; 9 | } 10 | 11 | .SideNav-hideOnMobile { 12 | display: none; 13 | } 14 | 15 | /* 16 | @custom-media --sm-viewport only screen and (min-width: 48em); // 768px 17 | @custom-media --md-viewport only screen and (min-width: 64em); // 994px 18 | @custom-media --lg-viewport only screen and (min-width: 75em); / 1200px 19 | */ 20 | @media only screen and (min-width: 768px) { 21 | .SideNav-wrapper { 22 | display: flex; 23 | margin-left: 1pc; 24 | padding-top: 6px; 25 | margin-top: 0; 26 | padding-right: 6px; 27 | flex: none; 28 | } 29 | 30 | .SideNav-hideOnMobile { 31 | display: flex; 32 | } 33 | } 34 | 35 | @media only screen and (min-width: 994px) { 36 | } 37 | 38 | @media only screen and (min-width: 1200px) { 39 | } 40 | -------------------------------------------------------------------------------- /src/withTracker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import GoogleAnalytics from 'react-ga'; 3 | 4 | GoogleAnalytics.initialize('UA-98636451-1'); 5 | 6 | const withTracker = (WrappedComponent, options = {}) => { 7 | const trackPage = (page) => { 8 | GoogleAnalytics.set({ 9 | page, 10 | ...options, 11 | }); 12 | GoogleAnalytics.pageview(page); 13 | }; 14 | 15 | const HOC = class extends Component { 16 | componentDidMount() { 17 | const page = this.props.location.pathname; 18 | trackPage(page); 19 | } 20 | 21 | componentWillReceiveProps(nextProps) { 22 | const currentPage = this.props.location.pathname; 23 | const nextPage = nextProps.location.pathname; 24 | 25 | if (currentPage !== nextPage) { 26 | trackPage(nextPage); 27 | } 28 | } 29 | 30 | render() { 31 | return ; 32 | } 33 | }; 34 | 35 | return HOC; 36 | }; 37 | 38 | export default withTracker; 39 | -------------------------------------------------------------------------------- /src/reducers/resorts.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | resorts: [], 3 | resortsStatus: null, 4 | }; 5 | 6 | export const FETCH_RESORTS = 'FETCH_RESORTS'; 7 | export const FETCH_RESORTS_SUCCESS = 'FETCH_RESORTS_SUCCESS'; 8 | export const FETCH_RESORTS_FAIL = 'FETCH_RESORTS_FAIL'; 9 | 10 | function resorts(state = initialState, action) { 11 | if (action.type === FETCH_RESORTS_SUCCESS) { 12 | const newState = { 13 | ...state, 14 | resorts: action.payload.resorts, 15 | resortsStatus: 'success', 16 | }; 17 | return newState; 18 | } 19 | 20 | if (action.type === FETCH_RESORTS_FAIL) { 21 | const newState = { 22 | ...state, 23 | resorts: [], 24 | resortsStatus: 'fail', 25 | }; 26 | return newState; 27 | } 28 | 29 | if (action.type === FETCH_RESORTS) { 30 | const newState = { 31 | ...state, 32 | resortsStatus: 'fetching', 33 | }; 34 | return newState; 35 | } 36 | 37 | return state; 38 | } 39 | 40 | export default resorts; 41 | -------------------------------------------------------------------------------- /src/components/TwitterCard/TwitterCard.css: -------------------------------------------------------------------------------- 1 | .TwitterCard { 2 | height: 90px; 3 | flex: none; 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: 1pc; 7 | background-color: #FAFAFA; 8 | justify-content: center; 9 | align-items: center; 10 | padding: 1.5pc; 11 | } 12 | 13 | .TwitterCard-link { 14 | display: flex; 15 | } 16 | 17 | .TwitterCard-link-underline { 18 | text-decoration: underline; 19 | } 20 | 21 | .TwitterCard-link img { 22 | margin-right: 1.5pc; 23 | } 24 | 25 | .TwitterCard-title { 26 | font-size: 18px; 27 | font-weight: 300; 28 | } 29 | 30 | .dashed-gradient { 31 | background-image: linear-gradient(to right, #9B9B9B 50%, transparent 50%), linear-gradient(to right, #9B9B9B 50%, transparent 50%), linear-gradient(to bottom, #9B9B9B 50%, transparent 50%), linear-gradient(to bottom, #9B9B9B 50%, transparent 50%); 32 | background-position: left top, left bottom, left top, right top; 33 | background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; 34 | background-size: 20px 1px, 20px 1px, 1px 20px, 1px 20px; 35 | } 36 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Slope Ninja 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Footer.css'; 3 | import getSmartLogo from './getSmartLogo'; 4 | import heart from './heart.svg'; 5 | 6 | const Footer = () => ( 7 |
8 |
9 | 10 | © Slope Ninja. All rights reserved. Made with 11 | heart 20 | in San Francisco. Powered by Dark Sky. 21 | 22 |
23 | 24 | Slope Ninja 32 | 33 |
34 | ); 35 | export default Footer; 36 | 37 | //
38 | // Slope Ninja 39 | -------------------------------------------------------------------------------- /src/components/FlippableCard/FlippableCard.css: -------------------------------------------------------------------------------- 1 | .FlippableCard-container { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | z-index: 100; 6 | perspective: 800px; 7 | background-color: #4a4a4a; 8 | } 9 | 10 | .FlippableCard-card { 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | transform-style: preserve-3d; 15 | -webkit-transform-style: preserve-3d; 16 | } 17 | 18 | .FlippableCard-face { 19 | position: absolute; 20 | width: 100%; 21 | height: 100%; 22 | background: #fff; 23 | color: #47525d; 24 | backface-visibility: hidden; 25 | } 26 | 27 | .FlippableCard-back-vertical { 28 | -webkit-transform: rotateX(180deg); 29 | transform: rotateX(180deg); 30 | } 31 | 32 | .FlippableCard-flipped-vertical { 33 | -webkit-transform: rotateX(180deg); 34 | transform: rotateX(180deg); 35 | } 36 | 37 | .FlippableCard-back-horizontal { 38 | -webkit-transform: rotateY(180deg); 39 | transform: rotateY(180deg); 40 | } 41 | 42 | .FlippableCard-flipped-horizontal { 43 | -webkit-transform: rotateY(180deg); 44 | transform: rotateY(180deg); 45 | } 46 | -------------------------------------------------------------------------------- /src/components/AppDownloadCard/AppDownloadCard.css: -------------------------------------------------------------------------------- 1 | .AppDownloadCard { 2 | height: 178px; 3 | flex: none; 4 | display: flex; 5 | flex-direction: column; 6 | margin-bottom: 1pc; 7 | background-color: #FAFAFA; 8 | justify-content: center; 9 | align-items: center; 10 | } 11 | 12 | .AppDownloadCard-links { 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | margin-top: 1.5pc; 18 | } 19 | 20 | .AppDownloadCard-links img { 21 | margin: 0 0.5pc 0.5pc 0.5pc; 22 | } 23 | 24 | .AppDownloadCard-title { 25 | font-size: 18px; 26 | font-weight: 300; 27 | } 28 | 29 | .dashed-gradient { 30 | background-image: linear-gradient(to right, #9B9B9B 50%, transparent 50%), linear-gradient(to right, #9B9B9B 50%, transparent 50%), linear-gradient(to bottom, #9B9B9B 50%, transparent 50%), linear-gradient(to bottom, #9B9B9B 50%, transparent 50%); 31 | background-position: left top, left bottom, left top, right top; 32 | background-repeat: repeat-x, repeat-x, repeat-y, repeat-y; 33 | background-size: 20px 1px, 20px 1px, 1px 20px, 1px 20px; 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Julia Qiu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pin 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/OpenSourceBanner/OpenSourceBanner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import github from './githubLogo.svg'; 4 | import './OpenSourceBanner.css'; 5 | 6 | const OpenSourceBanner = (props) => { 7 | const snowingResort = props.resorts.find( 8 | (resort) => resort.weather.condition === 'snow', 9 | ); 10 | 11 | if (snowingResort) { 12 | return null; 13 | } 14 | 15 | return ( 16 | 22 | 32 | 33 | Slope Ninja is open source. Send a pull request on GitHub. 34 | 35 | 36 | ); 37 | }; 38 | 39 | const mapStateToProps = (state) => ({ 40 | resorts: state.app.resorts.resorts, 41 | }); 42 | 43 | const ConnectedWeatherBanner = connect( 44 | mapStateToProps, 45 | )(OpenSourceBanner); 46 | 47 | export default ConnectedWeatherBanner; 48 | -------------------------------------------------------------------------------- /src/components/AppDownloadCard/AppDownloadCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import appleStore from '../../../public/images/appleStore.svg'; 3 | import googlePlay from '../../../public/images/googlePlay.svg'; 4 | // import alexaStore from '../../../public/images/alexaStore.svg'; 5 | 6 | import './AppDownloadCard.css'; 7 | 8 | const AppDownloadCard = () => ( 9 |
10 | 11 | Download the Slope Ninja app. 12 | 13 |
14 | 22 |
23 | {/* 24 | download from Alexa Skills Store 25 | */} 26 |
27 |
28 |
29 | ); 30 | 31 | export default AppDownloadCard; 32 | -------------------------------------------------------------------------------- /src/components/HighwayIcon/AmbiguousIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* eslint-disable max-len */ 4 | const AmbiguousIcon = () => ( 5 | 6 | 7 | 8 | 15 | 23 | 24 | 25 | ); 26 | /* eslint-enable */ 27 | 28 | export default AmbiguousIcon; 29 | -------------------------------------------------------------------------------- /src/components/ResortInfoCardContent/Chains.css: -------------------------------------------------------------------------------- 1 | .ResortInfoBox { 2 | min-height: 160px; 3 | height: 100%; 4 | width: 100%; 5 | border: 1px solid #EDEDED; 6 | background-color: #FFFFFF; 7 | padding: 1pc; 8 | box-sizing: border-box; 9 | } 10 | 11 | .ResortInfoBody-content-chains { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | } 16 | 17 | .ResortInfoBody-content-chains-row { 18 | margin-top: 1pc; 19 | display: flex; 20 | flex-direction: row; 21 | min-height: 36px; 22 | } 23 | 24 | .ResortInfoBody-content-chains-link { 25 | background: none; 26 | border: none; 27 | margin: 0; 28 | padding: 0; 29 | outline: none; 30 | outline-offset: 0; 31 | /* Additional styles to look like a link */ 32 | cursor: pointer; 33 | text-decoration: underline; 34 | -webkit-appearance: none; 35 | padding-right: 1pc; 36 | font: inherit; 37 | font-size: 24px; 38 | font-weight: 300; 39 | height: 100%; 40 | } 41 | .ResortInfoBody-content-chains-text { 42 | font-size: 24px; 43 | font-weight: 300; 44 | } 45 | 46 | .ResortInfoBody-title { 47 | font-size: 24px; 48 | } 49 | 50 | @media only screen and (min-width: 1200px) { 51 | .ResortInfoBox { 52 | border: none; 53 | padding: 0.5pc; 54 | } 55 | .ResortInfoBody-content-chains-text{ 56 | font-size: 24px; 57 | } 58 | .ResortInfoBody-title { 59 | font-size: 24px; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/FourOhFour/FourOhFour.css: -------------------------------------------------------------------------------- 1 | .FourOhFour { 2 | flex: 1; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | align-self: stretch; 7 | flex-direction: column; 8 | border: 1px solid #EDEDED; 9 | margin-bottom: 0; 10 | margin-right: 0; 11 | 12 | padding-top: 2pc; 13 | padding-bottom: 2pc; 14 | 15 | background-image: 16 | -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, #f8f8f8), color-stop(.25, transparent)), 17 | -webkit-gradient(linear, 0 0, 100% 100%, color-stop(.25, #f8f8f8), color-stop(.25, transparent)), 18 | -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.75, transparent), color-stop(.75, #f8f8f8)), 19 | -webkit-gradient(linear, 0 0, 100% 100%, color-stop(.75, transparent), color-stop(.75, #f8f8f8)); 20 | background-size:100px 100px; 21 | background-position:0 0, 50px 0, 50px -50px, 0 50px; 22 | } 23 | .FourOhFour-imgbox { 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .FourOhFour-img { 30 | max-width: 90%; 31 | max-height: 50vh; 32 | } 33 | 34 | .FourOhFour-text { 35 | font-size: 24px; 36 | font-weight: 300; 37 | margin-top: 1pc; 38 | text-align: center; 39 | } 40 | 41 | @media only screen and (min-width: 768px) { 42 | .FourOhFour { 43 | padding-top: 0; 44 | padding-bottom: 0; 45 | } 46 | .FourOhFour-text { 47 | font-size: 36px; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/HighwayIcon/HighwayIcon.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react'; 3 | 4 | const HighwayIcon = ({ highwayNumber = '00', width = 54, height = 54 }) => { 5 | let fontSize = '28'; 6 | let fontWeight = '400'; 7 | let translate = 'translate(10 8)'; 8 | 9 | if (highwayNumber.length > 2) { 10 | fontSize = '20'; 11 | fontWeight = '700'; 12 | translate = 'translate(9.2 6)'; 13 | } 14 | 15 | return ( 16 | 17 | 18 | 19 | 20 | { highwayNumber } 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default HighwayIcon; 28 | -------------------------------------------------------------------------------- /src/actions/newsletterSubscription.js: -------------------------------------------------------------------------------- 1 | import { 2 | SEND_NEWSLETTER_SUBSCRIPTION, 3 | SEND_NEWSLETTER_SUBSCRIPTION_SUCCESS, 4 | SEND_NEWSLETTER_SUBSCRIPTION_FAIL, 5 | SEND_NEWSLETTER_SUBSCRIPTION_RESET, 6 | } from '../reducers/newsletterSubscription'; 7 | 8 | import { setShowNewsletterSubscription } from './userSession'; 9 | 10 | import { 11 | sendNewsletterSubscription, 12 | } from '../api'; 13 | 14 | // aciton creator 15 | const newsletterSubscription = () => ({ 16 | type: SEND_NEWSLETTER_SUBSCRIPTION, 17 | }); 18 | 19 | // aciton creator 20 | const newsletterSubscriptionSuccess = (email) => ({ 21 | type: SEND_NEWSLETTER_SUBSCRIPTION_SUCCESS, 22 | payload: { 23 | email, 24 | }, 25 | }); 26 | 27 | // aciton creator 28 | export const newsletterSubscriptionFail = (error) => ({ 29 | type: SEND_NEWSLETTER_SUBSCRIPTION_FAIL, 30 | payload: { 31 | error, 32 | }, 33 | }); 34 | 35 | // aciton creator 36 | export const newsletterSubscriptionReset = () => ({ 37 | type: SEND_NEWSLETTER_SUBSCRIPTION_RESET, 38 | }); 39 | 40 | const EMAIL_SIGNUP_DISMISS_DURATION = 800; 41 | 42 | // thunk 43 | export const createNewsletterSubscription = (email) => async (dispatch) => { 44 | dispatch(newsletterSubscription()); 45 | try { 46 | const data = await sendNewsletterSubscription(email); 47 | dispatch(newsletterSubscriptionSuccess(data.email)); 48 | 49 | setTimeout(() => { 50 | dispatch(setShowNewsletterSubscription(email)); 51 | }, EMAIL_SIGNUP_DISMISS_DURATION); 52 | } catch (error) { 53 | dispatch(newsletterSubscriptionFail(error.message)); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/reducers/newsletterSubscription.js: -------------------------------------------------------------------------------- 1 | import { STATE as BUTTON_STATE } from 'react-progress-button'; 2 | 3 | const initialState = { 4 | buttonState: BUTTON_STATE.NOTHING, 5 | email: '', 6 | newsletterSubscriptionStatus: null, 7 | }; 8 | 9 | export const SEND_NEWSLETTER_SUBSCRIPTION = 'SEND_NEWSLETTER_SUBSCRIPTION'; 10 | export const SEND_NEWSLETTER_SUBSCRIPTION_SUCCESS = 'SEND_NEWSLETTER_SUBSCRIPTION_SUCCESS'; 11 | export const SEND_NEWSLETTER_SUBSCRIPTION_FAIL = 'SEND_NEWSLETTER_SUBSCRIPTION_FAIL'; 12 | export const SEND_NEWSLETTER_SUBSCRIPTION_RESET = 'SEND_NEWSLETTER_SUBSCRIPTION_RESET'; 13 | 14 | const createNewsletterSubscription = (state = initialState, action) => { 15 | if (action.type === SEND_NEWSLETTER_SUBSCRIPTION) { 16 | const newState = { 17 | ...state, 18 | buttonState: BUTTON_STATE.LOADING, 19 | }; 20 | return newState; 21 | } 22 | 23 | if (action.type === SEND_NEWSLETTER_SUBSCRIPTION_SUCCESS) { 24 | const newState = { 25 | ...state, 26 | email: action.payload.email, 27 | buttonState: BUTTON_STATE.SUCCESS, 28 | }; 29 | return newState; 30 | } 31 | 32 | if (action.type === SEND_NEWSLETTER_SUBSCRIPTION_FAIL) { 33 | const newState = { 34 | ...state, 35 | buttonState: BUTTON_STATE.ERROR, 36 | }; 37 | return newState; 38 | } 39 | 40 | // This is a hack. See EmailSignup component for details. 41 | if (action.type === SEND_NEWSLETTER_SUBSCRIPTION_RESET) { 42 | const newState = { 43 | ...state, 44 | buttonState: BUTTON_STATE.NOTHING, 45 | }; 46 | return newState; 47 | } 48 | 49 | return state; 50 | }; 51 | 52 | export default createNewsletterSubscription; 53 | -------------------------------------------------------------------------------- /src/components/Main/Main.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import ResortInfoCard from '../ResortInfoCard/ResortInfoCard'; 5 | import Map from './Map'; 6 | import FourOhFour from '../FourOhFour/FourOhFour'; 7 | import HomeButton from '../HomeButton/HomeButton'; 8 | import './Main.css'; 9 | 10 | const LAKE_TAHOE_COORDS = { 11 | lat: 38.9967267, 12 | lng: -119.976311, 13 | }; 14 | 15 | const Main = (props) => { 16 | const { shortName } = props.match.params; 17 | 18 | const resort = props.resorts.find( 19 | (r) => r.shortName === shortName, 20 | ); 21 | 22 | // debugger; 23 | let hideMainOnMobileClassName; 24 | if (!shortName) { 25 | hideMainOnMobileClassName = 'Main-hideOnMobile'; 26 | } 27 | 28 | const className = classNames(['Main-wrapper', hideMainOnMobileClassName]); 29 | 30 | if (!shortName) { 31 | return ( 32 |
33 | 39 |
40 | ); 41 | } 42 | 43 | if (!resort) { 44 | return ( 45 |
46 | 47 |
48 | ); 49 | } 50 | 51 | return ( 52 |
53 | 54 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | const mapStateToProps = (state) => ({ 61 | resorts: state.app.resorts.resorts, 62 | resortsStatus: state.app.resorts.resortsStatus, 63 | }); 64 | 65 | const ConnectedMain = connect( 66 | mapStateToProps, 67 | )(Main); 68 | 69 | export default ConnectedMain; 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slope Ninja 2 | 3 | [![npm](https://img.shields.io/github/license/slopeninja/slopeninja-frontend.svg)](https://github.com/slopeninja/slopeninja-frontend/blob/master/LICENSE.md) 4 | 5 | ![Slope Ninja](https://github.com/slopeninja/slopeninja-frontend/blob/master/.github/mockup.png) 6 | 7 | This repository hosts the source code for http://slope.ninja. If you have any questions, feel free to reach out to me on Twitter [@juliaqiuxy](https://twitter.com/juliaqiuxy). 8 | 9 | ### Visit Slope Ninja on the Web 10 | 11 | Slope Ninja is available on the Web at [http://slope.ninja](http://slope.ninja). 12 | 13 | ### Download Slope Ninja on iOS 14 | 15 | You can download Slope Ninja for iOS from [the App Store](https://itunes.apple.com/us/app/slope-ninja/id1297809634?ls=1&mt=8). 16 | 17 | [![Download on the App Store](https://github.com/slopeninja/slopeninja-frontend/blob/master/.github/appStore.svg)](https://itunes.apple.com/us/app/slope-ninja/id1297809634?ls=1&mt=8) 18 | 19 | ### Download Slope Ninja on Android 20 | 21 | You can download Slope Ninja for Android from [the Play Store](https://play.google.com/store/apps/details?id=ninja.slope.app). 22 | 23 | [![Download on the App Store](https://github.com/slopeninja/slopeninja-frontend/blob/master/.github/playStore.svg)](https://play.google.com/store/apps/details?id=ninja.slope.app) 24 | 25 | 26 | 27 | ### I love this, how do I contribute? 28 | 29 | * Simply star this repository. Keeps me motivated 30 | * Help me spread the world on Facebook and Twitter 31 | * Contribute Code! Contributions are very welcome. 32 | 33 | 34 | 35 | ### License 36 | All pull requests that get merged will be made available under [the MIT license](https://github.com/slopeninja/slopeninja-frontend/blob/master/LICENSE.md), as the rest of the repository. 37 | -------------------------------------------------------------------------------- /src/components/SideNav/ResortNavCard.css: -------------------------------------------------------------------------------- 1 | .ResortNavCard { 2 | height: 140px; 3 | display: flex; 4 | flex-direction: row; 5 | border: 1px solid; 6 | border-color: #EDEDED; 7 | position: relative; 8 | overflow: hidden; 9 | } 10 | 11 | .ResortNavCard-logo { 12 | width: 104px; 13 | display: flex; 14 | justify-content: center; 15 | margin: 1pc; 16 | } 17 | 18 | .ResortNavCard-logo img { 19 | width: 64px; 20 | height: 64px; 21 | } 22 | 23 | .ResortNavCard-info { 24 | flex: 1; 25 | display: flex; 26 | flex-direction: column; 27 | } 28 | 29 | .ResortNavCard-title { 30 | flex: 1; 31 | } 32 | 33 | .ResortNavCard-title h3 { 34 | font-size: 24px; 35 | font-weight: 300; 36 | margin-top: 0.5pc; 37 | margin-bottom: 0; 38 | padding: 0; 39 | } 40 | 41 | .ResortNavCard-title h5 { 42 | font-size: 16px; 43 | font-weight: 300; 44 | margin-top: 0.5pc; 45 | margin-bottom: 0; 46 | padding: 0; 47 | color: #9B9B9B; 48 | } 49 | 50 | 51 | .ResortNavCard-status { 52 | display: flex; 53 | flex-direction: row; 54 | flex: 2; 55 | } 56 | 57 | .ResortNavCard-status-section { 58 | flex: 1; 59 | display: flex;; 60 | justify-content: center; 61 | flex-direction: column; 62 | } 63 | 64 | .ResortNavCard-status-section img { 65 | margin-top: 1pc; 66 | width: 78px; 67 | height: 14px; 68 | } 69 | 70 | .ResortNavCard-status-section h5 { 71 | font-size: 16px; 72 | font-weight: 300; 73 | padding: 0; 74 | margin: 0; 75 | } 76 | /* 77 | @custom-media --sm-viewport only screen and (min-width: 48em); // 768px 78 | @custom-media --md-viewport only screen and (min-width: 64em); // 994px 79 | @custom-media --lg-viewport only screen and (min-width: 75em); / 1200px 80 | */ 81 | @media only screen and (min-width: 768px) { 82 | .ResortNavCard:hover { 83 | background-color: #EDEDED; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /public/images/twitterLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8800 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slopeninja-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "eslint": "^7.2.0", 7 | "eslint-config-airbnb": "^18.2.1", 8 | "eslint-plugin-import": "^2.21.1", 9 | "eslint-plugin-jsx-a11y": "^6.4.1", 10 | "eslint-plugin-react": "^7.21.5", 11 | "eslint-plugin-react-hooks": "^1.7.0", 12 | "react-scripts": "0.9.5" 13 | }, 14 | "dependencies": { 15 | "better-react-spinkit": "^2.0.0-6", 16 | "bootstrap": "next", 17 | "classnames": "^2.2.5", 18 | "email-validator": "^1.0.7", 19 | "google-map-react": "^0.23.0", 20 | "history": "^4.6.1", 21 | "isomorphic-fetch": "^2.2.1", 22 | "lodash.samplesize": "^4.2.0", 23 | "normalize.css": "^5.0.0", 24 | "react": "^15.4.2", 25 | "react-dom": "^15.4.2", 26 | "react-ga": "^2.2.0", 27 | "react-progress-button": "^5.0.4", 28 | "react-redux": "^5.0.4", 29 | "react-redux-form": "^1.11.1", 30 | "react-router": "^4.1.1", 31 | "react-router-dom": "^4.1.1", 32 | "react-router-redux": "5.0.0-alpha.6", 33 | "react-window-resize-listener": "^1.1.0", 34 | "redux": "^3.6.0", 35 | "redux-logger": "^3.0.1", 36 | "redux-persist": "^4.8.2", 37 | "redux-thunk": "^2.2.0", 38 | "serve": "^10.1.1", 39 | "uuid": "^3.0.1", 40 | "wicg-focus-ring": "^1.0.1" 41 | }, 42 | "scripts": { 43 | "dev-with-local-backend": "REACT_APP_API_URL='http://localhost:8080' react-scripts start", 44 | "dev-with-remote-backend": "REACT_APP_API_URL='https://api.slope.ninja' react-scripts start", 45 | "build": "REACT_APP_API_URL='https://api.slope.ninja' react-scripts build", 46 | "start": "serve -s build/ -l 3001", 47 | "lint": "node_modules/eslint/bin/eslint.js src/", 48 | "test": "react-scripts test --env=jsdom", 49 | "eject": "react-scripts eject" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/images/resorts/palisades.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/OpenSourceBanner/githubLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/ResortInfoCardContent/ResortInfoCardTextInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './ResortInfoCardTextInfo.css'; 3 | 4 | const Temperature = ({ temperature }) => { 5 | const temp = temperature !== null ? `${temperature}°` : '-'; 6 | 7 | return ( 8 |
9 | Temperature 10 | {temp} 11 |
12 | ); 13 | }; 14 | 15 | const BaseCondition = ({ base }) => { 16 | const condition = base || '-'; 17 | 18 | return ( 19 |
20 | Base Condition 21 | { condition } 22 |
23 | ); 24 | }; 25 | 26 | const NewSnow = ({ newSnow }) => { 27 | const snow = newSnow !== null ? `${newSnow}"` : '-'; 28 | 29 | return ( 30 |
31 | New Snow 32 | {snow} 33 |
34 | ); 35 | }; 36 | 37 | const SnowDepth = ({ snowDepth }) => { 38 | const depth = snowDepth !== null ? `${snowDepth}"` : '-'; 39 | 40 | return ( 41 |
42 | Snow Depth 43 | {depth} 44 |
45 | ); 46 | }; 47 | 48 | const ResortInfoCardTextInfo = ({ resort }) => ( 49 |
50 |
53 | 54 |
55 |
58 | 59 |
60 |
63 | 64 |
65 |
68 | 69 |
70 |
71 | ); 72 | 73 | export default ResortInfoCardTextInfo; 74 | -------------------------------------------------------------------------------- /src/components/FlippableCard/FlippableCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | import './FlippableCard.css'; 5 | 6 | const FlippableCard = ({ 7 | currentCard, 8 | children, 9 | duriation = 0.8, 10 | cubicBezier = [0.15, 0.90, 0.25, 1.25], 11 | renderFrontCard, 12 | horizontal, 13 | }) => { 14 | let flippleCardFlippedStyle = ''; 15 | if (currentCard) { 16 | if (horizontal) { 17 | flippleCardFlippedStyle = 'FlippableCard-flipped-horizontal'; 18 | } else { 19 | flippleCardFlippedStyle = 'FlippableCard-flipped-vertical'; 20 | } 21 | } 22 | 23 | const flippableCardClassNames = classNames([ 24 | 'FlippableCard-card', 25 | flippleCardFlippedStyle, 26 | ]); 27 | 28 | const backFaceClassNames = classNames([ 29 | 'FlippableCard-face', 30 | horizontal ? 'FlippableCard-back-horizontal' : 'FlippableCard-back-vertical', 31 | ]); 32 | 33 | const cubicBezierStr = cubicBezier.join(','); 34 | 35 | const flippableCardStyles = { 36 | transition: `transform ${duriation}s cubic-bezier(${cubicBezierStr})`, 37 | '-o-transition': `transform ${duriation}s cubic-bezier(${cubicBezierStr})`, 38 | '-moz-transition': `transform ${duriation}s cubic-bezier(${cubicBezierStr})`, 39 | '-webkit-transition': `transform ${duriation}s cubic-bezier(${cubicBezierStr})`, 40 | }; 41 | 42 | const frontCardContent = renderFrontCard(); 43 | 44 | const cards = React.Children.toArray(children); 45 | 46 | if (cards.length === 0) { 47 | throw Error('You need to include at least 1 child component in Flippable'); 48 | } 49 | 50 | const backCardContent = cards.find( 51 | (element) => element.props.id === currentCard, 52 | ); 53 | 54 | if (currentCard && !backCardContent) { 55 | throw Error(`${currentCard} is not known to Flippable. Make sure your id is spelled correctly.`); 56 | } 57 | 58 | return ( 59 |
60 |
61 |
62 | {frontCardContent} 63 |
64 |
65 | {backCardContent} 66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default FlippableCard; 73 | -------------------------------------------------------------------------------- /src/components/ResortInfoCardContent/ResortInfoCardIconInfo.css: -------------------------------------------------------------------------------- 1 | .ResortInfoBox { 2 | min-height: 160px; 3 | height: 100%; 4 | width: 100%; 5 | border: 1px solid #EDEDED; 6 | background-color: #FFFFFF; 7 | padding: 1pc; 8 | box-sizing: border-box; 9 | } 10 | 11 | .ResortInfoBody-title { 12 | font-size: 24px; 13 | } 14 | 15 | .ResortInfoBody-content { 16 | font-weight: 300; 17 | font-size: 24px; 18 | display: flex; 19 | flex-direction: column; 20 | margin-top: 1pc; 21 | } 22 | 23 | .ResortInfoBody-content-openroutes { 24 | display: flex; 25 | flex-direction: row; 26 | } 27 | 28 | .ResortInfoBox-content-openroute-icon { 29 | margin-right: 1pc; 30 | margin-top: 1pc; 31 | position: relative; 32 | } 33 | 34 | .OpenRoutes-exception-indicator { 35 | width: 20px; 36 | height: 20px; 37 | position: absolute; 38 | top: -8px; 39 | left: 32px; 40 | } 41 | 42 | .OpenRoutes-exception-indicator svg { 43 | height: 100%; 44 | width: 100%; 45 | } 46 | 47 | .RoadTooltip-content { 48 | font-weight: 300; 49 | font-size: 16px; 50 | display: flex; 51 | flex-direction: column; 52 | margin-top: 1pc; 53 | } 54 | 55 | .RoadTooltip-labels { 56 | display: flex; 57 | flex-direction: row; 58 | flex-wrap: wrap; 59 | margin-top: 1pc; 60 | } 61 | 62 | .RoadTooltip-label:last-child { 63 | margin-right: 0; 64 | } 65 | 66 | .RoadTooltip-label { 67 | border: 1px solid #4A4A4A; 68 | border-radius: 1px; 69 | display: inline; 70 | padding: 5px; 71 | margin-right: 6px; 72 | margin-bottom: 6px; 73 | text-align: center; 74 | } 75 | 76 | .RoadTooltip-text { 77 | font-size: 16px; 78 | font-weight: 300; 79 | } 80 | 81 | .RoadTooltip-title { 82 | background: none; 83 | border: none; 84 | margin: 0; 85 | padding: 0; 86 | outline: none; 87 | outline-offset: 0; 88 | /* Additional styles to look like a link */ 89 | cursor: pointer; 90 | text-decoration: underline; 91 | -webkit-appearance: none; 92 | font: inherit; 93 | font-size: 24px; 94 | color: #4A4A4A; 95 | } 96 | 97 | 98 | @media only screen and (min-width: 1200px) { 99 | 100 | .ResortInfoBox { 101 | border: none; 102 | padding: 0.5pc; 103 | } 104 | .ResortInfoBody-title { 105 | font-size: 24px; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/components/SideNav/ResortNavCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Link, 4 | } from 'react-router-dom'; 5 | import ProgressBar from '../ResortInfoCard/ProgressBar'; 6 | import Snow from './Snow'; 7 | 8 | import './ResortNavCard.css'; 9 | 10 | const ResortNavCard = ({ resort, selected }) => { 11 | let liftsProgress; 12 | let trailsProgress; 13 | 14 | if (resort.liftCounts.open !== null && resort.liftCounts.total !== null) { 15 | liftsProgress = Math.ceil( 16 | (resort.liftCounts.open / resort.liftCounts.total) * 100, 17 | ); 18 | } 19 | 20 | if (resort.trailCounts.open !== null && resort.trailCounts.total !== null) { 21 | trailsProgress = Math.ceil( 22 | (resort.trailCounts.open / resort.trailCounts.total) * 100, 23 | ); 24 | } 25 | 26 | let selectedSytle; 27 | if (selected) { 28 | selectedSytle = { 29 | backgroundColor: '#EDEDED', 30 | }; 31 | } 32 | let snow; 33 | if (resort.weather.condition === 'snow') { 34 | snow = (); 35 | } 36 | 37 | return ( 38 | 44 |
45 | {snow} 46 |
47 | logo 51 |
52 |
53 |
54 |

{resort.name}

55 |
{resort.location}
56 |
57 |
58 |
59 |
Open Lifts
60 | 64 |
65 |
66 |
Open Trails
67 | 71 |
72 |
73 |
74 |
75 | 76 | ); 77 | }; 78 | 79 | export default ResortNavCard; 80 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/tornado.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tornado 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/fog.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fog 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/wind.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wind 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/ResortInfoCardContent/Chains.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import HighwayIcon from '../HighwayIcon/HighwayIcon'; 3 | import './Chains.css'; 4 | 5 | const Chains = ({ roads, onChangeCard }) => { 6 | const routesKeys = Object.keys(roads); 7 | const R1Highways = routesKeys.filter((key) => roads[key].chainStatus === 'R1'); 8 | const R2Highways = routesKeys.filter((key) => roads[key].chainStatus === 'R2'); 9 | 10 | const r1HighwayIcons = R1Highways.map( 11 | (key) => ( 12 | 13 | 14 | 15 | ), 16 | ); 17 | const r2HighwayIcons = R2Highways.map( 18 | (key) => ( 19 | 20 | 21 | 22 | ), 23 | ); 24 | 25 | let r1Row; 26 | if (r1HighwayIcons.length > 0) { 27 | r1Row = ( 28 |
29 | 37 | { r1HighwayIcons } 38 |
39 | ); 40 | } 41 | 42 | let r2Row; 43 | if (r2HighwayIcons.length > 0) { 44 | r2Row = ( 45 |
46 | 54 | { r2HighwayIcons } 55 |
56 | ); 57 | } 58 | 59 | let noChainRow; 60 | 61 | if (!r1Row && !r2Row) { 62 | noChainRow = ( 63 |
64 | 65 | No chains required. 66 | 67 |
68 | ); 69 | } 70 | 71 | return ( 72 |
73 | Chains 74 |
75 | {r1Row} 76 | {r2Row} 77 | {noChainRow} 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default Chains; 84 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import 'wicg-focus-ring'; 2 | import React from 'react'; 3 | import { 4 | Route, 5 | } from 'react-router-dom'; 6 | import ReactDOM from 'react-dom'; 7 | import { Provider } from 'react-redux'; 8 | import { 9 | createStore, 10 | combineReducers, 11 | applyMiddleware, 12 | compose, 13 | } from 'redux'; 14 | import { persistStore, autoRehydrate } from 'redux-persist'; 15 | import { combineForms } from 'react-redux-form'; 16 | // import { createLogger } from 'redux-logger'; 17 | import createHistory from 'history/createBrowserHistory'; 18 | import { 19 | ConnectedRouter, 20 | routerReducer, 21 | routerMiddleware, 22 | } from 'react-router-redux'; 23 | import reduxThunk from 'redux-thunk'; 24 | 25 | import 'normalize.css'; 26 | import 'bootstrap/dist/css/bootstrap-grid.css'; 27 | 28 | import './index.css'; 29 | 30 | /* reducers */ 31 | import resorts from './reducers/resorts'; 32 | import userSession from './reducers/userSession'; 33 | import createNewsletterSubscription from './reducers/newsletterSubscription'; 34 | 35 | /* forms */ 36 | import newsletterSubscription from './forms/newsletterSubscription'; 37 | 38 | import App from './components/App/App'; 39 | import ScrollToTop from './ScrollToTop'; 40 | import withTracker from './withTracker'; 41 | 42 | const history = createHistory(); 43 | 44 | const app = combineReducers({ 45 | resorts, 46 | createNewsletterSubscription, 47 | }); 48 | 49 | const formReducers = combineForms({ 50 | newsletterSubscription, 51 | }, 'forms'); 52 | 53 | const rootReducer = combineReducers({ 54 | app, 55 | userSession, 56 | router: routerReducer, 57 | forms: formReducers, 58 | }); 59 | 60 | /* eslint-disable no-underscore-dangle */ 61 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 62 | /* eslint-enable */ 63 | 64 | const store = createStore( 65 | rootReducer, 66 | undefined, 67 | composeEnhancers( 68 | applyMiddleware( 69 | // middleware for intercepting and dispatching navigation actions 70 | // createLogger(), 71 | reduxThunk, 72 | routerMiddleware(history), 73 | ), 74 | autoRehydrate(), 75 | ), 76 | ); 77 | // begin periodically persisting the store 78 | persistStore( 79 | store, 80 | { 81 | whitelist: ['userSession'], 82 | }, 83 | ); 84 | 85 | ReactDOM.render( 86 | 87 | 88 | 89 | 90 | 91 | 92 | , 93 | document.getElementById('🏂'), 94 | ); 95 | -------------------------------------------------------------------------------- /public/images/resorts/squaw.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Squaw 5 | Created with Sketch. 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/components/Footer/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 3 Copy 5 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/cloudy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | cloudy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/components/Footer/holidayLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/SideNav/SideNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { connect } from 'react-redux'; 4 | import { ThreeBounce } from 'better-react-spinkit'; 5 | 6 | import ResortNavCard from './ResortNavCard'; 7 | // import AppDownloadCard from '../AppDownloadCard/AppDownloadCard'; 8 | import TwitterCard from '../TwitterCard/TwitterCard'; 9 | import './SideNav.css'; 10 | 11 | export const LoadingIndicator = () => ( 12 |
20 | 21 |
22 | ); 23 | 24 | const ErrorIndicator = () => ( 25 |
33 | Opps. Something 34 | {'\''} 35 | s not right. 36 |
37 | ); 38 | 39 | const SideNav = ({ resorts, resortsStatus, match }) => { 40 | const { shortName } = match.params; 41 | let hideSideNavOnMobileClassName; 42 | if (shortName) { 43 | hideSideNavOnMobileClassName = 'SideNav-hideOnMobile'; 44 | } 45 | 46 | let sideNavContent; 47 | if (resortsStatus === 'success') { 48 | resorts.sort((a, b) => { 49 | const nameA = a.name.toUpperCase(); // ignore upper and lowercase 50 | const nameB = b.name.toUpperCase(); // ignore upper and lowercase 51 | if (nameA < nameB) { 52 | return -1; 53 | } 54 | if (nameA > nameB) { 55 | return 1; 56 | } 57 | return 0; 58 | }); 59 | sideNavContent = resorts.map((resort) => ( 60 | 65 | )); 66 | 67 | const [first, second, third, ...rest] = sideNavContent; 68 | sideNavContent = [ 69 | // , 70 | , 71 | first, 72 | second, 73 | third, 74 | ...rest, 75 | ]; 76 | } 77 | 78 | if (resortsStatus === 'fetching') { 79 | sideNavContent = ( 80 | 81 | ); 82 | } 83 | 84 | if (resortsStatus === 'fail') { 85 | sideNavContent = ( 86 | 87 | ); 88 | } 89 | 90 | const className = classNames(['SideNav-wrapper', hideSideNavOnMobileClassName]); 91 | 92 | return ( 93 | 98 | ); 99 | }; 100 | 101 | const mapStateToProps = (state) => ({ 102 | resorts: state.app.resorts.resorts, 103 | resortsStatus: state.app.resorts.resortsStatus, 104 | }); 105 | 106 | const ConnectedSideNav = connect( 107 | mapStateToProps, 108 | )(SideNav); 109 | 110 | export default ConnectedSideNav; 111 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/ResortInfoCard.css: -------------------------------------------------------------------------------- 1 | .ResortInfoCard { 2 | flex: 1; 3 | border: 1px solid #EDEDED; 4 | } 5 | 6 | .ResortInfoHeader-header { 7 | margin: 5px; 8 | min-height: 120px; 9 | } 10 | 11 | .ResortInfoHeader-branding { 12 | display: flex; 13 | flex-direction: row; 14 | flex: 1; 15 | align-items: center; 16 | } 17 | 18 | .ResortInfoHeader-header-title { 19 | font-size: 30px; 20 | font-weight: 500; 21 | padding: 0; 22 | } 23 | 24 | .ResortInfoHeader-header-subtitle { 25 | font-size: 24px; 26 | font-weight: 300; 27 | margin: 0; 28 | padding: 0; 29 | } 30 | 31 | .ResortInfoHeader-condition { 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | min-height: 100px; 36 | } 37 | 38 | .ResortInfoHeader-weatherIcon { 39 | animation-name: bounce; 40 | transform-origin: top bottom; 41 | animation-iteration-count: infinite; 42 | animation-duration: 3s; 43 | animation-timing-function: ease-out; 44 | } 45 | 46 | .ResortInfoHeader-status { 47 | display: flex; 48 | justify-content: center; 49 | align-items: center; 50 | min-height: 100px; 51 | } 52 | 53 | .ResortInfoHeader-status h3 { 54 | font-size: 24px; 55 | font-weight: 300; 56 | } 57 | 58 | .ResortInfoHeader-logo { 59 | margin: 0.5pc; 60 | } 61 | 62 | .ResortInfoHeader-logo img { 63 | width: 64px; 64 | } 65 | .ResortInfoBody-container { 66 | margin: 0; 67 | } 68 | 69 | /*Animate.css*/ 70 | @keyframes bounce { 71 | from, 20%, 53%, 80%, to { 72 | animation-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000); 73 | transform: translate3d(0,8,0); 74 | } 75 | 76 | 40%, 43% { 77 | animation-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060); 78 | transform: translate3d(0, -8px, 0); 79 | } 80 | 81 | 70% { 82 | transform: translate3d(0, -4px, 0); 83 | } 84 | 85 | 90% { 86 | transform: translate3d(0,0,0); 87 | } 88 | } 89 | 90 | 91 | /* 92 | @custom-media --sm-viewport only screen and (min-width: 48em); // 768px 93 | @custom-media --md-viewport only screen and (min-width: 64em); // 994px 94 | @custom-media --lg-viewport only screen and (min-width: 75em); / 1200px 95 | */ 96 | @media only screen and (min-width: 576px) { 97 | 98 | 99 | } 100 | 101 | @media only screen and (min-width: 1200px) { 102 | 103 | .ResortInfoBody-container { 104 | margin: 20px; 105 | } 106 | 107 | .ResortInfoHeader-header { 108 | background-color: #FAFAFA; 109 | } 110 | 111 | .ResortInfoHeader-header-title { 112 | font-size: 36px; 113 | } 114 | 115 | .ResortInfoHeader-header-subtitle { 116 | font-size: 24px; 117 | } 118 | 119 | .ResortInfoHeader-extras { 120 | float: right; 121 | } 122 | 123 | .ResortInfoHeader-condition { 124 | min-height: 60px; /* header height / 2 */ 125 | } 126 | 127 | .ResortInfoHeader-status { 128 | min-height: 60px; /* header height / 2 */ 129 | } 130 | 131 | .ResortInfoHeader-logo { 132 | margin: 1pc; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/components/WeatherBanner/WeatherBanner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import sampleSize from 'lodash.samplesize'; 4 | import snowflake from '../ResortInfoCard/images/snow-white.svg'; 5 | import './WeatherBanner.css'; 6 | 7 | const generateBannerText = (resorts) => { 8 | if (!resorts) { 9 | return null; 10 | } 11 | 12 | const len = resorts.length; 13 | 14 | if (len === 1) { 15 | const [r1] = resorts; 16 | return ( 17 | 18 | Snowing at 19 | {' '} 20 | {r1.name} 21 | ! Keep up the snow dance until it 22 | spreads to the rest of the mountains around. 23 | 24 | ); 25 | } 26 | 27 | if (len === 2) { 28 | const [r1, r2] = resorts; 29 | return ( 30 | 31 | That snow dance of yours! 32 | {' '} 33 | {r1.name} 34 | {' '} 35 | just joined 36 | {' '} 37 | {r2.name} 38 | {' '} 39 | with reports of pow and more to come. 40 | 41 | ); 42 | } 43 | 44 | if (len === 3) { 45 | const [r1, r2, r3] = resorts; 46 | return ( 47 | 48 | Powder vibes! It’s getting white all over 49 | {' '} 50 | {r1.name} 51 | , 52 | {r2.name} 53 | {' '} 54 | and 55 | {' '} 56 | {r3.name} 57 | . 58 | 59 | ); 60 | } 61 | 62 | const [r1, r2] = sampleSize(resorts, 2); 63 | return ( 64 | 65 | Wowza! Everyone’s abuzz with reports of snow. It’s now dumping at 66 | {' '} 67 | {r1.name} 68 | , 69 | {' '} 70 | {r2.name} 71 | {' '} 72 | and 73 | {' '} 74 | {len - 2} 75 | {' '} 76 | other 77 | resorts. 78 | 79 | ); 80 | }; 81 | 82 | const WeatherBanner = (props) => { 83 | const snowingResorts = props.resorts.filter( 84 | (resort) => resort.weather.condition === 'snow', 85 | ); 86 | 87 | if (snowingResorts.length === 0) { 88 | return null; 89 | } 90 | 91 | const bannerText = generateBannerText(snowingResorts); 92 | 93 | return ( 94 |
95 | snowflake 104 | {bannerText} 105 |
106 | ); 107 | }; 108 | 109 | const mapStateToProps = (state) => ({ 110 | resorts: state.app.resorts.resorts, 111 | }); 112 | 113 | const ConnectedWeatherBanner = connect(mapStateToProps)(WeatherBanner); 114 | 115 | export default ConnectedWeatherBanner; 116 | -------------------------------------------------------------------------------- /src/components/Main/Map.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import GoogleMap from 'google-map-react'; 3 | import { WindowResizeListener } from 'react-window-resize-listener'; 4 | import mapTheme from './mapTheme'; 5 | import MapPin from './MapPin'; 6 | import './Map.css'; 7 | 8 | WindowResizeListener.DEBOUNCE_TIME = 50; 9 | 10 | const GOOGLE_MAP_API_KEY = { 11 | key: 'AIzaSyCceGlAwHncILM7vq047eJJXQBgZN5JVe8', 12 | }; 13 | 14 | class Map extends Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.googleMap = null; 19 | this.currentUserCenter = props.coords; 20 | } 21 | 22 | componentWillMount() { 23 | this.handleMapDrag = this.handleMapDrag.bind(this); 24 | this.handleWindowChange = this.handleWindowChange.bind(this); 25 | this.handleGoogleApiLoaded = this.handleGoogleApiLoaded.bind(this); 26 | } 27 | 28 | componentWillReceiveProps(nextProps) { 29 | this.currentUserCenter = nextProps.coords; 30 | if (this.googleMap) { 31 | this.googleMap.setCenter(this.currentUserCenter); 32 | this.googleMap.setZoom(nextProps.zoom); 33 | } 34 | } 35 | 36 | handleGoogleApiLoaded({ map }) { 37 | this.googleMap = map; 38 | } 39 | 40 | handleMapDrag() { 41 | if (this.googleMap) { 42 | const latLng = this.googleMap.getCenter(); 43 | this.currentUserCenter = { 44 | lat: latLng.lat(), 45 | lng: latLng.lng(), 46 | }; 47 | } 48 | } 49 | 50 | handleWindowChange() { 51 | if (this.googleMap) { 52 | this.googleMap.setCenter(this.currentUserCenter); 53 | } 54 | } 55 | 56 | render() { 57 | let mapPins; 58 | if (this.props.resorts) { 59 | mapPins = this.props.resorts.map( 60 | (resort) => ( 61 | 67 | ), 68 | ); 69 | } 70 | const createMapOptions = (maps) => ({ 71 | backgroundColor: '#FFFFFF', 72 | zoomControlOptions: { 73 | position: maps.ControlPosition.TOP_RIGHT, 74 | style: maps.ZoomControlStyle.SMALL, 75 | }, 76 | scrollwheel: false, 77 | styles: mapTheme, 78 | }); 79 | 80 | return ( 81 |
82 | 83 | 96 | { mapPins } 97 | 98 |
99 | ); 100 | } 101 | } 102 | 103 | export default Map; 104 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Route, 4 | Redirect, 5 | Switch, 6 | } from 'react-router-dom'; 7 | 8 | import { connect } from 'react-redux'; 9 | 10 | import 11 | SideNav, { 12 | LoadingIndicator, 13 | } from '../SideNav/SideNav'; 14 | 15 | import Main from '../Main/Main'; 16 | import Footer from '../Footer/Footer'; 17 | import FourOhFour from '../FourOhFour/FourOhFour'; 18 | import WeatherBanner from '../WeatherBanner/WeatherBanner'; 19 | import OpenSourceBanner from '../OpenSourceBanner/OpenSourceBanner'; 20 | import EmailSignup from '../EmailSignup/EmailSignup'; 21 | import Privacy from '../Privacy/Privacy'; 22 | 23 | import './App.css'; 24 | 25 | import { fetchResorts } from '../../actions/resorts'; 26 | // import { setShowNewsletterSubscription } from '../../actions/userSession'; 27 | 28 | class App extends Component { 29 | componentDidMount() { 30 | this.props.fetchResorts(); 31 | } 32 | 33 | componentWillReceiveProps() { 34 | } 35 | 36 | render() { 37 | if (this.props.resortsStatus === 'fetching') { 38 | return ( 39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | let emailSignup; 46 | if (this.props.showEmailSignup) { 47 | emailSignup = ( 48 | 49 | ); 50 | } 51 | return ( 52 |
53 | 54 | 55 | ( 58 |
59 | {emailSignup} 60 | 61 | 62 |
63 | 64 | ( 68 | 69 | )} 70 | /> 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
79 |
80 |
81 | )} 82 | /> 83 |
84 |
85 | ); 86 | } 87 | } 88 | 89 | const mapStateToProps = (state) => ({ 90 | resortsStatus: state.app.resorts.resortsStatus, 91 | showEmailSignup: state.userSession.showEmailSignup, 92 | }); 93 | 94 | const mapDispatchToProps = (dispatch) => ({ 95 | fetchResorts: () => { 96 | dispatch(fetchResorts); 97 | }, 98 | }); 99 | 100 | const ConnectedApp = connect(mapStateToProps, mapDispatchToProps)(App); 101 | 102 | export default ConnectedApp; 103 | -------------------------------------------------------------------------------- /public/images/resorts/homewood.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Homewood 5 | Created with Sketch. 6 | 7 | 22 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/partly-cloudy-night.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | partly-cloudy-night 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/images/resorts/boreal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Boreal 5 | Created with Sketch. 6 | 7 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/ResortInfoCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import { connect } from 'react-redux'; 3 | import ResortInfoCardTextInfo from '../ResortInfoCardContent/ResortInfoCardTextInfo'; 4 | import ResortInfoCardIconInfo from '../ResortInfoCardContent/ResortInfoCardIconInfo'; 5 | 6 | import './ResortInfoCard.css'; 7 | 8 | import clearDay from './images/clear-day.svg'; 9 | import clearNight from './images/clear-night.svg'; 10 | import rain from './images/rain.svg'; 11 | import snow from './images/snow.svg'; 12 | import sleet from './images/sleet.svg'; 13 | import wind from './images/wind.svg'; 14 | import fog from './images/fog.svg'; 15 | import cloudy from './images/cloudy.svg'; 16 | import partlyCloudyDay from './images/partly-cloudy-day.svg'; 17 | import partlyCloudyNight from './images/partly-cloudy-night.svg'; 18 | import hail from './images/hail.svg'; 19 | import thunderstorm from './images/thunderstorm.svg'; 20 | import tornado from './images/tornado.svg'; 21 | 22 | const WEATHER_ICONS = { 23 | 'clear-day': clearDay, 24 | 'clear-night': clearNight, 25 | clear: clearDay, 26 | rain, 27 | snow, 28 | sleet, 29 | wind, 30 | fog, 31 | cloudy, 32 | 'partly-cloudy-day': partlyCloudyDay, 33 | 'partly-cloudy-night': partlyCloudyNight, 34 | hail, 35 | thunderstorm, 36 | tornado, 37 | }; 38 | 39 | const ResortInfoHeader = ({ resort }) => ( 40 |
41 |
42 |
43 |
44 | 49 |
50 | logo 54 |
55 |
56 |
57 | 62 |

{resort.name}

63 |
64 |

65 | {'Today\'s Forecast'} 66 |

67 |
68 |
69 |
70 | 71 |
72 |
73 |
74 |
75 |
76 |

{resort.status === 'open' ? 'Open' : 'Closed'}

77 |
78 |
79 |
80 |
81 | logo 87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | ); 95 | 96 | const ResortInfoBody = ({ resort }) => ( 97 |
98 | 99 | 100 |
101 | ); 102 | 103 | const ResortInfoCard = ({ resort }) => ( 104 |
105 | 106 | 107 |
108 | ); 109 | 110 | export default ResortInfoCard; 111 | -------------------------------------------------------------------------------- /public/images/resorts/donner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Donner 5 | Created with Sketch. 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/clear-night.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | clear-night 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/images/resorts/heavenly.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hevenly 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/snow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | snow 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/resorts/northstar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Northstar 5 | Created with Sketch. 6 | 7 | 19 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/rain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | rain 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/thunderstorm.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | thunderstorm 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/snow-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Page-1 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/images/resorts/sierra.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sierra 5 | Created with Sketch. 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/components/WeatherBanner/snowflake.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 3 Copy 4 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/components/ResortInfoCard/images/hail.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hail 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/images/resorts/diamond.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Diamond 5 | Created with Sketch. 6 | 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /public/images/resorts/kirkwood.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kirkwood 5 | Created with Sketch. 6 | 7 | 15 | -------------------------------------------------------------------------------- /src/components/WeatherBanner/snowman.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 3 Copy 3 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/components/ResortInfoCardContent/ResortInfoCardIconInfo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ProgressBar from '../ResortInfoCard/ProgressBar'; 3 | import HighwayIcon from '../HighwayIcon/HighwayIcon'; 4 | import IncidentIcon from '../HighwayIcon/IncidentIcon'; 5 | import AmbiguousIcon from '../HighwayIcon/AmbiguousIcon'; 6 | 7 | import FlippableCard from '../FlippableCard/FlippableCard'; 8 | import back from '../ResortInfoCard/images/back.svg'; 9 | import Chains from './Chains'; 10 | import './ResortInfoCardIconInfo.css'; 11 | 12 | const OpenRoutes = ({ roads }) => { 13 | const highwayIcons = roads.map((road) => { 14 | const iconStyle = { 15 | opacity: (road.status === 'closed') ? 0.1 : 1, 16 | }; 17 | 18 | let incidentIcon; 19 | if (road.status === 'incident') { 20 | incidentIcon = ( 21 | 27 | 28 | 29 | ); 30 | } 31 | 32 | let ambiguousIcon; 33 | if (road.status === 'ambiguous') { 34 | ambiguousIcon = ( 35 | 41 | 42 | 43 | ); 44 | } 45 | 46 | return ( 47 | 55 | 56 | {incidentIcon} 57 | {ambiguousIcon} 58 | 59 | ); 60 | }); 61 | 62 | return ( 63 |
64 | Open Routes 65 |
66 | { highwayIcons } 67 |
68 |
69 | ); 70 | }; 71 | 72 | const OpenLifts = ({ liftCounts }) => { 73 | let percent; 74 | let liftData = '-'; 75 | if (liftCounts.open !== null && liftCounts.total !== null) { 76 | liftData = `${liftCounts.open} / ${liftCounts.total}`; 77 | percent = Math.ceil( 78 | (liftCounts.open / liftCounts.total) * 100, 79 | ); 80 | } 81 | 82 | return ( 83 |
84 | Open Lifts 85 |
86 | {liftData} 87 | 88 |
89 |
90 | ); 91 | }; 92 | 93 | const OpenTrails = ({ trailCounts }) => { 94 | let percent; 95 | let trailData = '-'; 96 | if (trailCounts.open !== null && trailCounts.total !== null) { 97 | trailData = `${trailCounts.open} / ${trailCounts.total}`; 98 | percent = Math.ceil( 99 | (trailCounts.open / trailCounts.total) * 100, 100 | ); 101 | } 102 | 103 | return ( 104 |
105 | Open Trails 106 |
107 | {trailData} 108 | 109 |
110 |
111 | ); 112 | }; 113 | 114 | const RoadTooltip = ({ onChangeCard, id, labels }) => { 115 | const labelElements = labels.map((label) => ( 116 |
117 | {label} 118 |
119 | )); 120 | 121 | return ( 122 |
123 | 131 | 132 | At least one of: 133 | 134 |
135 | { labelElements } 136 |
137 |
138 | ); 139 | }; 140 | 141 | class ResortInfoCardIconInfo extends Component { 142 | constructor(props) { 143 | super(props); 144 | 145 | this.renderFrontCard = this.renderFrontCard.bind(this); 146 | this.handleFlipCard = this.handleFlipCard.bind(this); 147 | 148 | this.state = { 149 | currentCard: undefined, 150 | }; 151 | } 152 | 153 | handleFlipCard(currentCard) { 154 | this.setState({ 155 | currentCard, 156 | }); 157 | } 158 | 159 | renderFrontCard() { 160 | const { resort } = this.props; 161 | return ( 162 | 166 | ); 167 | } 168 | 169 | render() { 170 | const { resort } = this.props; 171 | 172 | return ( 173 |
174 |
175 | 176 |
177 |
178 |
184 | 190 | 195 | 200 | 201 |
202 |
203 |
204 | 205 |
206 | 207 |
208 | 209 |
210 |
211 | ); 212 | } 213 | } 214 | export default ResortInfoCardIconInfo; 215 | -------------------------------------------------------------------------------- /src/components/EmailSignup/EmailSignup.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-pascal-case */ 2 | import React, { Component } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { Control, Form } from 'react-redux-form'; 5 | import ProgressButton from 'react-progress-button'; 6 | 7 | import snowboarders from '../FourOhFour/snowboarders.svg'; 8 | import './EmailSignup.css'; 9 | 10 | import { setShowNewsletterSubscription } from '../../actions/userSession'; 11 | import { 12 | createNewsletterSubscription, 13 | newsletterSubscriptionFail, 14 | newsletterSubscriptionReset, 15 | } from '../../actions/newsletterSubscription'; 16 | 17 | import { isValidEmail, isNotEmpty } from '../../util/validators'; 18 | 19 | const BUTTON_ERROR_DISMISS_DURATION = 1200; 20 | const BUTTON_SUCCESS_DISMISS_DURATION = 1200; 21 | 22 | // FIXME: This is here until the following bug is fixed: 23 | // https://github.com/davidkpiano/react-redux-form/issues/777 24 | const preventFormSubmissionOnEnter = (event) => { 25 | if (event.key === 'Enter') { 26 | event.preventDefault(); 27 | } 28 | }; 29 | 30 | const EmailSignupForm = ({ 31 | onFormSubmit, 32 | onFormSubmitFailed, 33 | onDismissClick, 34 | submitButtonStatus, 35 | }) => ( 36 |
42 | 53 | 61 | Submit 62 | 63 | 75 | 76 | ); 77 | 78 | class EmailSignup extends Component { 79 | constructor(props) { 80 | super(props); 81 | 82 | this.handleFormSubmit = this.handleFormSubmit.bind(this); 83 | this.handleFormSubmitFailed = this.handleFormSubmitFailed.bind(this); 84 | this.handleDismissClick = this.handleDismissClick.bind(this); 85 | 86 | this.buttonResetTimeout = null; 87 | } 88 | 89 | componentWillUnmount() { 90 | clearTimeout(this.buttonResetTimeout); 91 | } 92 | 93 | handleFormSubmitFailed() { 94 | this.props.failNewsletterSubscription('Invalid Email'); 95 | 96 | // react-progress-button isn't a fully controlled component, 97 | // so we need to sync states 98 | this.buttonResetTimeout = setTimeout(() => { 99 | this.props.resetNewsletterSubscription(); 100 | }, BUTTON_ERROR_DISMISS_DURATION); 101 | } 102 | 103 | handleFormSubmit(newsletterSubscription) { 104 | const { email } = newsletterSubscription; 105 | this.props.createNewsletterSubscription(email); 106 | } 107 | 108 | handleDismissClick() { 109 | this.props.disableEmailSignup(); 110 | } 111 | 112 | render() { 113 | const EmailSignupTitle = () => ( 114 |
115 |
116 |
117 |
120 | snowboarders 124 |
125 |
126 |
127 |
128 |
135 | Want updates via email? 136 |
137 |
145 | {"Don't worry, we hate spam! Here’s a "} 146 | 154 | sample 155 | 156 | . 157 |
158 |
159 |
160 |
161 |
162 | ); 163 | 164 | return ( 165 |
166 |
167 |
168 | 169 |
170 |
171 | 177 |
178 |
179 |
180 | ); 181 | } 182 | } 183 | 184 | const mapStateToProps = (state) => ({ 185 | buttonState: state.app.createNewsletterSubscription.buttonState, 186 | }); 187 | 188 | const mapDispatchToProps = (dispatch) => ({ 189 | disableEmailSignup: () => { 190 | dispatch(setShowNewsletterSubscription(null)); 191 | }, 192 | createNewsletterSubscription: (email) => { 193 | dispatch(createNewsletterSubscription(email)); 194 | }, 195 | failNewsletterSubscription: (errorMessage) => { 196 | dispatch(newsletterSubscriptionFail(errorMessage)); 197 | }, 198 | resetNewsletterSubscription: () => { 199 | dispatch(newsletterSubscriptionReset()); 200 | }, 201 | }); 202 | 203 | export default connect(mapStateToProps, mapDispatchToProps)(EmailSignup); 204 | /* eslint-enable */ 205 | --------------------------------------------------------------------------------