├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
20 | in San Francisco. Powered by Dark Sky.
21 |
22 |
23 |
24 |
32 |
33 |
34 | );
35 | export default Footer;
36 |
37 | //
38 | //
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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
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 | [](https://github.com/slopeninja/slopeninja-frontend/blob/master/LICENSE.md)
4 |
5 | 
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 | [](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 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/fog.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/wind.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/components/Footer/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/cloudy.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |

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 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/partly-cloudy-night.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/resorts/boreal.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
{resort.status === 'open' ? 'Open' : 'Closed'}
77 |
78 |
79 |
80 |
81 |

87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | );
95 |
96 | const ResortInfoBody = ({ resort }) => (
97 |
98 |
99 |
100 |
101 | );
102 |
103 | const ResortInfoCard = ({ resort }) => (
104 |
108 | );
109 |
110 | export default ResortInfoCard;
111 |
--------------------------------------------------------------------------------
/public/images/resorts/donner.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/clear-night.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/resorts/heavenly.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/snow.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/resorts/northstar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/rain.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/thunderstorm.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/snow-white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/resorts/sierra.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/WeatherBanner/snowflake.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/ResortInfoCard/images/hail.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/images/resorts/diamond.svg:
--------------------------------------------------------------------------------
1 |
2 |
25 |
--------------------------------------------------------------------------------
/public/images/resorts/kirkwood.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/components/WeatherBanner/snowman.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
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 |

124 |
125 |
126 |
127 |
128 |
135 | Want updates via email?
136 |
137 |
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 |
--------------------------------------------------------------------------------