Add schedule/detail components for conference

This commit is contained in:
Matthew Taylor 2016-05-19 11:34:59 -04:00
parent b969c4e2ed
commit cad75217f0
10 changed files with 599 additions and 3 deletions

View file

@ -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>

View file

@ -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;
}
});
};
};

View file

@ -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;
}
});
};
};

View file

@ -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;

View file

@ -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/?",

View file

@ -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}>
&larr; 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} &ndash; {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}>
&larr; 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'));

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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} &ndash; {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} &ndash; {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'));

View file

@ -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%;
}
}
}
}
}