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) =>
+
+ );
+ }
+
+ 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"
+ }
+}