diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aff4c32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# IntelliJ +.idea +*.iml + +# Node +node_modules +npm-debug.log + +#Project +dist diff --git a/Gulpfile.js b/Gulpfile.js new file mode 100644 index 0000000..17bb423 --- /dev/null +++ b/Gulpfile.js @@ -0,0 +1,48 @@ +var gulp = require('gulp'); +var reactify = require('reactify'); +var browserify = require('browserify'); +var source = require('vinyl-source-stream'); +var del = require('del'); +var pkg = require('./package.json'); + +var paths = { + main: "./" + pkg.main, + js: ['app/js/**/*.js'], + statics: ['app/images', 'app/styles/**/*.*', 'app/index.html'], + dist: './dist' +}; + +var config = { + browserify: { + entries: [paths.main], + debug: true, + standalone: pkg.name, + extensions: ['.jsx', '.js'] + } +}; + +gulp.task('clean', function(done) { + del(['dist'], done); +}); + +gulp.task('copy', function() { + gulp.src(paths.statics) + .pipe(gulp.dest(paths.dist)) +}); + +gulp.task('js', function() { + browserify(config.browserify) + .transform(reactify) + .bundle() + .pipe(source('bundle.js')) + .pipe(gulp.dest(paths.dist + "/js/")); +}); + +gulp.task('watch', function() { + gulp.watch(paths.js, ['js']); + gulp.watch(paths.statics, ['copy']); +}); + +gulp.task('dist', ['js', 'copy']); + +gulp.task('default', ['watch', 'dist']); diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..aa422cc --- /dev/null +++ b/app/index.html @@ -0,0 +1,29 @@ + + + + React Hackathon + + + + + + + + + + + + + + + + + +
+ + + + + diff --git a/app/js/app.jsx b/app/js/app.jsx new file mode 100644 index 0000000..2403044 --- /dev/null +++ b/app/js/app.jsx @@ -0,0 +1,53 @@ +"use strict"; + +var React = require("react"); +var Http = require("./http"); + +var Search = require("./search"); +var ResultList = require("./results"); +var Map = require("./map"); +var Members = require("./members"); + +module.exports = React.createClass({ + displayName: 'App', + + getInitialState() { + return {}; + }, + + updateResults(results) { + this.setState({ results: results }); + }, + + resultSelected(result) { + this.setState({ + showResult: true, + result: result + }); + }, + + render() { + var comp; + if (this.state.showResult) { + comp = ( +
+ + +
+ ); + + } else if (this.state.results) { + + comp = ; + } + + return ( +
+
ReactJS Meetups
+ + {comp} +
+ ); + } +}); diff --git a/app/js/config.jsx b/app/js/config.jsx new file mode 100644 index 0000000..f878820 --- /dev/null +++ b/app/js/config.jsx @@ -0,0 +1,8 @@ +"use strict"; + +module.exports = { + meetup: { + searchUrl: "http://api.meetup.com/find/groups?key=f2f14303864524413516c4327452b2c", + membersUrl: "https://api.meetup.com/2/members?key=f2f14303864524413516c4327452b2c" + } +}; diff --git a/app/js/http.jsx b/app/js/http.jsx new file mode 100644 index 0000000..3bdf688 --- /dev/null +++ b/app/js/http.jsx @@ -0,0 +1,36 @@ +"use strict"; + +var request = require('superagent'); +var Immutable = require('immutable'); + +var Http = { + + /** + * Gets JSON data from a given URL. + */ + get(url, params, callback) { + if (!callback) { + callback = params; + params = {}; + } + + request + .get(url) + .query(params) + .end(function(result) { + if (result.ok) { + var pageData; + if (result.body) { + pageData = Immutable.fromJS(result.body); + } else { + pageData = Immutable.fromJS(JSON.parse(result.text)); + } + } + if (pageData) { + callback(pageData); + } + }); + } +}; + +module.exports = Http; diff --git a/app/js/index.jsx b/app/js/index.jsx new file mode 100644 index 0000000..caad3af --- /dev/null +++ b/app/js/index.jsx @@ -0,0 +1,11 @@ +"use strict"; + +var React = require("react"); +var App = require("./app"); + +React.initializeTouchEvents(true); + +React.render( + , + document.getElementById("app") +); diff --git a/app/js/map.jsx b/app/js/map.jsx new file mode 100644 index 0000000..1c9b286 --- /dev/null +++ b/app/js/map.jsx @@ -0,0 +1,64 @@ +"use strict"; + +var React = require('react'); + +module.exports = React.createClass({ + displayName: 'GoogleMapsChart', + + getInitialState: function() { + return { + map: undefined + }; + }, + + componentDidMount: function () { + this._initMap(); + this._insertMarkers(this.props.data); + }, + + _initMap() { + var mapOptions = { + center: {lat: 48.8721388, lng: 2.3411542}, // Mozilla Paris HQ + zoom: 10 + }; + var map = new google.maps.Map( + React.findDOMNode(this), + mapOptions + ); + + this.setState({ + map: map + }); + }, + + _insertMarkers(data) { + var map = this.state.map; + if (map && data) { + data.forEach((member) => { + this._addMarkerToMap(member); + }); + } + }, + + /** + * required properties in markerData + * lat: marker latitude + * lon: marker longitude + * name: onHover string for the marker + */ + _addMarkerToMap(markerData, map) { + new google.maps.Marker({ + position: new google.maps.LatLng(markerData.get('lat'), markerData.get('lon')), + map: map, + title: markerData.get('name') + }); + }, + + render() { + + return ( +
+
+ ) + } +}); diff --git a/app/js/members.jsx b/app/js/members.jsx new file mode 100644 index 0000000..417baf4 --- /dev/null +++ b/app/js/members.jsx @@ -0,0 +1,53 @@ +"use strict"; + +var React = require("react"); +var Http = require("./http"); +var config = require("./config"); + +module.exports = React.createClass({ + displayName: 'App', + + propTypes: { + groupId: React.PropTypes.object.isRequired + }, + + getInitialState() { + return {}; + }, + + + componentDidMount() { + Http.get(config.meetup.membersUrl, { "group_id": this.props.groupId }, response => { + + this.setState({ + cities: response + .get('results') + .groupBy((member) => member.get('city')) + .map((m, c) => { + return { + city: c, + total: m.count() + } + }) + }) + }) + }, + + render(){ + var cities = this.state.cities; + if (cities) { + var stats = cities.map((c, i) => +
+
{c.city}
+
{c.total}
+
+ ); + } + + return ( +
+ {stats} +
+ ); + } +}); diff --git a/app/js/results.jsx b/app/js/results.jsx new file mode 100644 index 0000000..7ce9347 --- /dev/null +++ b/app/js/results.jsx @@ -0,0 +1,64 @@ +"use strict"; + +var React = require("react"); + +var Result = React.createClass({ + + displayName: "Result", + + propTypes: { + data: React.PropTypes.object.isRequired, + resultSelected: React.PropTypes.func + }, + + onClick() { + var callback = this.props.resultSelected; + callback && callback(this.props.data); + }, + + render() { + var data = this.props.data; + return ( +
+
+ +
+
+
{data.get("name")}
+
+ {data.get("city")} + {data.get("country")} +
+
+
+ ); + } +}); + +var ResultList = React.createClass({ + + displayName: "ResultList", + + propTypes: { + data: React.PropTypes.object.isRequired, + resultSelected: React.PropTypes.func + }, + + + render() { + if (this.props.data) { + var results = this.props.data.map( + (result, index) => + ); + } + + return ( +
+ {results} +
+ ); + } +}); + +module.exports = ResultList; diff --git a/app/js/search.jsx b/app/js/search.jsx new file mode 100644 index 0000000..56ecd31 --- /dev/null +++ b/app/js/search.jsx @@ -0,0 +1,37 @@ +"use strict"; + +var React = require("react"); +var Http = require("./http"); +var config = require("./config"); + +module.exports = React.createClass({ + displayName: "Search", + + propTypes: { + onSearchResultsReceived: React.PropTypes.func + }, + + handleSearchChange() { + var value = React.findDOMNode(this.refs.search).value; + if (value && value.length > 3) { + Http.get(config.meetup.searchUrl, { "location": value }, response => { + var callback = this.props.onSearchResultsReceived; + + callback && callback(response); + }); + } + }, + + render() { + return ( +
+
Keywords:
+
+ + +
+
+ ); + } +}); diff --git a/app/styles/main.css b/app/styles/main.css new file mode 100644 index 0000000..4dad067 --- /dev/null +++ b/app/styles/main.css @@ -0,0 +1,54 @@ + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +.main .head { + font-size: large; + font-weight: bold; + text-align: center; +} + +.search .keywords { + font-size: small; +} + +.result { + display: flex; +} + +.result .thumb img { + width: 50px; + height: 50px; +} + +.result .details { + flex: 1; + padding-left: 5px; +} + +.result .city, .result .country { + color: #666; + font-size: small; + padding: 0 3px 0 0; +} + +.member { + clear: both; +} + +.member .city { + float: left; +} + +.member .total { + float: right; + font-weight: bold; +} + +.google-maps-chart { + height: 651px; + width: 367px; + margin: 0; + padding: 0; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e6c0273 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "ReactJS-Hackathon-App", + "version": "1.0.0", + "description": "ReactJS Hackathon App", + "author": "Nca-Team", + "license": "open-source", + "main": "app/js/index.jsx", + "repository": { + "type": "git", + "url": "https://github.com/oliverlaz/hackathon" + }, + "browserify": { + "transform": [ + ["reactify", { "es6": true }] + ] + }, + "dependencies": { + "babel-runtime": "^4.7.16", + "classnames": "^2.1.2", + "immutable": "^3.7.4", + "react": "^0.13.3", + "superagent": "^0.21.0" + }, + "devDependencies": { + "babelify": "^5.0.5", + "browserify": "^10.2.4", + "del": "~1.2.0", + "gulp": "^3.9", + "gulp-babel": "5.1.0", + "reactify": "^1.1.1", + "vinyl-source-stream": "^1.1.0" + }, + "scripts": { + "start": "gulp clean && gulp" + } +}