From cad75217f096faaa65951c23b135280cd31df888 Mon Sep 17 00:00:00 2001 From: Matthew Taylor <mewtaylor@gmail.com> Date: Thu, 19 May 2016 11:34:59 -0400 Subject: [PATCH] Add schedule/detail components for conference --- .../navigation/conference/navigation.jsx | 3 + src/redux/conference-details.js | 80 +++++++++ src/redux/conference-schedule.js | 159 ++++++++++++++++++ src/redux/reducer.js | 5 + src/routes.json | 14 ++ src/views/conference/details/details.jsx | 88 ++++++++++ src/views/conference/details/details.scss | 29 ++++ src/views/conference/index/index.jsx | 8 +- src/views/conference/schedule/schedule.jsx | 122 ++++++++++++++ src/views/conference/schedule/schedule.scss | 94 +++++++++++ 10 files changed, 599 insertions(+), 3 deletions(-) create mode 100644 src/redux/conference-details.js create mode 100644 src/redux/conference-schedule.js create mode 100644 src/views/conference/details/details.jsx create mode 100644 src/views/conference/details/details.scss create mode 100644 src/views/conference/schedule/schedule.jsx create mode 100644 src/views/conference/schedule/schedule.scss diff --git a/src/components/navigation/conference/navigation.jsx b/src/components/navigation/conference/navigation.jsx index 68b5831c6..72ab5ff8b 100644 --- a/src/components/navigation/conference/navigation.jsx +++ b/src/components/navigation/conference/navigation.jsx @@ -24,6 +24,9 @@ var Navigation = React.createClass({ <li className="link plan"> <a href="/conference/plan">Plan Your Visit</a> </li> + <li className="link schedule"> + <a href="/conference/schedule">Schedule</a> + </li> </ul> </li> </ul> diff --git a/src/redux/conference-details.js b/src/redux/conference-details.js new file mode 100644 index 000000000..dece82fdd --- /dev/null +++ b/src/redux/conference-details.js @@ -0,0 +1,80 @@ +var keyMirror = require('keymirror'); +var api = require('../mixins/api.jsx').api; + +var Types = keyMirror({ + SET_DETAILS: null, + SET_DETAILS_FETCHING: null, + SET_DETAILS_ERROR: null +}); + +module.exports.detailsReducer = function (state, action) { + if (typeof state === 'undefined') { + state = {}; + } + switch (action.type) { + case Types.SET_DETAILS: + return action.details; + case Types.SET_DETAILS_FETCHING: + return {fetching: action.fetching}; + case Types.SET_DETAILS_ERROR: + return {error: action.error}; + default: + return state; + } +}; + +module.exports.setDetailsError = function (error) { + return { + type: Types.SET_DETAILS_ERROR, + error: error + }; +}; + +module.exports.setDetails = function (details) { + return { + type: Types.SET_DETAILS, + details: details + }; +}; + +module.exports.setDetailsFetching = function () { + return { + type: Types.SET_DETAILS_FETCHING, + fetching: true + }; +}; + +module.exports.startGetDetails = function (id) { + return function (dispatch) { + dispatch(module.exports.setDetailsFetching()); + dispatch(module.exports.getDetails(id)); + }; +}; + +module.exports.getDetails = function (id) { + return function (dispatch) { + api({ + uri: '/conference/' + id + '/details' + }, function (err, body) { + if (err) { + dispatch(module.exports.setDetailsError(err)); + return; + } + + if (typeof body !== 'undefined') { + var columns = body.columns; + if (body.rows) { + var details = body.rows[0]; + var detailsObject = details.reduce(function (prev, cur, index) { + prev[columns[index]] = cur; + return prev; + }, {}); + dispatch(module.exports.setDetails(detailsObject)); + } else { + dispatch(module.exports.setDetailsError('Not Found')); + } + return; + } + }); + }; +}; diff --git a/src/redux/conference-schedule.js b/src/redux/conference-schedule.js new file mode 100644 index 000000000..0877e9a57 --- /dev/null +++ b/src/redux/conference-schedule.js @@ -0,0 +1,159 @@ +var keyMirror = require('keymirror'); +var api = require('../mixins/api.jsx').api; + +var Types = keyMirror({ + SET_DAY: null, + SET_SCHEDULE: null, + SET_SCHEDULE_FETCHING: null, + SET_DAY_ERROR: null, + SET_SCHEDULE_ERROR: null +}); + +module.exports.dayReducer = function (state, action) { + if (typeof state === 'undefined') { + state = ''; + } + switch (action.type) { + case Types.SET_DAY: + return action.day; + case Types.SET_DAY_ERROR: + return state; + default: + return state; + } +}; + +module.exports.scheduleReducer = function (state, action) { + if (typeof state === 'undefined') { + state = []; + } + switch (action.type) { + case Types.SET_SCHEDULE: + return action.schedule; + case Types.SET_SCHEDULE_FETCHING: + return state; + case Types.SET_SCHEDULE_ERROR: + return state; + default: + return state; + } +}; + +module.exports.setDayError = function (error) { + return { + type: Types.SET_DAY_ERROR, + error: error + }; +}; + +module.exports.setDay = function (day) { + return { + type: Types.SET_DAY, + day: day + }; +}; + +module.exports.setSchedule = function (schedule) { + return { + type: Types.SET_SCHEDULE, + schedule: schedule + }; +}; + +module.exports.setScheduleFetching = function () { + return { + type: Types.SET_SCHEDULE_FETCHING, + fetching: true + }; +}; + +module.exports.setScheduleError = function (error) { + return { + type: Types.SET_SCHEDULE_ERROR, + error: error + }; +}; + +module.exports.startGetSchedule = function (day) { + return function (dispatch) { + dispatch(module.exports.setScheduleFetching()); + dispatch(module.exports.getDaySchedule(day)); + }; +}; + +/** + * Gets the schedule for the given day from the api + * @param {String} day Day of the conference (Thursday, Friday or Satrurday) + * + * @return {Object} Schedule for the day, broken into chunks + */ +module.exports.getDaySchedule = function (day) { + return function (dispatch) { + api({ + uri: '/conference/schedule/' + day + }, function (err, body) { + if (err) { + dispatch(module.exports.setDayError(err)); + dispatch(module.exports.setScheduleError(err)); + return; + } + + if (typeof body !== 'undefined') { + var columns = body.columns; + var rows = body.rows || []; + // Group events by the time period in which they occur (for presentation) + var scheduleByChunk = rows.reduce(function (prev, cur) { + var cleanedRow = {}; + for (var i = 0; i < columns.length; i++) { + if (cur[i].length > 0) { + cleanedRow[columns[i]] = cur[i]; + } + } + cleanedRow['uri'] = '/conference/' + cleanedRow.rowid + '/details'; + var chunk = cleanedRow.Chunk; + if (typeof prev.chunks[chunk] === 'undefined') { + prev.chunks[chunk] = [cleanedRow]; + prev.info.push({ + name: chunk, + time: cleanedRow.Start + }); + } else { + prev.chunks[chunk].push(cleanedRow); + } + return prev; + }, {chunks: [], info: []}); + // group periods of time by start time + scheduleByChunk.info.sort(function compare (a, b) { + var aAm = (a.time.substr(a.time.length - 1, a.time.length) === 'a') ? true : false; + var bAm = (b.time.substr(b.time.length - 1, b.time.length) === 'a') ? true : false; + var aTime = parseInt(a.time.substr(0, a.time.length - 1)); + var bTime = parseInt(b.time.substr(0, b.time.length - 1)); + + if (aTime < bTime) { + if (bAm && !aAm) { + return 1; + } else { + return -1; + } + } else { + if (aAm && !bAm) { + return -1; + } else { + return 1; + } + } + }); + var schedule = []; + for (var i = 0; i < scheduleByChunk.info.length; i++) { + schedule.push({ + info: scheduleByChunk.info[i], + items: scheduleByChunk.chunks[scheduleByChunk.info[i].name] + }); + } + dispatch(module.exports.setDay(day)); + dispatch(module.exports.setSchedule(schedule)); + return; + } + }); + }; +}; diff --git a/src/redux/reducer.js b/src/redux/reducer.js index 47fa7ccf9..356cb55ac 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -1,10 +1,15 @@ var combineReducers = require('redux').combineReducers; var authReducers = require('./auth.js'); +var conferenceScheduleReducers = require('./conference-schedule.js'); +var conferenceDetailsReducers = require('./conference-details.js'); var appReducer = combineReducers({ session: authReducers.sessionReducer, token: authReducers.tokenReducer, + day: conferenceScheduleReducers.dayReducer, + schedule: conferenceScheduleReducers.scheduleReducer, + details: conferenceDetailsReducers.detailsReducer }); module.exports = appReducer; diff --git a/src/routes.json b/src/routes.json index 8dbe3f90f..6bc5b2014 100644 --- a/src/routes.json +++ b/src/routes.json @@ -74,6 +74,20 @@ "title": "What to Expect", "viewportWidth": "device-width" }, + { + "name": "conference-schedule", + "pattern": "^/conference/schedule/?$", + "view": "conference/schedule/schedule", + "title": "Conference Schedule", + "viewportWidth": "device-width" + }, + { + "name": "conference-details", + "pattern": "^/conference/:id/details/?$", + "view": "conference/details/details", + "title": "Event Details", + "viewportWidth": "device-width" + }, { "name": "donate", "pattern": "^/info/donate/?", diff --git a/src/views/conference/details/details.jsx b/src/views/conference/details/details.jsx new file mode 100644 index 000000000..5e2632f56 --- /dev/null +++ b/src/views/conference/details/details.jsx @@ -0,0 +1,88 @@ +var classNames = require('classnames'); +var connect = require('react-redux').connect; +var React = require('react'); +var render = require('../../../lib/render.jsx'); + +var detailsActions = require('../../../redux/conference-details.js'); + +var Page = require('../../../components/page/conference/page.jsx'); + +require('./details.scss'); + +var ConferenceDetails = React.createClass({ + type: 'ConferenceDetails', + propTypes: { + detailsId: React.PropTypes.number, + details: React.PropTypes.object + }, + componentDidMount: function () { + var pathname = window.location.pathname.toLowerCase(); + if (pathname[pathname.length - 1] === '/') { + pathname = pathname.substring(0, pathname.length - 1); + } + var path = pathname.split('/'); + var detailsId = path[path.length - 2]; + this.props.dispatch(detailsActions.startGetDetails(detailsId)); + }, + render: function () { + var backUri = '/conference/schedule'; + if (!this.props.details.error && !this.props.details.fetching) { + backUri = backUri + '#' + this.props.details.Day; + } + var classes = classNames({ + 'inner': true, + 'details': true, + 'fetching': this.props.details.fetching + }); + return ( + <div className={classes}> + <div className="back"> + <a href={backUri}> + ← Back to Full Schedule + </a> + </div> + {this.props.details.error ? [ + <h2>Agenda Item Not Found</h2> + ] : [ + <h2>{this.props.details.Title}</h2>, + <ul className="logistics"> + <li> + {this.props.details.Presenter} + </li> + <li> + {this.props.details.Start} – {this.props.details.End} + </li> + <li> + {this.props.details.Type} + </li> + <li> + {this.props.details.Location} + </li> + </ul>, + <div className="description"> + {this.props.details.Description} + </div>, + <div className="back"> + {this.props.details.fetching ? [] : [ + <a href={backUri}> + ← Back to Full Schedule + </a> + ]} + </div> + ]} + </div> + ); + } +}); + +var mapStateToProps = function (state) { + return { + details: state.details, + fetching: state.fetching, + error: state.error + }; +}; + +var ConnectedDetails = connect(mapStateToProps)(ConferenceDetails); + +render(<Page><ConnectedDetails /></Page>, document.getElementById('app')); diff --git a/src/views/conference/details/details.scss b/src/views/conference/details/details.scss new file mode 100644 index 000000000..9b77199c8 --- /dev/null +++ b/src/views/conference/details/details.scss @@ -0,0 +1,29 @@ +.details { + &.inner { + margin-top: 2rem; + + &.fetching { + opacity: .6; + } + } + + .back { + margin: 1rem 0; + } + + ul { + &.logistics { + margin: .25rem 0 2.5rem; + padding-left: 0; + list-style-type: none; + } + + li { + margin: .25rem 0; + } + } + + .description { + margin: 2rem 0; + } +} diff --git a/src/views/conference/index/index.jsx b/src/views/conference/index/index.jsx index 770225b4e..3625345a8 100644 --- a/src/views/conference/index/index.jsx +++ b/src/views/conference/index/index.jsx @@ -55,11 +55,13 @@ var ConferenceSplash = React.createClass({ </div> <div> <h3> - <img src="/images/conference/schedule/coming-soon.png" alt="schedule-coming-soon" /> - Schedule + <a href="/conference/schedule"> + <img src="/images/conference/schedule/schedule.png" alt="schedule" /> + Schedule + </a> </h3> <p> - Stay tuned for the full schedule of events and sessions + Full schedule of events and sessions </p> </div> </FlexRow> diff --git a/src/views/conference/schedule/schedule.jsx b/src/views/conference/schedule/schedule.jsx new file mode 100644 index 000000000..854adc338 --- /dev/null +++ b/src/views/conference/schedule/schedule.jsx @@ -0,0 +1,122 @@ +var classNames = require('classnames'); +var connect = require('react-redux').connect; +var React = require('react'); +var render = require('../../../lib/render.jsx'); + +var scheduleActions = require('../../../redux/conference-schedule.js'); + +var FlexRow = require('../../../components/flex-row/flex-row.jsx'); +var Page = require('../../../components/page/conference/page.jsx'); +var SubNavigation = require('../../../components/subnavigation/subnavigation.jsx'); +var TitleBanner = require('../../../components/title-banner/title-banner.jsx'); + +require('./schedule.scss'); + +var ConferenceSchedule = React.createClass({ + type: 'ConferenceSchedule', + propTypes: { + day: React.PropTypes.string, + schedule: React.PropTypes.array + }, + componentDidMount: function () { + var day = window.location.hash.substr(1) || 'thursday'; + this.handleScheduleChange(day); + }, + handleScheduleChange: function (day) { + this.props.dispatch(scheduleActions.startGetSchedule(day)); + }, + renderChunkItems: function (chunk) { + return chunk.map(function (item) { + if (item.Presenter) { + return ( + <a href={item.uri} className="item-url"> + <div key={item.rowid} className="agenda-item"> + <h3>{item.Title}</h3> + <FlexRow> + <p>{item.Start} – {item.End}</p> + <p>{item.Location}</p> + </FlexRow> + <FlexRow> + <p>{item.Presenter}</p> + <p>{item.Type}</p> + </FlexRow> + </div> + </a> + ); + } else { + return ( + <div key={item.rowid} className="agenda-item no-click"> + <h3>{item.Title}</h3> + <FlexRow> + <p>{item.Start} – {item.End}</p> + <p>{item.Location}</p> + </FlexRow> + </div> + ); + } + }); + }, + render: function () { + var tabClasses = { + 'thursday': classNames({ + 'selected': (this.props.day === 'thursday') + }), + 'friday': classNames({ + 'selected': (this.props.day === 'friday') + }), + 'saturday': classNames({ + 'last': true, + 'selected': (this.props.day === 'saturday') + }) + }; + return ( + <div className="schedule"> + <TitleBanner> + <h1> + Schedule + </h1> + </TitleBanner> + <SubNavigation> + <li className={tabClasses.thursday} + onClick={this.handleScheduleChange.bind(this, 'thursday')}> + <img src="/svgs/conference/expect/aug4-icon.svg" alt="August 4th Icon" /> + <span>Thursday</span> + </li> + <li className={tabClasses.friday} + onClick={this.handleScheduleChange.bind(this, 'friday')}> + <img src="/svgs/conference/expect/aug5-icon.svg" alt="August 5th Icon" /> + <span>Friday</span> + </li> + <li className={tabClasses.saturday} + onClick={this.handleScheduleChange.bind(this, 'saturday')}> + <img src="/svgs/conference/expect/aug6-icon.svg" alt="August 6th Icon" /> + <span>Saturday</span> + </li> + </SubNavigation> + <div className="inner"> + {this.props.schedule.map(function (chunk) { + return ([ + <h2 key={chunk.info.name} className="breaking-title"> + <span>{chunk.info.name} – {chunk.info.time}</span> + </h2>, + this.renderChunkItems(chunk.items) + ]); + }.bind(this))} + </div> + </div> + ); + } +}); + +var mapStateToProps = function (state) { + return { + day: state.day, + schedule: state.schedule, + fetching: state.fetching, + error: state.error + }; +}; + +var ConnectedSchedule = connect(mapStateToProps)(ConferenceSchedule); + +render(<Page><ConnectedSchedule /></Page>, document.getElementById('app')); diff --git a/src/views/conference/schedule/schedule.scss b/src/views/conference/schedule/schedule.scss new file mode 100644 index 000000000..2f565de36 --- /dev/null +++ b/src/views/conference/schedule/schedule.scss @@ -0,0 +1,94 @@ +@import "../../../colors"; + +.schedule { + .title-banner { + margin-bottom: 0; + } + + .sub-nav { + z-index: -1; + box-shadow: 0 2px 5px $ui-dark-gray; + padding: 0; + + li { + margin: 0; + border: 0; + border-top: 4px solid transparent; + border-left: 2px solid $active-gray; + border-radius: 0; + color: $type-gray; + font-size: 1rem; + + &.last { + border-right: 2px solid $active-gray; + } + + &:hover, + &:active, + &.selected { + border-top: 4px solid $ui-orange; + border-left: 2px solid $active-gray; + box-shadow: none; + background-color: inherit; + padding: .75em 1em; + } + + &.selected { + font-weight: 700; + } + } + + img { + margin-right: .5em; + width: 2em; + vertical-align: middle; + } + } + + .inner { + h2 { + &.breaking-title { + margin: 4rem 0 2rem 0; + border-bottom: 1px solid $ui-dark-gray; + width: 100%; + height: 1.7rem; // match the line-height for h2 + text-align: center; + } + + span { + background-color: $ui-white; + padding: 0 10px; + } + } + + a.item-url { + display: block; + + &:hover { + background-color: lighten($ui-blue, 40); + } + } + + .agenda-item { + margin: 1rem 0; + border: 1px solid $active-gray; + border-radius: 5px; + padding: 1.25rem 2.25rem; + + &.no-click { + background-color: $ui-gray; + } + + .flex-row { + margin: .5rem 0; + justify-content: flex-start; + align-items: flex-start; + + p { + margin: 0; + width: 50%; + } + } + } + } +}