<repo_name>tlycken/tusenvuxenpoang<file_sep>/app/src/index.jsx
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import createHistory from 'history/createBrowserHistory'

import { configureStore } from './store/configure.js'
import App from './app/app.jsx'

const history = createHistory()

var configure = {
  store: () => configureStore({ history }),
  app: App,
  css: require('./index.css').default
}

let store = configure.store()

const run = () => {
  let App = configure.app
  let css = configure.css

  render(
    <Provider store={store}>
      <App history={history} id="app" />
    </Provider>,
    document.getElementById('react-root')
  )
}
run()

if (module.hot) {
  module.hot.accept('./app/app.jsx', () => {
    var app = require('./app/app.jsx').default
    configure = { ...configure, app }
    run()
  })

  module.hot.accept('./index.css', () => {
    const css = require('./index.css').default
    configure = { ...configure, css }
    run()
  })

  // module.hot.accept(
  //   './store/configure.js',
  //   () => {
  //     const store = require('./store/configure.js').configureStore
  //     configure = { ...configure, store }
  //     run()
  //   }
  // )
}
<file_sep>/app/src/pages/rsvp/thanks.jsx
import React from 'react'

export default () => (
  <div>
    <h2>Tack!</h2>
  </div>
)
<file_sep>/app/src/pages/find-your-way/find-your-way.jsx
import React from 'react'
import Map from './map.jsx'

export default () => (
  <div>
    <h2>Hitta till bröllopet</h2>
    <p>
      Vi kommer att fira vårt bröllop i Hälleforsnäs, en gammal bruksort i
      närheten av Flen och Katrineholm. Hit tar ni er enklast med bil eller tåg.
    </p>
    <p>
      Den korta sträckan från boendet på Granhedsgården till vigseln på Sjöviken
      promenerar ni till fots - vi ser till att alla kommer till festen och
      sedan tillbaka till Granhedsgården på natten.
    </p>
    <h3>Tåg</h3>
    <p>
      SJ kör hela vägen till Hälleforsnäs station, med byte i Eskilstuna för den
      som kommer från Stockholm. Från Hälleforsnäs till Granhedsgården och
      Sjöviken är det ungefär 6 km - om ni kommer med tåg, berätta det för oss i
      förväg, så kan vi försöka ordna skjuts den sträckan!
    </p>
    <h3> Bil</h3>
    <p>
      Att köra från Stockholm till Hälleforsnäs tar någonstans mellan 1,5 och 2
      timmar, beroende på trafikläget. Granhedsgården går att hitta på google
      maps. Precis innan ni kommer fram, om ni kommer från Hälleforsnäs-hållet,
      kör ni förbi ett stort rött hus; Sjöviken.
    </p>
    <Map />
  </div>
)
<file_sep>/app/src/pages/accommodation/accommodation.jsx
import React from 'react'

export default () => (
  <div>
    <h2>Övernattning</h2>
    <p>
      För övernattning mellan lördagen och söndagen föreslår vi{' '}
      <a href="http://www.granhedsgarden.se/">Granhedsgården</a>, som ligger
      granne med Sjöviken. Det är en kursgård, med rum av olika storlek och
      karaktär, men huvudsakligen vandrarhemsstandard. Vi har reserverat hela
      gården, så där finns plats för alla!{' '}
    </p>

    <p>
      Ni bokar sängplats på Granhedsgården genom oss, i samband med OSA till
      bröllopet. Även all betalning går via oss; information om det kommer när
      ni gjort er reservation. Priset för boende på Granhedsgården är 300 kr per
      person.
    </p>

    <p>
      På Granhedsgården finns både bastu och badmöjligheter. Utcheckning behöver
      inte ske förrän kl 17, så här finns alla möjligheter att njuta av en lugn
      söndag innan återresan till Stockholm.
    </p>

    <p>
      Om man inte vill bo på Granhedsgården finns några andra alternativ:
      Katrineholm ett par mil bort erbjuder flera hotell, och på Air BnB går det
      att hitta gott om privatpersoner som hyr ut hus i Hälleforsnäs med omnejd.
      Tänk bara på att vi bara ordnar transport till Granhedsgården på natten
      efter bröllopsfesten - väljer ni att bo någon annanstans får ni ordna resa
      dit på natten på egen hand!
    </p>
  </div>
)
<file_sep>/app/src/pages/pages.jsx
import Home from './home/home.jsx'
import WeddingWeekend from './wedding-weekend/wedding-weekend.jsx'
import FindYourWay from './find-your-way/find-your-way.jsx'
import TheSites from './the-sites/the-sites.jsx'
import Accommodation from './accommodation/accommodation.jsx'
import Rsvp from './rsvp/rsvp.jsx'
export { NotFound } from './errors/errors.jsx'
export { Oops } from './errors/errors.jsx'
export { default as Thanks } from './rsvp/thanks.jsx'

export const menuItems = [
  { url: '/', text: 'Hem', component: Home, exact: true },
  { url: '/brollopshelgen', text: 'Bröllopshelgen', component: WeddingWeekend },
  { url: '/platserna', text: 'Platserna', component: TheSites },
  { url: '/overnattning', text: 'Övernattning', component: Accommodation },
  { url: '/hitta', text: 'Hitta rätt', component: FindYourWay },
  { url: '/osa', text: 'O.S.A.', component: Rsvp }
]
<file_sep>/app/src/pages/find-your-way/map.jsx
import React from 'react'
import {
  withGoogleMap,
  withScriptjs,
  GoogleMap,
  Marker
} from 'react-google-maps'
import { compose, withProps } from 'recompose'
import './map.css'

const googleMapURL =
  'https://maps.googleapis.com/maps/api/js?v=3.exp&key=<KEY>&libraries=geometry,drawing,places'
const loadingElement = <div style={{ height: '100%' }} />
const containerElement = <div className="map" />
const mapElement = <div style={{ height: '100%' }} />

export default compose(
  withProps({
    googleMapURL,
    loadingElement,
    containerElement,
    mapElement
  }),
  withScriptjs,
  withGoogleMap
)(props => (
  <GoogleMap defaultZoom={13} defaultCenter={{ lat: 59.135, lng: 16.45 }}>
    <Marker
      position={{ lat: 59.1525932, lng: 16.4894057 }}
      label="Bröllopsfest i Kolhuset"
    />
    <Marker
      position={{ lat: 59.115302, lng: 16.410561 }}
      label="Vigsel på Sjöviken"
    />
    <Marker
      position={{ lat: 59.1135539, lng: 16.4056433 }}
      label="Övernattning på Granhedsgården"
    />
  </GoogleMap>
))
<file_sep>/deploy.sh
#!/bin/bash

if [[ $# -ne 1 ]]; then
  echo "Usage: ./deploy.sh [web|post-rsvp]"
  exit 1
fi

if [[ "$1" == "web" ]]; then
  echo "Building app..."
  cd app && yarn build > /dev/null && cd .. || cd ..

  echo "Uploading web assets..."
  aws s3 sync ./public s3://tusenvuxenpoang.se --delete --region eu-central-1 > /dev/null

  echo "Invalidating CloudFront cache"
  aws cloudfront create-invalidation --distribution-id "E4ZXM1QM1OLQJ" --paths /index.html /bundle.js /styles.css > /dev/null
elif [[ "$1" == "post-rsvp" ]]; then
  echo "Zipping lambda function..."
  cd post-rsvp && zip -ur ../post-rsvp.zip *  > /dev/null && cd .. || cd ..

  echo "Updating lambda code..."
  aws lambda update-function-code --function-name post-rsvp-1kVP --zip-file fileb://./post-rsvp.zip > /dev/null
else
  echo "I don't know how to deploy $1"
  exit 1
fi<file_sep>/app/src/pages/errors/errors.jsx
import React from 'react'
import { connect } from 'react-redux'

export const NotFound = () => <p>Oops... den här sidan finns inte!</p>

export const Oops = () => (
  <div>
    <h2>Oops, nu gick nåt lite snett!</h2>
    <p>Försök igen, eller be Tomas laga...</p>
  </div>
)
<file_sep>/app/src/app/app.jsx
import React from 'react'
import { AppContainer } from 'react-hot-loader'
import { BrowserRouter, Route, Switch, withRouter } from 'react-router-dom'
import { ConnectedRouter } from 'react-router-redux'

import * as Components from '../components/components.jsx'
import { menuItems, Thanks, Oops, NotFound } from '../pages/pages.jsx'

import './app.css'

let Content = withRouter(({ location, menuItems }) => (
  <Components.Page menuItems={menuItems}>
    <Switch location={location}>
      {menuItems
        .filter(item => !!item.component)
        .map(item => (
          <Route
            key={item.url}
            exact={item.exact}
            path={item.url}
            component={item.component}
          />
        ))}
      <Route path="/thanks" component={Thanks} />
      <Route path="/oops" component={Oops} />
      <Route component={NotFound} />
    </Switch>
  </Components.Page>
))

const App = props => (
  <div className="app">
    <Components.LeftBorder />
    <AppContainer>
      <ConnectedRouter history={props.history}>
        <Content menuItems={menuItems} />
      </ConnectedRouter>
    </AppContainer>
  </div>
)

export default App
<file_sep>/app/src/components/components.jsx
export { default as Menu } from './menu/menu.jsx'
export { default as Page } from './page/page.jsx'
export { default as LeftBorder } from './left-border/left-border.jsx'
<file_sep>/app/src/store/middleware/post-rsvp.js
import { push } from 'react-router-redux'

const sendRsvpData = rsvpForm =>
  fetch('/rsvp', {
    body: JSON.stringify(rsvpForm),
    method: 'post',
    mode: 'cors'
  })

export const POST_RSVP = 'rsvp/POST_RSVP'

export const postRsvp = dispatch => formData => {
  console.log(JSON.stringify(formData))
  dispatch({
    type: POST_RSVP,
    data: formData
  })
}

export const RSVP_SUCCESSFUL = 'rsvp/RSVP_SUCCESSFUL'
export const RSVP_FAILED = 'rsvp/RSVP_FAILED'

export default ({ dispatch }) => next => action => {
  switch (action.type) {
    case POST_RSVP:
      sendRsvpData(action.data)
        .then(() => dispatch({ type: RSVP_SUCCESSFUL }))
        .catch(err => {
          console.error(err)
          dispatch({ type: RSVP_FAILED, err })
        })
      break
    case RSVP_SUCCESSFUL:
      dispatch(push('/thanks'))
      break
    case RSVP_FAILED:
      dispatch(push({ pathname: '/oops' }))
      break
  }
  next(action)
}
<file_sep>/app/src/pages/home/home.jsx
import React from 'react'
import { connect } from 'react-redux'
import ILoveYou from './jag älskar dig.png'
import './home.css'
import Heart from './heart.png'

const Name = ({ first, middle, last }) => (
  <span className="name-badge">
    <span className="first">{first}</span>
    <span className="middle">{middle}</span>
    <span className="last">{last}</span>
  </span>
)

const Home = props => (
  <div className="home">
    <p>Välkomna på bröllopet mellan</p>
    <div className="home-names">
      <Name first="Sara" middle="Linnéa" last="Aschan" />
      <img className="heart" src={Heart} />
      <Name first="Tomas" middle="<NAME>" last="Lycken" />
    </div>
    <p>lördagen den 1 september 2018!</p>
  </div>
)

export default Home
<file_sep>/app/src/pages/the-sites/the-sites.jsx
import React from 'react'

export default () => (
  <div>
    <h2>Sjöviken</h2>
    <p>
      I Sörmlands skogar, vid stranden till Gålsjön, ligger ett stort rött hus
      med några uthus - Sjöviken. Storleken vittnar om dess förflutna som kollo
      för barn på sommaräventyr, men sedan maj 2016 tillhör det brudens
      föräldrar, Pia och Johan. På de två år som gått sedan dess har Sjöviken
      hunnit ge oss glada festligheter, lata sommardagar på bryggan, långa
      svamppromenader i skogarna och bastubad med tillhörande dopp i isvak.
      Julaftnarna här, utan både TV och internet, är redan tradition!
    </p>

    <p>
      Den första september 2018 blir Sjöviken skådeplats för ännu ett firande,
      när vigseln mellan Sara och Tomas äger rum på den stora grässluttningen
      ner mot sjön.
    </p>
    <h2>Kolhuset - eller “Här vill jag gifta mig”</h2>
    <p>
      Hälleforsnäs bruk öppnade 1659 och blev under 1800-talet och början av
      1900-talet en stor verksamhet, med som mest omkring 1000 anställda. Till
      järngjuteriet behövdes stora mängder träkol, och för att ha någonstans att
      lagra denna byggdes Kolhuset - en jättelik byggnad med en golvyta på
      nästan 4000 kvadratmeter och ett tak som, när det byggdes, var Europas
      största fribärande takkonstruktion. När kolanvändningen på Hälleforsnäs
      bruk så småningom upphörde blev Kolhuset ett förråd och till sist en
      skräpkammare. 1996 bildades föreningen Kolhusteatern och Kolhuset fick
      nytt liv som teaterscen. Numera sätts en ny pjäs upp här varje sommar, med
      stora ensembler av både barn och vuxna. Sommaren 2018 blir det musikalen
      Råttfångaren, baserad på legenden om Råttfångaren i Hameln. Biljetter
      finns på <a href="http://kolhusteatern.se/">Kolhusteaterns hemsida</a>,
      för den som är intresserad!
    </p>
    <p>
      Sommaren 2017 besökte Sara och <NAME>aterns uppsättning av
      musikalen <NAME>. Förälskelsen var ögonblicklig! Innan
      föreställningen var över konstaterade Sara att “Här vill jag gifta mig!”{' '}
    </p>

    <p>
      Det är i Kolhuset vi äter middag och festar efter vigseln på Sjöviken. I
      och med det får byggnaden ännu ett nytt användningsområde, eftersom den
      aldrig förut använts som festlokal (vad vi vet)!
    </p>
  </div>
)
<file_sep>/app/src/store/middleware/index.js
import thunk from 'redux-thunk'
import { routerMiddleware } from 'react-router-redux'
import postRsvp from './post-rsvp'

export default ({ history }) => [thunk, routerMiddleware(history), postRsvp]
<file_sep>/post-rsvp/sheets.js
const GoogleSpreadsheet = require('google-spreadsheet')

const SPREADSHEET_ID = process.env.SpreadsheetId
const CLIENT_EMAIL = process.env.GAuthClientEmail

// hack around lambda's inability to store env vars with newlines
// see https://forums.aws.amazon.com/thread.jspa?threadID=248843&tstart=0&messageID=790654#790654
const PRIVATE_KEY = process.env.GAuthPrivateKey.split('\\n').join('\n')

const creds = {
  client_email: CLIENT_EMAIL,
  private_key: PRIVATE_KEY
}

exports.authenticate = () =>
  new Promise((resolve, reject) => {
    const doc = new GoogleSpreadsheet(SPREADSHEET_ID)
    doc.useServiceAccountAuth(
      creds,
      (err, _) => (!err ? resolve(doc) : reject(err))
    )
  })

exports.addRow = doc => data =>
  new Promise((resolve, reject) =>
    doc.addRow(1, data, (err, _) => (!err ? resolve() : reject(err)))
  )
<file_sep>/app/src/pages/rsvp/rsvp.jsx
import React from 'react'
import { TransitionGroup, CSSTransition } from 'react-transition-group'
import { connect } from 'react-redux'

import {
  Form,
  FieldSet,
  RadioGroup,
  TextBox,
  TextArea,
  SubmitButton,
  NumberTextBox,
  Button
} from '../../components/form/form.jsx'
import './rsvp.css'

import { postRsvp } from '../../store/middleware/post-rsvp'

const timeout = { enter: 300, exit: 0 }

class NameInput extends React.Component {
  constructor(props) {
    super(props)
  }

  componentDidMount() {
    this.input.focus()
  }

  render() {
    return (
      <input
        ref={input => {
          this.input = input
        }}
        name="namn"
        type="text"
        value={this.props.name}
        onChange={this.props.onChange}
      />
    )
  }
}

class RsvpForm extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      names: [''],
      attending: 'not specified',
      email: '',
      errors: {
        names: '',
        email: '',
        attending: '',
        staying: ''
      }
    }

    this.updateName = this.updateName.bind(this)
    this.setEmail = this.setEmail.bind(this)
    this.addOne = this.addOne.bind(this)
    this.remove = this.remove.bind(this)
    this.setAttending = this.setAttending.bind(this)
    this.setFoods = this.setFoods.bind(this)
    this.setStaying = this.setStaying.bind(this)
    this.submit = this.submit.bind(this)
  }

  updateName(k) {
    return ({ target: { value } }) => {
      this.setState(prev => {
        const names = [...prev.names]
        names[k] = value
        return { names }
      }, this.validate())
    }
  }

  setEmail(e) {
    this.setState({ email: e.target.value }, this.validate())
  }

  addOne() {
    this.setState(
      prev => Object.assign({}, prev, { names: [...prev.names, ''] }),
      this.validate()
    )
  }

  remove(k) {
    return () =>
      this.setState(
        prev =>
          Object.assign({}, prev, {
            names: prev.names.filter((n, i) => i !== k)
          }),
        this.validate()
      )
  }

  setStaying(staying) {
    this.setState({ staying }, this.validate())
  }

  setFoods(e) {
    this.setState({ foods: e.target.value }, this.validate())
  }

  setAttending(attending) {
    this.setState({ attending }, this.validate())
  }

  validate(callback) {
    return () => {
      if (!this.state.submitted) {
        return
      }
      const multi = this.state.names.length > 1
      const you = multi ? 'ni' : 'du'
      const errors = {}
      if (
        this.state.names.length === 0 ||
        this.state.names.filter(n => n.length === 0).length > 0
      ) {
        errors.names =
          this.state.names.length <= 1
            ? 'Du måste säga vem du är!'
            : 'Ni måste säga vilka ni är!'
      }
      if (this.state.attending === 'not specified') {
        errors.attending = `Kommer ${you} eller inte?`
      } else if (this.state.attending === 'ja') {
        if (this.state.email.length === 0) {
          errors.email = `Vi behöver ett sett att kontakta ${
            multi ? 'er' : 'dig'
          }!`
        }
        if (this.state.staying === undefined) {
          errors.staying = `Vi behöver veta om ${you} vill sova på Granhedsgården!`
        }
      }
      this.setState({ errors }, callback)
    }
  }

  submit() {
    this.setState(
      { submitted: true },
      this.validate(() => {
        if (Object.values(this.state.errors).join('').length === 0) {
          const { names, email, attending, staying, foods } = this.state
          this.props.submit({ names, email, attending, staying, foods })
        }
      })
    )
  }

  render() {
    const props = {
      ...this.props,
      ...this.state
    }
    const multi = props.names.length >= 2
    const you = !multi ? 'du' : 'ni'
    const ime = !multi ? 'jag' : 'vi'
    const showAttending = props.attending === 'ja'
    const showNotAttending = props.attending === 'nej'
    const showSubmit = showAttending || showNotAttending

    const names = [...props.names.keys()].map(k => (
      <div className="name-line" key={`name_${k}`}>
        <NameInput name={props.names[k]} onChange={this.updateName(k)} />
        {k > 0 ? (
          <span className="remove-one" onClick={this.remove(k)}>
            <i className="fa fa-minus-square" />
          </span>
        ) : null}
      </div>
    ))

    return (
      <div>
        <Form>
          <div className="form-control">
            <label>
              <div
                className={`validation ${!!props.errors.names ? 'error' : ''}`}
              >
                {props.errors.names}
              </div>
              {`Vad heter ${you}?`}
              <div className="names">
                {names}
                <div className="name-line">
                  <span className="add-one" onClick={this.addOne}>
                    <i className="fa fa-plus-square" />
                  </span>
                </div>
              </div>
            </label>
          </div>

          <RadioGroup
            label={`Kommer ${you}?`}
            name="rsvp"
            error={props.errors.attending}
            options={[
              { value: 'ja', text: `Klart ${ime} gör!` },
              { value: 'nej', text: `Nej, ${ime} kan inte :(` }
            ]}
            onSelect={this.setAttending}
            selected={props.attending}
          />

          <TransitionGroup component="rsvp" className="rsvp">
            <CSSTransition
              key={`attending_${props.attending}`}
              classNames={{
                appear: 'fade-appear',
                enter: 'fade-enter',
                enterActive: 'fade-enter-active',
                appearActive: 'fade-appear-active'
              }}
              timeout={timeout}
            >
              <div>
                {showAttending && (
                  <div>
                    <p>Hurra, vad roligt!</p>
                    <p>Då behöver vi veta lite mer:</p>
                    <TextBox
                      name="email"
                      label={`Epost${multi ? ' (till någon av er)' : ''}:`}
                      onChange={this.setEmail}
                    />
                    <TextArea
                      name="food"
                      label={`Har ${
                        multi ? 'ni' : 'du'
                      } några specialmatsbehov?`}
                      onChange={this.setFoods}
                    />
                    <RadioGroup
                      label={`Vill ${
                        props.multi ? 'ni' : 'du'
                      } bo på Granhedsgården natten 1-2 september?`}
                      error={props.errors.staying}
                      onSelect={this.setStaying}
                      options={[
                        { value: 'ja', text: 'Ja, gärna!' },
                        {
                          value: 'nej',
                          text: `Nej, ${
                            props.multi ? 'vi' : 'jag'
                          } sover någon annanstans.`
                        }
                      ]}
                    />
                  </div>
                )}
                {showNotAttending && (
                  <div>
                    <p>
                      Vad tråkigt! Vi kommer sakna {multi ? 'er' : 'dig'}. Men
                      vi hoppas att vi ses nån annan gång!
                    </p>
                  </div>
                )}
                {showSubmit && (
                  <SubmitButton
                    text="Svara"
                    onClick={this.submit}
                    disabled={Object.values(props.errors).join('').length > 0}
                  />
                )}
              </div>
            </CSSTransition>
          </TransitionGroup>
        </Form>
      </div>
    )
  }
}

const stateToProps = state => ({
  rsvpFormState: state.rsvpFormState
})
const dispatchToProps = dispatch => ({
  submit: postRsvp(dispatch)
})

export default connect(stateToProps, dispatchToProps)(RsvpForm)
<file_sep>/app/src/components/menu/menu.jsx
import React from 'react'
import PropTypes from 'prop-types'
import { NavLink } from 'react-router-dom'
import MobileMenu from 'react-burger-menu/lib/menus/slide'

import './burger-menu.css'
import './menu.css'

const MenuItem = props => (
  <li>
    <NavLink exact to={props.url}>
      {props.text}
    </NavLink>
  </li>
)

MenuItem.propTypes = {
  url: PropTypes.string.isRequired,
  text: PropTypes.string.isRequired
}

const Menu = props => (
  <div>
    <nav className="full-menu">
      <ul>{props.links.map((l, i) => <MenuItem key={i} {...l} />)}</ul>
    </nav>
    <MobileMenu
      className="mobile-menu"
      pageWrapId="main"
      outerContainerId="content"
      right
    >
      <ul>{props.links.map((l, i) => <MenuItem key={i} {...l} />)}</ul>
    </MobileMenu>
  </div>
)

Menu.propTypes = {
  links: PropTypes.arrayOf(PropTypes.shape(MenuItem.propTypes)).isRequired
}

export default Menu
<file_sep>/post-rsvp/index.js
const sheets = require('./sheets')

exports.handler = (event, context, callback) => {
  const data = JSON.parse(event.body)
  sheets
    .authenticate()
    .then(doc => {
      const lines = data.names
        .map(name => ({
          Namn: name,
          Kommer: data.attending,
          Epost: data.email,
          Granhedsgården: data.staying,
          Mat: data.foods
        }))
        .map(sheets.addRow(doc))
      return Promise.all(lines)
    })
    .then(() =>
      callback(null, {
        body: JSON.stringify('OK'),
        statusCode: 200
      })
    )
    .catch(err => {
      callback(err, {
        body: JSON.stringify({ message: `Error: ${err.message}` }),
        statusCode: 500
      })
    })
}
