<repo_name>mfountoulakis/hoopify<file_sep>/components/Button.js
import React from 'react';
import styled from 'styled-components';
import { space, color } from 'styled-system';

const StyledButton = styled.button(color, space, {
  fontFamily: 'sans-serif',
  fontSize: '24px',
  backgroundColor: 'inherit',
});

const Button = props => <StyledButton {...props} />;

Button.defaultProps = {
  color: 'blue',
  px: [2, 3],
};

export default Button;
<file_sep>/next.config.js
const withOffline = moduleExists('next-offline') ? require('next-offline') : {};
const withManifest = moduleExists('next-manifest')
  ? require('next-manifest')
  : {};
const withPlugins = moduleExists('next-compose-plugins')
  ? require('next-compose-plugins')
  : {};

const nextConfig = {
  publicRuntimeConfig: {
    BASEURL: process.env.BASEURL,
  },

  webpack: (config, { dev }) => {
    if (dev) {
      config.module.rules.push({
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'eslint-loader',
        options: {
          // eslint options (if necessary)
        },
      });
    }

    return config;
  },
};

module.exports =
  moduleExists('next-offline') && moduleExists('next-compose-plugins')
    ? withPlugins(
        [
          [
            withOffline,
            {
              workboxOpts: {
                swDest: 'static/service-worker.js',
                runtimeCaching: [
                  {
                    urlPattern: /^https?.*/,
                    handler: 'networkFirst',
                    options: {
                      cacheName: 'https-calls',
                      networkTimeoutSeconds: 15,
                      expiration: {
                        maxEntries: 150,
                        maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
                      },
                      backgroundSync: {
                        name: 'hoopify-sync-queue',
                        options: {
                          maxRetentionTime: 24 * 60, // Retry for max of 24 Hours
                        },
                      },
                      cacheableResponse: {
                        statuses: [0, 200],
                      },
                    },
                  },
                ],
              },
            },
          ],
        ],
        nextConfig,
      )
    : nextConfig;

function moduleExists(name) {
  try {
    return require.resolve(name);
  } catch (error) {
    return false;
  }
}
<file_sep>/lib/theme.js
export default {
  colors: {
    black: '#000',
    white: '#fff',
    gray: '#777',
    orange: '#FA823F',
  },
  fonts: {
    pilat: `'Pilat Bold', sans-serif`,
    pilatExtended: `'Pilat Extended Bold', sans-serif`,
  },
  fontSizes: [10, 13, 18, 32],
};
<file_sep>/components/ActiveScore.js
import React from 'react';
import PropTypes from 'prop-types';
import { Flex } from '@rebass/grid';

import Text from './Text';

class ActiveScore extends React.Component {
  render() {
    const { activeGame = {} } = this.props;

    return (
      <Flex justifyContent="space-between">
        <Flex flexDirection={'column'} alignItems={'center'}>
          <Text fontSize={4}>{activeGame.hTeam.score}</Text>
          <Text color={'gray'} fontSize={0}>
            {activeGame.hTeam.triCode}
            {activeGame.hTeam.record}
          </Text>
        </Flex>
        <Flex flexDirection={'column'} alignItems={'center'}>
          <Text fontSize={4}>{activeGame.vTeam.score}</Text>
          <Text color={'gray'} fontSize={0}>
            {activeGame.vTeam.triCode}
            {activeGame.vTeam.record}
          </Text>
        </Flex>
      </Flex>
    );
  }
}

ActiveScore.propTypes = {
  activeGame: PropTypes.object.isRequired,
};

export default ActiveScore;
<file_sep>/pages/_app.js
import React from 'react';
import App, { Container } from 'next/app';
import { ThemeProvider } from 'styled-components';
import getConfig from 'next/config';
import fetch from 'isomorphic-unfetch';
import { compose, filter, map, prop } from 'lodash/fp';
import GameContext from '../context/GameContext';
import theme from '../lib/theme';
import { View } from '../components/Layout';

export default class MyApp extends App {
  constructor(props) {
    super(props);
    this.state = {
      favTeam: '',
      loading: true,
    };
  }

  static async getInitialProps({ Component, ctx }) {
    let pageProps = {};

    const { publicRuntimeConfig } = getConfig();
    const url = `${publicRuntimeConfig.BASEURL}`;
    const result = await fetch(`${url}/api/today`);
    const json = await result.json();

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps, games: json.games };
  }

  setFavTeam = favTeam => {
    return Promise.resolve(localStorage.setItem('favTeam', favTeam));
  };

  getFavTeam = () => {
    return Promise.resolve(localStorage.getItem('favTeam'));
  };

  componentDidMount() {
    const { router } = this.props;

    if (window.localStorage) {
      this.getFavTeam().then(favTeam => {
        !!favTeam && favTeam.length
          ? this.filterFavorite()
          : router.push('/teams');
        this.setState({ favTeam, loading: false });
      });
    }

    if ('serviceWorker' in navigator) {
      window.addEventListener('beforeinstallprompt', e => {
        e.preventDefault(); // Prevents prompt display initially
        this.setState({ promptEvent: e });
      });
    }
  }

  filterFavorite = () => {
    const { favTeam } = this.state;
    const { games, router } = this.props;

    const mapGame = xs =>
      xs.map(xs => {
        return {
          id: xs.gameId,
          matchup: [xs.hTeam.triCode, xs.vTeam.triCode],
        };
      });

    const teamPlayingToday = compose(
      map(prop('id')),
      filter({ matchup: [favTeam] }),
      mapGame,
    );

    teamPlayingToday(games).length
      ? router.push(`/game/${teamPlayingToday(games)}`)
      : null;
  };

  render() {
    const { Component, pageProps } = this.props;

    return (
      <Container>
        <ThemeProvider theme={theme}>
          <GameContext.Provider value={this.state.favTeam}>
            <View filterFavorite={this.filterFavorite}>
              <Component
                {...pageProps}
                {...this.props}
                {...this.state}
                filterFavorite={this.filterFavorite}
                setFavTeam={v =>
                  this.setState({ favTeam: v }, () => {
                    this.setFavTeam(this.state.favTeam);
                  })
                }
              />
            </View>
          </GameContext.Provider>
        </ThemeProvider>
      </Container>
    );
  }
}
<file_sep>/components/Layout/index.js
import React from 'react';
import PropTypes from 'prop-types';
import { Box, Flex } from '@rebass/grid';

import Navigation from '../Navigation';

export const MaxBound = props => <Box px={2} mx={'auto'} {...props} />;

const bottomCss = {
  bottom: '0',
  flexDirection: 'column',
  height: '140px',
  marginTop: '100%',
  postion: 'fixed',
  width: '100%',
};

export const View = props => {
  return (
    <Box>
      <MaxBound pt={6}>{props.children}</MaxBound>
      {props.bottom ? (
        <Flex css={bottomCss}>{props.bottom}</Flex>
      ) : (
        <Navigation filterFavorite={props.filterFavorite} />
      )}
    </Box>
  );
};

View.propTypes = {
  filterFavorite: PropTypes.func.isRequired,
  children: PropTypes.node.isRequired,
  bottom: PropTypes.node,
};
<file_sep>/components/ActionButton.js
import React from 'react';
import styled from 'styled-components';
import { color, space, fontSize } from 'styled-system';

const StyledActionButton = styled.button(space, fontSize, color, {
  alignItems: 'center',
  border: 'none',
  bottom: '0',
  display: 'flex',
  flexDirection: 'column',
  fontFamily: 'Pilat Extended Bold',
  height: '140px',
  position: 'fixed',
  width: '100%',
  letterSpacing: '0.05em',
});

const ActionButton = props => <StyledActionButton {...props} />;

ActionButton.defaultProps = {
  bg: 'orange',
  color: 'white',
  fontSize: 2,
  pt: 3,
};

export default ActionButton;
<file_sep>/components/Scoreboard.js
import React from 'react';
import PropTypes from 'prop-types';
import { Flex } from '@rebass/grid';

import Text from './Text';
import ActivityIndicator from './ActivityIndicator';
import Logo from './Logo';

/**
 * TODO: Make the activity indicator smarter, as in, different state
 * for timeouts and stuff
 **/

class Scoreboard extends React.Component {
  render() {
    const { game } = this.props;

    return (
      <Flex flex={1} justifyContent="space-between">
        <Flex alignItems="center">
          <Logo
            mr={3}
            src={`../static/images/teams/${
              game.basicGameData.hTeam.triCode
            }.svg`}
          />
          <Flex flexDirection="column" alignItems="center">
            <Text fontSize={3}>{game.basicGameData.hTeam.score}</Text>
            <Text caps fontSize={0}>
              {game.basicGameData.hTeam.triCode}
            </Text>
          </Flex>
        </Flex>
        <Flex flexDirection="column" alignItems="center">
          <Text>{game.basicGameData.time}</Text>
          <Text caps fontSize={0}>{`Q${
            game.basicGameData.period.current
          }`}</Text>
          <Text caps fontSize={3}>
            {game.basicGameData.clock}
          </Text>
          <Flex my={2}>
            <ActivityIndicator gameOn={game.basicGameData.isGameActivated} />
          </Flex>
        </Flex>
        <Flex alignItems="center">
          <Flex flexDirection="column" alignItems="center">
            <Text fontSize={3}>{game.basicGameData.vTeam.score}</Text>
            <Text caps fontSize={0}>
              {game.basicGameData.vTeam.triCode}
            </Text>
          </Flex>
          <Logo
            ml={3}
            src={`../static/images/teams/${
              game.basicGameData.vTeam.triCode
            }.svg`}
          />
        </Flex>
      </Flex>
    );
  }
}

Scoreboard.propTypes = {
  game: PropTypes.object.isRequired,
};

export default Scoreboard;
<file_sep>/components/Select.js
import React from 'react';
import styled from 'styled-components';
import { space } from 'styled-system';

const StyledSelect = styled.select(space, {
  appearance: 'none',
  backgroundColor: 'black',
  backgroundImage: `url("/static/icons/chevron.svg")`,
  backgroundPosition: 'right 32px top 50%',
  backgroundRepeat: 'no-repeat',
  backgroundSize: '14px 8px',
  border: '2px solid white',
  color: 'white',
  display: 'block',
  fontFamily: 'Pilat Extended Bold',
  fontSize: '12px',
  height: '72px',
  textTransform: 'uppercase',
  width: '100%',
  letterSpacing: '0.05em',
});

const Select = props => <StyledSelect {...props} />;

Select.defaultProps = {
  px: 3,
  mb: 3,
};

export default Select;
<file_sep>/components/Logo.js
import React from 'react';
import { Flex } from '@rebass/grid';

const Logo = props => (
  <Flex flex="1" css={{ maxWidth: '64px', height: '64px' }} {...props}>
    <img style={{ objectFit: 'contain' }} src={props.src} />
  </Flex>
);

export default Logo;
<file_sep>/components/Navigation.js
import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'next/router';
import styled from 'styled-components';
import { Flex } from '@rebass/grid';
import { Heart, Activity, Settings } from 'react-feather';

import ActionButton from './ActionButton';
import Text from './Text';
import t from '../lib/theme';

const navCSS = {
  backdropFilter: 'saturate(80%) blur(20px)',
  background: 'rgba(0, 0, 0, 0.8)',
  borderTop: '1px solid rgba(255, 255, 255, 0.1)',
  bottom: '0',
  height: '122px',
  position: 'fixed',
  width: '100%',
};

const Tabs = styled(Flex)({
  justifyContent: 'space-between',
  width: '100%',
});

const Tab = styled(Flex)({
  alignItems: 'center',
  flexDirection: 'column',
  fontFamily: 'sans-serif',
  height: '48px',
  justifyContent: 'space-between',
  width: '48px',
  color: '#fff',
});

class Navigation extends React.Component {
  handleClick = e => {
    this.props.filterFavorite();
    e.preventDefault();
  };

  render() {
    const { router, filterFavorite } = this.props;
    return (
      <React.Fragment>
        {router.pathname === '/teams' ? (
          <ActionButton onClick={filterFavorite}>GO</ActionButton>
        ) : (
          <Flex as="nav" px={4} pt={3} css={navCSS}>
            <Tabs>
              <a href="/" style={{ textDecoration: 'none' }}>
                <Tab>
                  <Activity
                    size={24}
                    color={
                      router.pathname === '/' ? `${t.colors.orange}` : '#fff'
                    }
                  />
                  <Text fontSize={0}>Today</Text>
                </Tab>
              </a>

              <a
                href="/game"
                style={{ textDecoration: 'none' }}
                onClick={e => this.handleClick(e)}
              >
                <Tab>
                  <Heart
                    size={24}
                    color={
                      router.pathname === '/game'
                        ? `${t.colors.orange}`
                        : '#fff'
                    }
                  />
                  <Text fontSize={0}>My Team</Text>
                </Tab>
              </a>

              <a href="/settings" style={{ textDecoration: 'none' }}>
                <Tab>
                  <Settings
                    size={24}
                    color={
                      router.pathname === '/settings'
                        ? `${t.colors.orange}`
                        : '#fff'
                    }
                  />
                  <Text fontSize={0}>Settings</Text>
                </Tab>
              </a>
            </Tabs>
          </Flex>
        )}
      </React.Fragment>
    );
  }
}

Navigation.propTypes = {
  filterFavorite: PropTypes.func.isRequired,
  router: PropTypes.object.isRequired,
};

export default withRouter(Navigation);
<file_sep>/pages/teams.js
import React, { Component } from 'react';
import fetch from 'isomorphic-unfetch';
import PropTypes from 'prop-types';
import getConfig from 'next/config';

import Text from '../components/Text';
import Select from '../components/Select';

class Teams extends Component {
  static async getInitialProps() {
    const { publicRuntimeConfig } = getConfig();
    const url = `${publicRuntimeConfig.BASEURL}`;
    const result = await fetch(`${url}/api/teams`);
    const json = await result.json();
    return {
      teams: json,
    };
  }

  handleChange = event => this.props.setFavTeam(event.target.value);

  render() {
    const {
      teams: {
        league: { standard },
      },
    } = this.props;

    const Nba = standardTeams =>
      standardTeams.filter(team => team.isNBAFranchise === true);

    const standardTeams = Nba(standard);
    return (
      <React.Fragment>
        <Text fontSize={3} mb={4} as={'label'} htmlFor={'team-picker'}>
          Choose your favorite NBA team to start:
        </Text>
        <Select htmlName={'team-picker'} onChange={e => this.handleChange(e)}>
          {standardTeams.map(team => (
            <option value={team.tricode} key={team.teamId}>
              {team.fullName}
            </option>
          ))}
        </Select>
        <Text color={'gray'}>
          Free agent fan? Don’t stress it, you can choose a different team later
          in the settings.
        </Text>
      </React.Fragment>
    );
  }
}

Teams.propTypes = {
  filterFavorite: PropTypes.func.isRequired,
  teams: PropTypes.object.isRequired,
  setFavTeam: PropTypes.func.isRequired,
};

export default Teams;
<file_sep>/components/ActivityIndicator.js
import React from 'react';
import PropTypes from 'prop-types';
import posed from 'react-pose';
import styled from 'styled-components';
import { Flex } from '@rebass/grid';

const IndicatorBackground = styled(Flex)({
  height: 2,
  width: 40,
});

const IndicatorInner = styled(Flex)({
  backgroundColor: '#00FA9A',
  height: '100%',
  width: 20,
  borderRadius: 2,
});

const AnimatedIndicator = posed(IndicatorInner)({
  left: {
    x: '0%',
    transition: {
      type: 'spring',
      ease: 'easeOut',
    },
  },
  right: {
    x: '100%',
    transition: {
      type: 'spring',
      ease: 'easeOut',
    },
  },
});

class ActivityIndicator extends React.Component {
  constructor() {
    super();
    this.props = {
      gameOn: false,
    };
    this.state = {
      isActive: true,
      sliderOn: false,
    };
  }

  static propTypes = {
    gameOn: PropTypes.bool.isRequired,
  };

  componentDidMount() {
    if (this.props.gameOn) {
      this.interval = setInterval(this.slide, 300);
    }
  }

  componentWillUnmount() {
    clearInterval(this.interval);
  }

  slide = () =>
    this.setState({
      sliderOn: !this.state.sliderOn,
    });

  render() {
    const { sliderOn } = this.state;
    return (
      <IndicatorBackground>
        <AnimatedIndicator pose={sliderOn ? 'left' : 'right'} />
      </IndicatorBackground>
    );
  }
}

export default ActivityIndicator;
<file_sep>/components/Text.js
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { space, color, fontSize } from 'styled-system';

const caps = props => (props.caps ? { textTransform: 'uppercase' } : null);

const Text = styled.p(space, color, fontSize, caps, {
  letterSpacing: '0.02em',
});

Text.displayName = 'Text';

Text.propTypes = {
  ...space.propTypes,
  ...color.propTypes,
  ...fontSize.propTypes,
  caps: PropTypes.bool,
};

Text.defaultProps = {
  fontSize: 1,
};

export default Text;
<file_sep>/lib/teamNames.js
export default {
  ATL: 'Atlanta Hawks',
  BOS: 'Boston Celtics',
  CHA: 'Charlotte Hornets',
  CHI: 'Chicago Bulls',
  CLE: 'Cleveland Cavaliers',
  DAL: 'Dallas Mavericks',
  DEN: '<NAME>',
  DET: 'Detroit Pistons',
  GSW: 'Golden State Warriors',
  HOU: 'Houston Rockets',
  IND: 'Indiana Pacers',
  LAC: 'LA Clippers',
  LAL: 'LA Lakers',
  MEM: 'Memphis Grizzlies',
  MIA: 'Miami Heat',
  MIL: 'Milwaukee Bucks',
  MIN: 'Minnesota Timberwolves',
  BKN: 'Brooklyn Nets',
  NOP: 'New Orleans Pelicans',
  NYK: 'New York Knicks',
  OKC: 'Oklahoma City Thunder',
  ORL: 'Orlando Magic',
  PHI: 'Philadelphia 76ers',
  PHX: 'Phoenix Suns',
  POR: 'Portland Trail Blazers',
  SAS: 'San Antonio Spurs',
  SAC: 'Sacramento Kings',
  TOR: 'Toronto Raptors',
  UTA: 'Utah Jazz',
  WAS: 'Washington Wizards',
};
<file_sep>/components/GameTime.js
import React from 'react';
import PropTypes from 'prop-types';
import { Flex } from '@rebass/grid';

import Text from './Text';

const GameTime = ({ time }) => (
  <Flex justifyContent={'center'}>
    <Text fontSize={2}>{time}</Text>
  </Flex>
);

GameTime.propTypes = {
  time: PropTypes.string.isRequired,
};

export default GameTime;
