├── .gitignore ├── bs-config.js ├── src ├── main.js ├── AppDispatcher.js ├── components │ ├── mixins │ │ └── InputBinder.js │ ├── ArrowButton.js │ ├── FormAddDayEvent.js │ ├── GridMonth.js │ ├── Calendar.js │ ├── FormEditEvent.js │ ├── GridDay.js │ └── DetailsPane.js ├── stores │ ├── ChangeEmitter.js │ ├── UserSelectedStore.js │ └── EventStore.js └── actions │ ├── EventActions.js │ └── UserSelectedActions.js ├── webpack.config.js ├── bower.json ├── index.html ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | dist/ 4 | -------------------------------------------------------------------------------- /bs-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | files: ['./dist/*'], 3 | reloadDelay: 250, 4 | reloadDebounce: 2000, 5 | server: true 6 | }; -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Calendar = require('./components/Calendar'); 3 | 4 | React.render( 5 | , 6 | document.getElementById('calendar') 7 | ); -------------------------------------------------------------------------------- /src/AppDispatcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Register a singular Dispatcher. 3 | * 4 | * Reference: http://facebook.github.io/flux/docs/dispatcher.html#content 5 | */ 6 | var Dispatcher = require('flux').Dispatcher; 7 | 8 | module.exports = new Dispatcher(); -------------------------------------------------------------------------------- /src/components/mixins/InputBinder.js: -------------------------------------------------------------------------------- 1 | let React = require('react'); 2 | 3 | let InputBinder = React.createMixin({ 4 | bindInputValue(propName) { 5 | return (e) => this.setState({[propName]: e.target.value}); 6 | } 7 | }); 8 | 9 | module.exports = InputBinder; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | context: __dirname + "/src", 3 | entry: { 4 | main: "./main.js" 5 | }, 6 | output: { 7 | path: __dirname + "/dist/", 8 | filename: "[name].js", 9 | chunkFilename: "[id].js" 10 | }, 11 | module: { 12 | loaders: [ 13 | {test: /\.js$/, exclude: /node_modules/, loaders: ['babel-loader?optional=runtime&stage=1']} 14 | ] 15 | } 16 | }; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactCalendar", 3 | "version": "0.0.0", 4 | "authors": [ 5 | "Erik Aybar " 6 | ], 7 | "moduleType": [ 8 | "node" 9 | ], 10 | "license": "MIT", 11 | "private": true, 12 | "ignore": [ 13 | "**/.*", 14 | "node_modules", 15 | "bower_components", 16 | "test", 17 | "tests" 18 | ], 19 | "dependencies": { 20 | "bootstrap": "~3.3.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ReactCalendar 6 | 7 | 8 | 9 | 10 |
11 |

Calendar

12 |
13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/stores/ChangeEmitter.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | /** 5 | * @extends EventEmitter.prototype 6 | */ 7 | var CHANGE_EVENT = 'CHANGE_EVENT'; 8 | var ChangeEmitter = _.assign({}, EventEmitter.prototype, { 9 | emitChange() { 10 | this.emit(CHANGE_EVENT); 11 | }, 12 | 13 | addChangeListener(callback) { 14 | this.on(CHANGE_EVENT, callback); 15 | } 16 | }); 17 | 18 | module.exports = ChangeEmitter; -------------------------------------------------------------------------------- /src/components/ArrowButton.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var _ = require('lodash'); 3 | 4 | var _styles = { 5 | cursor: 'pointer' 6 | }; 7 | 8 | var ArrowButton = React.createClass({ 9 | propTypes: { 10 | onClick: React.PropTypes.func.isRequired, 11 | direction: React.PropTypes.oneOf(['left', 'right']).isRequired 12 | }, 13 | 14 | render() { 15 | return ( 16 | 20 | 21 | 22 | ) 23 | }, 24 | 25 | onClick(e) { 26 | e.preventDefault(); 27 | e.stopPropogation(); 28 | this.props.onClick(); 29 | } 30 | }); 31 | 32 | module.exports = ArrowButton; -------------------------------------------------------------------------------- /src/actions/EventActions.js: -------------------------------------------------------------------------------- 1 | var AppDispatcher = require('../AppDispatcher'); 2 | 3 | var _actions = { 4 | EVENT_CREATE: 'EVENT_CREATE', 5 | EVENT_UPDATE: 'EVENT_UPDATE', 6 | EVENT_DESTROY: 'EVENT_DESTROY' 7 | }; 8 | 9 | var EventActions = { 10 | create(eventData) { 11 | AppDispatcher.dispatch({ 12 | actionType: _actions.EVENT_CREATE, 13 | eventData: eventData 14 | }); 15 | }, 16 | 17 | update(eventId, eventData) { 18 | AppDispatcher.dispatch({ 19 | actionType: _actions.EVENT_UPDATE, 20 | eventId: eventId, 21 | eventData: eventData 22 | }); 23 | }, 24 | 25 | destroy(eventId) { 26 | AppDispatcher.dispatch({ 27 | actionType: _actions.EVENT_DESTROY, 28 | eventId: eventId 29 | }); 30 | } 31 | }; 32 | 33 | module.exports = EventActions; 34 | module.exports.actionNames = _actions; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactCalendar", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf ./dist", 8 | "build": "npm run clean && ./node_modules/.bin/webpack --optimize-minimize", 9 | "build-dev": "npm run clean && ./node_modules/.bin/webpack --watch --verbose", 10 | "server": "./node_modules/.bin/browser-sync start --config='./bs-config.js'", 11 | "develop": "npm run server & npm run build-dev" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "flux": "^2.0.1", 17 | "lodash": "^3.6.0", 18 | "moment": "^2.9.0", 19 | "react": "^0.13.1" 20 | }, 21 | "devDependencies": { 22 | "webpack": "^1.8.5", 23 | "browser-sync": "2.6.5", 24 | "babel-core": "^5.1.10", 25 | "babel-loader": "^5.0.0", 26 | "babel-runtime": "^5.1.13" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### To Run/Develop 2 | 3 | ``` 4 | npm install && bower install 5 | npm run develop 6 | # You may have to reload your browser manually, once. 7 | ``` 8 | 9 | See `package.json` for details :) 10 | 11 | ### 1st Attempt at Implementing [Facebook's Flux Architecture](http://facebook.github.io/flux/) 12 | 13 | Note: It was intentionally implemented **without** Flux first so that I could work through refactoring my *non-Flux hacks* into Flux. Also, this is my first shot/attempt at Flux. Don't judge me! 14 | 15 | ...TODO details/context :smile: 16 | 17 | Head on over to check out some of the [Pull Requests from building...](https://github.com/erikthedeveloper/react-calendar/pulls?q=is%3Apr+is%3Aclosed) 18 | 19 | some screenshots... 20 | 21 | ![image](https://cloud.githubusercontent.com/assets/1240178/7160977/b183da6e-e349-11e4-9492-484c86374804.png) 22 | 23 | ![image](https://cloud.githubusercontent.com/assets/1240178/7160990/c4becd96-e349-11e4-9695-eea1d6fb777d.png) 24 | 25 | ![image](https://cloud.githubusercontent.com/assets/1240178/7161021/eda23504-e349-11e4-90ae-8a9af29178b4.png) 26 | 27 | -------------------------------------------------------------------------------- /src/actions/UserSelectedActions.js: -------------------------------------------------------------------------------- 1 | var AppDispatcher = require('../AppDispatcher'); 2 | 3 | var _actions = { 4 | SELECT_DAY: 'SELECT_DAY', 5 | SELECT_EVENT: 'SELECT_EVENT', 6 | PANE_NAV_UP: 'PANE_NAV_UP', 7 | MONTH_NAV_PREV: 'MONTH_NAV_PREV', 8 | MONTH_NAV_NEXT: 'MONTH_NAV_NEXT' 9 | }; 10 | 11 | var UserSelectedActions = { 12 | 13 | selectDay(dateArgs) { 14 | AppDispatcher.dispatch({ 15 | actionType: _actions.SELECT_DAY, 16 | dateArgs: dateArgs 17 | }); 18 | }, 19 | 20 | selectEvent(eventId) { 21 | AppDispatcher.dispatch({ 22 | actionType: _actions.SELECT_EVENT, 23 | eventId: eventId 24 | }); 25 | }, 26 | 27 | paneNavUp() { 28 | AppDispatcher.dispatch({ 29 | actionType: _actions.PANE_NAV_UP 30 | }); 31 | }, 32 | 33 | monthNavPrev() { 34 | AppDispatcher.dispatch({ 35 | actionType: _actions.MONTH_NAV_PREV 36 | }); 37 | }, 38 | 39 | monthNavNext() { 40 | AppDispatcher.dispatch({ 41 | actionType: _actions.MONTH_NAV_NEXT 42 | }); 43 | } 44 | 45 | }; 46 | 47 | module.exports = UserSelectedActions; 48 | module.exports.actionNames = _actions; -------------------------------------------------------------------------------- /src/components/FormAddDayEvent.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var EventActions = require('../actions/EventActions'); 3 | let InputBinder = require('./mixins/InputBinder'); 4 | /** 5 | * 6 | */ 7 | var FormAddDayEvent = React.createClass({ 8 | mixins: [InputBinder], 9 | 10 | propTypes: { 11 | moment: React.PropTypes.object.isRequired 12 | }, 13 | 14 | getInitialState() { 15 | return { 16 | newTitle: '' 17 | } 18 | }, 19 | 20 | render() { 21 | return ( 22 |
23 | 29 | 34 |
35 | ); 36 | }, 37 | 38 | addEvent() { 39 | var titleInput = React.findDOMNode(this.refs['newEventTitle']); 40 | var newEventData = { 41 | title: this.state.newTitle.trim(), 42 | moment: this.props.moment 43 | }; 44 | 45 | if (newEventData.title.length === 0 ) 46 | return; 47 | 48 | EventActions.create(newEventData); 49 | titleInput.value = ""; 50 | titleInput.focus(); 51 | 52 | } 53 | }); 54 | 55 | module.exports = FormAddDayEvent; -------------------------------------------------------------------------------- /src/components/GridMonth.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var _ = require('lodash'); 3 | var moment = require('moment'); 4 | 5 | var EventStore = require('../stores/EventStore'); 6 | var UserSelectedStore = require('../stores/UserSelectedStore'); 7 | 8 | var GridDay = require('./GridDay'); 9 | var DummyDay = require('./GridDay').GridDayDummy; 10 | 11 | var styles = { 12 | dayHeading: { 13 | float: 'left', 14 | width: 100/7 + '%', 15 | textAlign: 'center' 16 | } 17 | }; 18 | 19 | var GridMonth = React.createClass({ 20 | 21 | propTypes: { 22 | selectedMoment: React.PropTypes.object.isRequired 23 | }, 24 | 25 | render() { 26 | var monthMoment = this.props.selectedMoment; 27 | return ( 28 |
29 | {moment.weekdaysShort().map((day) => 30 |
{day}
)} 31 | {this.renderDayBlocks(monthMoment)} 32 |
33 | ) 34 | }, 35 | 36 | renderDayBlocks(monthMoment) { 37 | var dayBlocks = []; 38 | var daysInMonth = monthMoment.daysInMonth(); 39 | var padDays = function (daysToPad) { 40 | while (daysToPad--) dayBlocks.push(); 41 | }; 42 | 43 | padDays(monthMoment.clone().date(1).day()); 44 | 45 | for (var i = 1; i <= daysInMonth; i++) { 46 | var dayMoment = moment(monthMoment).date(i); 47 | var isMonthMode = UserSelectedStore.getPaneType() !== 'month'; 48 | dayBlocks.push(); 54 | } 55 | 56 | padDays(6 - monthMoment.clone().date(daysInMonth).day()); 57 | 58 | return dayBlocks; 59 | } 60 | 61 | }); 62 | 63 | module.exports = GridMonth; -------------------------------------------------------------------------------- /src/components/Calendar.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var EventStore = require('../stores/EventStore'); 3 | var UserSelectedStore = require('../stores/UserSelectedStore'); 4 | var UserSelectedActions = require('../actions/UserSelectedActions'); 5 | var GridMonth = require('./GridMonth'); 6 | var DetailsPane = require('./DetailsPane'); 7 | var ArrowButton = require('./ArrowButton'); 8 | 9 | /** 10 | * Bootstrap 3 Grid Calendar component w/ self-contained "Details Pane" 11 | */ 12 | var Calendar = React.createClass({ 13 | 14 | render() { 15 | 16 | var monthMoment = this.state.selectedMoment; 17 | 18 | return ( 19 |
20 |
21 | 22 |

23 | 24 | {monthMoment.format('MMMM')} - {monthMoment.format('YYYY')} 25 | 26 |

27 | 28 | 29 |
30 |
31 | 32 |
33 |
34 | ) 35 | }, 36 | 37 | getInitialState() { 38 | return { 39 | selectedMoment: UserSelectedStore.getMoment(), 40 | selectedType: UserSelectedStore.getPaneType(), 41 | events: EventStore.getAll() 42 | } 43 | }, 44 | 45 | componentDidMount() { 46 | EventStore.addChangeListener(() => this.setState({events: EventStore.getAll()})); 47 | UserSelectedStore.addChangeListener(() => this.setState({ 48 | selectedMoment: UserSelectedStore.getMoment() 49 | })); 50 | }, 51 | 52 | prevMonth() { 53 | UserSelectedActions.monthNavPrev(); 54 | }, 55 | 56 | nextMonth() { 57 | UserSelectedActions.monthNavNext(); 58 | } 59 | 60 | }); 61 | 62 | module.exports = Calendar; -------------------------------------------------------------------------------- /src/stores/UserSelectedStore.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var moment = require('moment'); 3 | var AppDispatcher = require('../AppDispatcher'); 4 | var ActionNames = require('../actions/UserSelectedActions').actionNames; 5 | var ChangeEmitter = require('./ChangeEmitter'); 6 | var EventStore = require('./EventStore'); 7 | 8 | var _moment = moment(); 9 | var _paneType = 'month'; 10 | var _eventId; 11 | 12 | /** 13 | * @extends ChangeEmitter 14 | */ 15 | var UserSelectedStore = _.assign({}, ChangeEmitter, { 16 | 17 | /** 18 | * @return {moment} 19 | */ 20 | getMoment() { 21 | return _moment; 22 | }, 23 | 24 | /** 25 | * @return {string} 26 | */ 27 | getPaneType() { 28 | return _paneType; 29 | }, 30 | 31 | /** 32 | * @return {*} 33 | */ 34 | getEvent() { 35 | return EventStore.get(_eventId); 36 | } 37 | 38 | }); 39 | 40 | UserSelectedStore.dispatchToken = AppDispatcher.register(function (action) { 41 | 42 | switch (action.actionType) { 43 | case ActionNames.SELECT_DAY: 44 | _moment = moment(action.dateArgs); 45 | _paneType = 'day'; 46 | UserSelectedStore.emitChange(); 47 | break; 48 | 49 | case ActionNames.SELECT_EVENT: 50 | _eventId = action.eventId; 51 | _paneType = 'event'; 52 | _moment = EventStore.get(_eventId).moment; 53 | UserSelectedStore.emitChange(); 54 | break; 55 | 56 | case ActionNames.PANE_NAV_UP: 57 | _paneType = _paneType === 'event' 58 | ? 'day' 59 | : 'month'; 60 | UserSelectedStore.emitChange(); 61 | break; 62 | 63 | case ActionNames.MONTH_NAV_PREV: 64 | _moment.subtract(1, 'month'); 65 | _paneType = 'month'; 66 | UserSelectedStore.emitChange(); 67 | break; 68 | 69 | case ActionNames.MONTH_NAV_NEXT: 70 | _moment.add(1, 'month'); 71 | _paneType = 'month'; 72 | UserSelectedStore.emitChange(); 73 | break; 74 | } 75 | 76 | return true; 77 | 78 | }); 79 | 80 | module.exports = UserSelectedStore; -------------------------------------------------------------------------------- /src/components/FormEditEvent.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var EventActions = require('../actions/EventActions'); 3 | let InputBinder = require('./mixins/InputBinder'); 4 | 5 | /** 6 | * 7 | * Update title, date, and notes 8 | */ 9 | var FormEditEvent = React.createClass({ 10 | mixins: [InputBinder], 11 | 12 | propTypes: { 13 | event: React.PropTypes.object.isRequired 14 | }, 15 | 16 | componentWillMount() { 17 | this.setStateFromEvent(this.props.event); 18 | }, 19 | 20 | componentWillReceiveProps(nextProps) { 21 | this.setStateFromEvent(nextProps.event); 22 | }, 23 | 24 | setStateFromEvent(event) { 25 | this.setState({ 26 | title: event.title, 27 | dateArgs: event.moment.format("YYYY-MM-DD"), 28 | notes: event.notes || "" 29 | }) 30 | }, 31 | 32 | render() { 33 | return ( 34 |
35 | 43 | 49 |