├── .gitignore
├── README.md
├── bundle.js
├── bundle.js.map
├── img
├── favicon-152.png
└── favicon.ico
├── index.html
├── map.geojson
├── params.json
├── select.css
└── web_client
├── App.js
├── Controller.js
├── Entry.js
├── NewEntryForm.js
├── package.json
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Example user template template
2 | ### Example user template
3 |
4 | # IntelliJ project files
5 | .idea
6 | out
7 | gen
8 | # Created by .ignore support plugin (hsz.mobi)
9 | web_client/node_modules
10 | web_client/build
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [GitMap Demo](https://idoco.github.io/GitMap/)
2 | **A location based job board powered only by GitHub.**
3 |
4 | This demo explores the possibility of building a "serverless" location based app by using only [GitHub's cool ability to render GeoJSON files](https://help.github.com/articles/mapping-geojson-files-on-github/) as interactive maps, and the powerful GitHub API (Nicely wrapped by [Github.js](https://github.com/michael/github)).
5 |
6 | The map view is rendered by GitHub from a [GeoJSON file](map.geojson) stored on GitHub pages and new entries are added to it by forking and creating a pull request on behalf of the submitting user. This GitHub "serverless" architecture is powered by using GitHub itself as the app's database and writing to it by using GitHub's API directly from the user's browser.
7 |
8 | 
9 |
10 | #### How GitMap Works?
11 | - The static site is served by GitHub pages.
12 | - The main page loads a `render.githubusercontent` iframe which renders the [map.geojson](map.geojson) as a full screen map.
13 | - When publishing a new entry, your browser will execute the calls to GitHub API by using [Github.js](https://github.com/michael/github)
14 | - A request will be sent by you to fork this repository.
15 | - The [map.geojson](map.geojson) file in the forked repository will be edited to contain the new published data.
16 | - A pull request will be created on your behalf, to merge the new data to the main repository.
17 | - Your pull request will be manually approved and merged, and the new data will show up on the map after a few moments.
18 |
19 | #### GitHub Authentication
20 | [GitHub OAuth](https://developer.github.com/v3/oauth/) requires that a secret client key will be used to convert the GitHub user login code to the authorization token. Since it is considered unsafe to expose this key, instead of storing it in the webapp I had set up a small [hook.io](https://hook.io/) hook to do the conversion using the secret key. Because this architecture is very irregular I am not sure that [the impact of exposing the client secret](http://tools.ietf.org/html/rfc6819#section-4.1.1) fully apply to it, so I will reconsider this in the future.
21 |
22 | #### Why is [map.geojson](https://github.com/idoco/GitMap/blob/gh-pages/map.geojson?short_path=5406685) filed with empty entries?
23 | This was a very simple trick to avoid merge conflicts issues. Every user adds his new entry in a random line in the file, which makes the chances of two users editing the same line simultaneously very slim. (The probability might still be higher than what you would expect as in the similar case of the [Birthday problem](https://en.wikipedia.org/wiki/Birthday_problem))
24 |
25 | #### My first project with react
26 | 
27 |
28 |
--------------------------------------------------------------------------------
/img/favicon-152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idoco/GitMap/a4e665a10f0f76fc6c10b63bfb9ad5570b470b73/img/favicon-152.png
--------------------------------------------------------------------------------
/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/idoco/GitMap/a4e665a10f0f76fc6c10b63bfb9ad5570b470b73/img/favicon.ico
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | GitMap
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
62 |
63 |
64 |
65 |
74 |
75 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/map.geojson:
--------------------------------------------------------------------------------
1 | {
2 | "type": "FeatureCollection",
3 | "features": [
4 | {},
5 | {},
6 | {},
7 | {},
8 | {},
9 | {},
10 | {
11 | "type": "Feature",
12 | "geometry": {
13 | "type": "Point",
14 | "coordinates": [
15 | "121.449",
16 | "31.187"
17 | ]
18 | },
19 | "properties": {
20 | "title": "PHP Engineer",
21 | "description": "PHP Software Engineer",
22 | "marker-symbol": "clothing-store",
23 | "marker-size": "large",
24 | "contact": "robbinhan "
25 | }
26 | },
27 | {},
28 | {
29 | "type": "Feature",
30 | "geometry": {
31 | "type": "Point",
32 | "coordinates": [
33 | "-2.099443",
34 | "53.484140"
35 | ]
36 | },
37 | "properties": {
38 | "title": "PHP Contractor",
39 | "description": "PHP Contractor",
40 | "marker-symbol": "lighthouse",
41 | "marker-size": "large",
42 | "contact": "Matthew Baggett "
43 | }
44 | },
45 | {},
46 | {
47 | "type": "Feature",
48 | "geometry": {
49 | "type": "Point",
50 | "coordinates": [
51 | -74.0065,
52 | 40.714
53 | ]
54 | },
55 | "properties": {
56 | "title": "",
57 | "description": "SpaceX was founded under the belief that a future where humanity is out exploring the stars is fundamentally more exciting than one where we are not. Today SpaceX is actively developing the technologies to make this possible, with the ultimate goal of enabling human life on Mars. Link ",
58 | "marker-symbol": "rocket",
59 | "marker-size": "large"
60 | }
61 | },
62 | {},
63 | {},
64 | {},
65 | {},
66 | {},
67 | {},
68 | {
69 | "type": "Feature",
70 | "geometry": {
71 | "type": "Point",
72 | "coordinates": [
73 | "101.112",
74 | "13.311"
75 | ]
76 | },
77 | "properties": {
78 | "title": "Senior Software Developer",
79 | "description": "How GitMap Works?\n\nThe static site is served by GitHub pages.\nThe main page loads a render.githubusercontent iframe which renders the map.geojson as a full screen map.\nWhen publishing a new entry, your browser will execute the calls to GitHub API by using Github.js\nA request will be sent by you to fork this repository.\nThe map.geojson file in the forked repository will be edited to contain the new published data.\nA pull request will be created on your behalf, to merge the new data to the main repository.\nYour pull request will be manually approved and merged, and the new data will show up on the map after a few moments.",
80 | "marker-symbol": "clothing-store",
81 | "marker-size": "large",
82 | "contact": "korrio "
83 | }
84 | },
85 | {},
86 | {},
87 | {},
88 | {},
89 | {},
90 | {},
91 | {},
92 | {
93 | "type": "Feature",
94 | "geometry": {
95 | "type": "Point",
96 | "coordinates": [
97 | "13.411",
98 | "52.521"
99 | ]
100 | },
101 | "properties": {
102 | "title": "JHipster Developer",
103 | "description": "JHipster is a Yeoman generator, used to create a Spring Boot + AngularJS project.\n\nGreetings, Java Hipster! ",
104 | "marker-symbol": "clothing-store",
105 | "marker-size": "large"
106 | }
107 | },
108 | {},
109 | {},
110 | {},
111 | {},
112 | {},
113 | {},
114 | {},
115 | {},
116 | {},
117 | {},
118 | {},
119 | {},
120 | {},
121 | {
122 | "type": "Feature",
123 | "geometry": {
124 | "type": "Point",
125 | "coordinates": [
126 | "-19.138",
127 | "64.108"
128 | ]
129 | },
130 | "properties": {
131 | "title": "Master Builder",
132 | "description": "Build stuff ",
133 | "marker-symbol": "rocket",
134 | "marker-size": "large",
135 | "contact": "Gurzalot "
136 | }
137 | },
138 | {
139 | "type": "Feature",
140 | "geometry": {
141 | "type": "Point",
142 | "coordinates": [
143 | "11.377",
144 | "45.560"
145 | ]
146 | },
147 | "properties": {
148 | "title": "Full-stack web developer",
149 | "description": "- NodeJS\n- Scala, Play Framework\n- PHP, Yii, Zend\n- MongoDB\n- Cassandra\n- SQL",
150 | "marker-symbol": "clothing-store",
151 | "marker-size": "large",
152 | "contact": "emanueleperuffo "
153 | }
154 | },
155 | {},
156 | {},
157 | {},
158 | {},
159 | {},
160 | {},
161 | {},
162 | {},
163 | {
164 | "type": "Feature",
165 | "geometry": {
166 | "type": "Point",
167 | "coordinates": [
168 | "17.099",
169 | "48.151"
170 | ]
171 | },
172 | "properties": {
173 | "title": "Full stack developer",
174 | "description": "C++, Node.js, Angular.js, React",
175 | "marker-symbol": "clothing-store",
176 | "marker-size": "large",
177 | "contact": "petersandor "
178 | }
179 | },
180 | {},
181 | {},
182 | {},
183 | {},
184 | {},
185 | {},
186 | {},
187 | {},
188 | {},
189 | {},
190 | {},
191 | {},
192 | {},
193 | {},
194 | {},
195 | {},
196 | {},
197 | {
198 | "type": "Feature",
199 | "geometry": {
200 | "type": "Point",
201 | "coordinates": [
202 | "-122.423",
203 | "37.772"
204 | ]
205 | },
206 | "properties": {
207 | "title": " ",
208 | "description": "PM-in-Residence \n $75k - $150k - No equity Product-in-Residence at GitMap \nBe a part of one of the most exciting teams, in one of the most exciting tech spaces, in one of the world’s most exciting cities for startups.",
209 | "marker-symbol": "rocket",
210 | "marker-size": "large"
211 | }
212 | },
213 | {},
214 | {},
215 | {},
216 | {},
217 | {},
218 | {},
219 | {},
220 | {},
221 | {},
222 | {},
223 | {},
224 | {},
225 | {},
226 | {},
227 | {},
228 | {},
229 | {},
230 | {},
231 | {},
232 | {},
233 | {},
234 | {},
235 | {},
236 | {},
237 | {},
238 | {
239 | "type": "Feature",
240 | "geometry": {
241 | "type": "Point",
242 | "coordinates": [
243 | "34.804",
244 | "32.061"
245 | ]
246 | },
247 | "properties": {
248 | "title": "Node developer",
249 | "description": "Bla bla Bla bla Bla bla Bla bla Bla bla Bla bla Bla bla Bla bla Bla bla Bla bla Bla bla ",
250 | "marker-symbol": "clothing-store",
251 | "marker-size": "large",
252 | "contact": "idoco "
253 | }
254 | },
255 | {},
256 | {},
257 | {},
258 | {},
259 | {},
260 | {},
261 | {},
262 | {},
263 | {},
264 | {},
265 | {
266 | "type": "Feature",
267 | "geometry": {
268 | "type": "Point",
269 | "coordinates": [
270 | "-118.442605",
271 | "34.036977"
272 | ]
273 | },
274 | "properties": {
275 | "title": "",
276 | "description": "We are looking for a sharp engineer who wants to tackle problems accross our entire stack: Big Data / processing. > API > Angular.js Frontend. You must be extremely comfortable with programming Javascript. Technology we’re using inlcudes Node.js, Golang Angular.js,Docker,Apache storm, Apache Spark and AWS.",
277 | "marker-symbol": "rocket",
278 | "marker-size": "large"
279 | }
280 | },
281 | {},
282 | {},
283 | {
284 | "type": "Feature",
285 | "geometry": {
286 | "type": "Point",
287 | "coordinates": [
288 | "7.580",
289 | "50.348"
290 | ]
291 | },
292 | "properties": {
293 | "title": "Web Developer",
294 | "description": "PHP, SQL, JavaScript, jQuery, HTML, CSS",
295 | "marker-symbol": "clothing-store",
296 | "marker-size": "large",
297 | "contact": "DarthDestroyer "
298 | }
299 | },
300 | {},
301 | {},
302 | {
303 | "type": "Feature",
304 | "geometry": {
305 | "type": "Point",
306 | "coordinates": [
307 | "80.226",
308 | "12.845"
309 | ]
310 | },
311 | "properties": {
312 | "title": "qbattles",
313 | "description": "Context aware social engagement app.",
314 | "marker-symbol": "rocket",
315 | "marker-size": "large",
316 | "contact": "itsgg "
317 | }
318 | },
319 | {},
320 | {},
321 | {
322 | "type": "Feature",
323 | "geometry": {
324 | "type": "Point",
325 | "coordinates": [
326 | "170.030",
327 | "-33.920"
328 | ]
329 | },
330 | "properties": {
331 | "title": "testtesttest",
332 | "description": "testtesttestsetsetsetset",
333 | "marker-symbol": "clothing-store",
334 | "marker-size": "large",
335 | "contact": "alisrbdni "
336 | }
337 | },
338 | {},
339 | {
340 | "type": "Feature",
341 | "geometry": {
342 | "type": "Point",
343 | "coordinates": [
344 | "25.1080001",
345 | "60.2642905"
346 | ]
347 | },
348 | "properties": {
349 | "title": "Enterpreneur",
350 | "description": "Web development and ecommerce startup.",
351 | "marker-symbol": "rocket",
352 | "marker-size": "large",
353 | "contact": "TomiToivio "
354 | }
355 | },
356 | {},
357 | {},
358 | {
359 | "type": "Feature",
360 | "geometry": {
361 | "type": "Point",
362 | "coordinates": [
363 | "23.195",
364 | "-33.938"
365 | ]
366 | },
367 | "properties": {
368 | "title": "Super Developer",
369 | "description": "Create super parts using super tools for a super product.",
370 | "marker-symbol": "rocket",
371 | "marker-size": "large",
372 | "contact": "dylanbr "
373 | }
374 | },
375 | {},
376 | {},
377 | {},
378 | {},
379 | {
380 | "type": "Feature",
381 | "geometry": {
382 | "type": "Point",
383 | "coordinates": [
384 | "14.421",
385 | "50.088"
386 | ]
387 | },
388 | "properties": {
389 | "title": "Martin Allien",
390 | "description": "Freelance Web/Graphic Designer | @AllienWorks",
391 | "marker-symbol": "clothing-store",
392 | "marker-size": "large",
393 | "contact": "allienworks "
394 | }
395 | },
396 | {
397 | "type": "Feature",
398 | "geometry": {
399 | "type": "Point",
400 | "coordinates": [
401 | "8.629",
402 | "50.083"
403 | ]
404 | },
405 | "properties": {
406 | "title": "Full Stack Web Developer",
407 | "description": "PHP, SQL, JavaScript, HTML, CSS, Amazon AWS",
408 | "marker-symbol": "clothing-store",
409 | "marker-size": "large"
410 | }
411 | },
412 | {},
413 | {},
414 | {},
415 | {},
416 | {},
417 | {},
418 | {},
419 | {},
420 | {},
421 | {},
422 | {},
423 | {
424 | "type": "Feature",
425 | "geometry": {
426 | "type": "Point",
427 | "coordinates": [
428 | "139.772",
429 | "35.671"
430 | ]
431 | },
432 | "properties": {
433 | "title": "Devices — Facebook Design Resources",
434 | "description": "Facebook resource.\n\nand simil",
435 | "marker-symbol": "rocket",
436 | "marker-size": "large"
437 | }
438 | },
439 | {},
440 | {},
441 | {},
442 | {},
443 | {},
444 | {
445 | "type": "Feature",
446 | "geometry": {
447 | "type": "Point",
448 | "coordinates": [
449 | "34.810",
450 | "32.084"
451 | ]
452 | },
453 | "properties": {
454 | "title": "XKCD Stack developer",
455 | "description": "Requirements:\nQBasic on Rails \nHypercard.js \nMicrosoft Bob Server® \nXKCD Stack ",
456 | "marker-symbol": "rocket",
457 | "marker-size": "large"
458 | }
459 | },
460 | {},
461 | {},
462 | {},
463 | {},
464 | {}
465 | ]
466 | }
--------------------------------------------------------------------------------
/params.json:
--------------------------------------------------------------------------------
1 | {"name":"Geojsonhack","tagline":"","body":"### GitHub Pages init\r\n\r\n","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."}
--------------------------------------------------------------------------------
/select.css:
--------------------------------------------------------------------------------
1 | /**
2 | * React Select
3 | * ============
4 | * Created by Jed Watson and Joss Mackison for KeystoneJS, http://www.keystonejs.com/
5 | * https://twitter.com/jedwatson https://twitter.com/jossmackison https://twitter.com/keystonejs
6 | * MIT License: https://github.com/keystonejs/react-select
7 | */
8 | .Select {
9 | position: relative;
10 | }
11 | .Select,
12 | .Select div,
13 | .Select input,
14 | .Select span {
15 | -webkit-box-sizing: border-box;
16 | -moz-box-sizing: border-box;
17 | box-sizing: border-box;
18 | }
19 | .Select.is-disabled > .Select-control {
20 | background-color: #f9f9f9;
21 | }
22 | .Select.is-disabled > .Select-control:hover {
23 | box-shadow: none;
24 | }
25 | .Select.is-disabled .Select-arrow-zone {
26 | cursor: default;
27 | pointer-events: none;
28 | }
29 | .Select-control {
30 | background-color: #fff;
31 | border-color: #d9d9d9 #ccc #b3b3b3;
32 | border-radius: 4px;
33 | border: 1px solid #ccc;
34 | color: #333;
35 | cursor: default;
36 | display: table;
37 | height: 36px;
38 | outline: none;
39 | overflow: hidden;
40 | position: relative;
41 | width: 100%;
42 | }
43 | .Select-control:hover {
44 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
45 | }
46 | .is-searchable.is-open > .Select-control {
47 | cursor: text;
48 | }
49 | .is-open > .Select-control {
50 | border-bottom-right-radius: 0;
51 | border-bottom-left-radius: 0;
52 | background: #fff;
53 | border-color: #b3b3b3 #ccc #d9d9d9;
54 | }
55 | .is-open > .Select-control > .Select-arrow {
56 | border-color: transparent transparent #999;
57 | border-width: 0 5px 5px;
58 | }
59 | .is-searchable.is-focused:not(.is-open) > .Select-control {
60 | cursor: text;
61 | }
62 | .is-focused:not(.is-open) > .Select-control {
63 | border-color: #007eff;
64 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 0 3px rgba(0, 126, 255, 0.1);
65 | }
66 | .Select-placeholder,
67 | :not(.Select--multi) > .Select-control .Select-value {
68 | bottom: 0;
69 | color: #aaa;
70 | left: 0;
71 | line-height: 34px;
72 | padding-left: 10px;
73 | padding-right: 10px;
74 | position: absolute;
75 | right: 0;
76 | top: 0;
77 | max-width: 100%;
78 | overflow: hidden;
79 | text-overflow: ellipsis;
80 | white-space: nowrap;
81 | }
82 | .has-value:not(.Select--multi) > .Select-control > .Select-value .Select-value-label,
83 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value .Select-value-label {
84 | color: #333;
85 | }
86 | .has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label,
87 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label {
88 | cursor: pointer;
89 | text-decoration: none;
90 | }
91 | .has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
92 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:hover,
93 | .has-value:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus,
94 | .has-value.is-pseudo-focused:not(.Select--multi) > .Select-control > .Select-value a.Select-value-label:focus {
95 | color: #007eff;
96 | outline: none;
97 | text-decoration: underline;
98 | }
99 | .Select-input {
100 | height: 34px;
101 | padding-left: 10px;
102 | padding-right: 10px;
103 | vertical-align: middle;
104 | }
105 | .Select-input > input {
106 | background: none transparent;
107 | border: 0 none;
108 | box-shadow: none;
109 | cursor: default;
110 | display: inline-block;
111 | font-family: inherit;
112 | font-size: inherit;
113 | height: 34px;
114 | margin: 0;
115 | outline: none;
116 | padding: 0;
117 | -webkit-appearance: none;
118 | }
119 | .is-focused .Select-input > input {
120 | cursor: text;
121 | }
122 | .has-value.is-pseudo-focused .Select-input {
123 | opacity: 0;
124 | }
125 | .Select-control:not(.is-searchable) > .Select-input {
126 | outline: none;
127 | }
128 | .Select-loading-zone {
129 | cursor: pointer;
130 | display: table-cell;
131 | position: relative;
132 | text-align: center;
133 | vertical-align: middle;
134 | width: 16px;
135 | }
136 | .Select-loading {
137 | -webkit-animation: Select-animation-spin 400ms infinite linear;
138 | -o-animation: Select-animation-spin 400ms infinite linear;
139 | animation: Select-animation-spin 400ms infinite linear;
140 | width: 16px;
141 | height: 16px;
142 | box-sizing: border-box;
143 | border-radius: 50%;
144 | border: 2px solid #ccc;
145 | border-right-color: #333;
146 | display: inline-block;
147 | position: relative;
148 | vertical-align: middle;
149 | }
150 | .Select-clear-zone {
151 | -webkit-animation: Select-animation-fadeIn 200ms;
152 | -o-animation: Select-animation-fadeIn 200ms;
153 | animation: Select-animation-fadeIn 200ms;
154 | color: #999;
155 | cursor: pointer;
156 | display: table-cell;
157 | position: relative;
158 | text-align: center;
159 | vertical-align: middle;
160 | width: 17px;
161 | }
162 | .Select-clear-zone:hover {
163 | color: #D0021B;
164 | }
165 | .Select-clear {
166 | display: inline-block;
167 | font-size: 18px;
168 | line-height: 1;
169 | }
170 |
171 | .Select-arrow-zone {
172 | cursor: pointer;
173 | display: table-cell;
174 | position: relative;
175 | text-align: center;
176 | vertical-align: middle;
177 | width: 25px;
178 | padding-right: 5px;
179 | }
180 | .Select-arrow {
181 | border-color: #999 transparent transparent;
182 | border-style: solid;
183 | border-width: 5px 5px 2.5px;
184 | display: inline-block;
185 | height: 0;
186 | width: 0;
187 | }
188 | .is-open .Select-arrow,
189 | .Select-arrow-zone:hover > .Select-arrow {
190 | border-top-color: #666;
191 | }
192 | @-webkit-keyframes Select-animation-fadeIn {
193 | from {
194 | opacity: 0;
195 | }
196 | to {
197 | opacity: 1;
198 | }
199 | }
200 | @keyframes Select-animation-fadeIn {
201 | from {
202 | opacity: 0;
203 | }
204 | to {
205 | opacity: 1;
206 | }
207 | }
208 | .Select-menu-outer {
209 | border-bottom-right-radius: 4px;
210 | border-bottom-left-radius: 4px;
211 | background-color: #fff;
212 | border: 1px solid #ccc;
213 | border-top-color: #e6e6e6;
214 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
215 | box-sizing: border-box;
216 | margin-top: -1px;
217 | max-height: 200px;
218 | position: absolute;
219 | top: 100%;
220 | width: 100%;
221 | z-index: 1;
222 | -webkit-overflow-scrolling: touch;
223 | }
224 | .Select-menu {
225 | max-height: 198px;
226 | overflow-y: auto;
227 | }
228 | .Select-option {
229 | box-sizing: border-box;
230 | background-color: #fff;
231 | color: #666666;
232 | cursor: pointer;
233 | display: block;
234 | padding: 8px 10px;
235 | }
236 | .Select-option:last-child {
237 | border-bottom-right-radius: 4px;
238 | border-bottom-left-radius: 4px;
239 | }
240 | .Select-option.is-focused {
241 | background-color: rgba(0, 126, 255, 0.08);
242 | color: #333;
243 | }
244 | .Select-option.is-disabled {
245 | color: #cccccc;
246 | cursor: default;
247 | }
248 | .Select-noresults {
249 | box-sizing: border-box;
250 | color: #999999;
251 | cursor: default;
252 | display: block;
253 | padding: 8px 10px;
254 | }
255 |
256 | @keyframes Select-animation-spin {
257 | to {
258 | transform: rotate(1turn);
259 | }
260 | }
261 | @-webkit-keyframes Select-animation-spin {
262 | to {
263 | -webkit-transform: rotate(1turn);
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/web_client/App.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var ReactDOM = require('react-dom');
3 | var Modal = require('react-modal');
4 |
5 | var NewEntryForm = require('./NewEntryForm');
6 |
7 | var Controller = require('./Controller');
8 | var controller = new Controller();
9 |
10 |
11 | var appElement = document.getElementById('example');
12 |
13 | const customStyles = {
14 | overlay: {
15 | zIndex: 100
16 | },
17 | content : {
18 | top : '50%',
19 | left : '50%',
20 | right : 'auto',
21 | bottom : 'auto',
22 | marginRight : '-50%',
23 | transform : 'translate(-50%, -50%)',
24 | zIndex: 101
25 | }
26 | };
27 | const linkMargin = {'margin-left': '4px', 'margin-right': '4px'};
28 |
29 | const geoJsonContent = "?url=https://raw.githubusercontent.com/idoco/GitMap/gh-pages/map.geojson";
30 | const renderMapUrl = "https://render.githubusercontent.com/view/geojson"+ geoJsonContent;
31 |
32 | var App = React.createClass({
33 |
34 | getInitialState: function() {
35 | console.log(controller.isUrlWithCode());
36 | return {
37 | modalIsOpen: controller.isUrlWithCode(),
38 | isAboutOpen: !controller.isUrlWithCode()
39 | };
40 | },
41 |
42 | openModal: function() {
43 | this.setState({modalIsOpen: true});
44 | },
45 |
46 | closeModal: function() {
47 | this.setState({modalIsOpen: false});
48 | },
49 |
50 | closeAbout: function() {
51 | this.setState({isAboutOpen: false});
52 | },
53 |
54 | render: function() {
55 | return (
56 |
57 |
58 |
59 |
64 |
65 |
66 |
69 | refresh
70 |
71 |
72 |
75 | add_location
76 |
77 |
78 |
82 |
86 |
87 |
88 |
92 |
93 |
GitMap
94 |
95 |
96 | This demo explores the possibility of building a "serverless" location based app by
97 | using only
98 |
100 | GitHub's cool ability to render GeoJSON files
101 | as interactive maps, and the
102 | powerful GitHub API (Nicely wrapped by
103 |
105 | Github.js
106 | ).
107 |
108 |
109 | The map view is rendered by GitHub from a
110 |
112 | GeoJSON file
113 |
114 | stored on GitHub pages and new entries are added to it by forking and creating a pull
115 | request on behalf of the submitting user. This GitHub "serverless" architecture is
116 | powered by using GitHub itself as the app's database and writing to it by using GitHub's
117 | API directly from the user's browser.
118 |
119 |
120 |
122 | Close
123 |
124 |
125 |
126 |
127 |
128 |
129 | );
130 | }
131 | });
132 |
133 | ReactDOM.render( , appElement);
134 |
--------------------------------------------------------------------------------
/web_client/Controller.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Created by idoco on 29/1/2016.
3 | * buttons logic
4 | */
5 |
6 | var Github = require("github-api");
7 | var request = require('superagent');
8 |
9 | var Entry = require("./Entry");
10 |
11 | function Controller() {
12 |
13 | var github, mainRepo, forkedRepo,
14 | authToken, username, entry, onPullRequestReady,
15 | retries = 10;
16 |
17 | function refreshMap() {
18 | var iFrame = document.getElementById("mapFrame");
19 | //noinspection SillyAssignmentJS
20 | iFrame.src = iFrame.src;
21 | }
22 |
23 | function postNewEntry(data) {
24 | if (authToken) {
25 | startGitHubFlow(data);
26 | } else {
27 | acquireAuthToken(data);
28 | }
29 | }
30 |
31 | function isTokenReady() {
32 | return authToken;
33 | }
34 |
35 | function isUrlWithCode() {
36 | return window.location.href.indexOf('?code') >= 0;
37 | }
38 |
39 | function acquireAuthToken(data) {
40 | var queryString = window.location.href.slice(window.location.href.indexOf('?code') + 1).split('=');
41 | authToken = queryString[1];
42 | window.history.replaceState({}, 'GitMap', window.location.href.slice(0, window.location.href.indexOf('?')));
43 |
44 | // the cake is a lie
45 | // I can eliminate this call by exposing the client_secret in the web_client, I wonder if that would be ok
46 | request.get('https://hook.io/idoco/github-doorman?code=' + authToken)
47 | .end(function (err, res) {
48 | if (err) return reportError(err);
49 | if (res.body.token === '') return reportError("One-time token already used");
50 |
51 | github = new Github({
52 | token: res.body.token,
53 | auth: "oauth"
54 | });
55 |
56 | github.getUser().show(null, function (err, user) {
57 | if (err) return reportError(err);
58 | username = user.login;
59 | startGitHubFlow(data);
60 | });
61 | });
62 | }
63 |
64 | function startGitHubFlow(data) {
65 | entry = data.entry;
66 | onPullRequestReady = data.callback;
67 |
68 | try {
69 | Entry.validateEntry(entry);
70 | } catch (e) {
71 | return reportError(e);
72 | }
73 |
74 | mainRepo = github.getRepo("idoco", "GitMap");
75 | mainRepo.fork(function (err) {
76 | if (err) return reportError(err);
77 | pollForFork();
78 | });
79 | }
80 |
81 | // loop 10 times to validate that the fork operation ended successfully
82 | function pollForFork() {
83 | forkedRepo = github.getRepo(username, "GitMap");
84 |
85 | forkedRepo.contents('gh-pages', "map.geojson",
86 | function (err) {
87 | if (err && retries) {
88 | console.error(err);
89 | retries--;
90 | setTimeout(pollForFork, 100);
91 | } else if (err) {
92 | return reportError(err);
93 | } else {
94 | readMapFile();
95 | }
96 | }
97 | );
98 | }
99 |
100 | function readMapFile() {
101 | mainRepo.read('gh-pages', 'map.geojson',
102 | function (err, geojson) {
103 | if (err) return reportError(err);
104 | editMapFile(geojson);
105 | }
106 | );
107 | }
108 |
109 | function editMapFile(geojson) {
110 |
111 | // pushing the new entry in a random index in the list should make automatic merging simpler
112 | var features = geojson.features;
113 | var randomIndexInFeaturesArray = Math.floor(Math.random() * (features.length-1));
114 | entry.properties.contact = ""+username+" ";
115 | features.splice(randomIndexInFeaturesArray, 0, entry);
116 |
117 | var options = {
118 | committer: {name: username, email: username + '@unknown.com'},
119 | encode: true // Whether to base64 encode the file. (default: true)
120 | };
121 |
122 | var jsonString = JSON.stringify(geojson, null, 4);
123 | forkedRepo.write('gh-pages', 'map.geojson', jsonString, 'Adding entry to map', options,
124 | function (err) {
125 | if (err) return reportError(err);
126 | createPullRequest();
127 | }
128 | );
129 | }
130 |
131 | function createPullRequest() {
132 | var pull = {
133 | title: "New entries by " + username,
134 | body: "This pull request has been automatically generated by Github.js",
135 | base: "gh-pages",
136 | head: username + ":" + "gh-pages"
137 | };
138 |
139 | mainRepo.createPullRequest(pull,
140 | function (err, pullRequest) {
141 | if (err) return reportError(err);
142 | onPullRequestReady({pullRequestUrl: pullRequest.html_url});
143 | }
144 | );
145 | }
146 |
147 | function reportError(err){
148 | console.error(err);
149 | if (err && err.request && err.request.responseText){
150 | try {
151 | var responseText = JSON.parse(err.request.responseText);
152 | if (responseText.errors && responseText.errors[0] && responseText.errors[0].message) {
153 | onPullRequestReady({err: "" + responseText.errors[0].message});
154 | } else {
155 | onPullRequestReady({err: "" + responseText.message});
156 | }
157 | } catch (e) {
158 | onPullRequestReady({err: "" + err.request.responseText});
159 | }
160 | } else {
161 | onPullRequestReady({err: ""+err});
162 | }
163 | }
164 |
165 | return {
166 | refreshMap: refreshMap,
167 | postNewEntry: postNewEntry,
168 | isTokenReady: isTokenReady,
169 | isUrlWithCode: isUrlWithCode
170 | };
171 | }
172 |
173 | module.exports = Controller;
174 |
175 |
--------------------------------------------------------------------------------
/web_client/Entry.js:
--------------------------------------------------------------------------------
1 | function Entry(options) {
2 | this.type = "Feature";
3 | this.geometry = {
4 | "type": "Point",
5 | "coordinates": [
6 | options.lng,
7 | options.lat
8 | ]
9 | };
10 | this.properties = {
11 | "title": options.title,
12 | "description": options.description,
13 | "marker-symbol": options.symbol,
14 | "marker-size": "large"
15 | };
16 | }
17 |
18 | Entry.validateEntry = function (entry) {
19 | var symbols = ["rocket", "industrial", "clothing-store"];
20 |
21 | var lng = entry.geometry.coordinates[0];
22 | var lat = entry.geometry.coordinates[1];
23 | var title = entry.properties.title;
24 | var description = entry.properties.description;
25 | var symbol = entry.properties['marker-symbol'];
26 |
27 | if (isNaN(lat) || isNaN(lng)) {
28 | throw "coordinates must be numbers";
29 | }
30 |
31 | if (lat > 90 || lat < -90 || lng > 180 || lng < -180) {
32 | throw "coordinates out of range";
33 | }
34 |
35 | if (symbols.indexOf(symbol) < 0) {
36 | throw "Not a valid symbol";
37 | }
38 |
39 | if (typeof title != 'string' || typeof description != 'string') {
40 | throw "Content must be text";
41 | }
42 |
43 | if (title.length > 100) {
44 | throw "Title is too long";
45 | }
46 |
47 | if (description.length > 2000) {
48 | throw "Description is too long";
49 | }
50 |
51 | if (title.length < 5) {
52 | throw "Title is too short";
53 | }
54 |
55 | if (description.length < 20) {
56 | throw "Description is too short";
57 | }
58 | };
59 |
60 | // used for testing
61 | Entry.createRandomEntry = function () {
62 | var symbols = ["rocket", "industrial", "clothing-store"];
63 | var randomFiveCharString = Math.random().toString(36).substr(2,5);
64 | var randomLat = (90 * Math.random() - 22.5).toFixed(3);
65 | var randomLng = (180 * Math.random() - 90).toFixed(3);
66 | var randomSymbol = symbols[Math.floor(Math.random()*symbols.length)];
67 |
68 | return new Entry({
69 | "lat": randomLat,
70 | "lng": randomLng,
71 | "title": "This is the title " + randomFiveCharString,
72 | "description": "And this is a long description " + randomFiveCharString,
73 | "symbol": randomSymbol
74 | });
75 | };
76 |
77 | module.exports = Entry;
78 |
--------------------------------------------------------------------------------
/web_client/NewEntryForm.js:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 | var Geocoder = require('react-select-geocoder');
3 |
4 | var Entry = require("./Entry");
5 |
6 | var NewEntryForm = React.createClass({
7 |
8 | getInitialState: function() {
9 | return {
10 | requestState: 'notSent',
11 | pullRequestUrl: '',
12 | err: ''
13 | };
14 | },
15 |
16 | componentDidUpdate: function() {
17 | componentHandler.upgradeDom();
18 | },
19 |
20 | componentDidMount: function() {
21 | componentHandler.upgradeDom();
22 | },
23 |
24 | submitForm: function() {
25 | var entry = new Entry({
26 | "lat": this.refs.lat.value,
27 | "lng": this.refs.lng.value,
28 | "title": this.refs.title.value,
29 | "description": this.refs.description.value,
30 | "symbol": this.refs.radio_startup.checked ? this.refs.radio_startup.value : this.refs.radio_job.value
31 | });
32 |
33 | var data = {
34 | entry : entry,
35 | callback : this.onPullRequestReady
36 | };
37 |
38 | console.info(data);
39 | this.setState({
40 | requestState: 'loading'
41 | });
42 | this.props.postNewEntry(data);
43 | },
44 |
45 | getUserLocation: function() {
46 | if (navigator.geolocation) {
47 | navigator.geolocation.getCurrentPosition(this.setLocation);
48 | } else {
49 | this.setState({
50 | requestState: 'error',
51 | err: 'Geolocation is not supported by this browser.'
52 | });
53 | }
54 | },
55 |
56 | getSelectedLocation: function(position){
57 | this.setLocation({
58 | coords :{
59 | longitude: Number(position.geometry.coordinates[0]),
60 | latitude: Number(position.geometry.coordinates[1])
61 | }
62 | });
63 | },
64 |
65 | setLocation: function(position) {
66 | document.getElementById("lat_input").value = position.coords.latitude.toFixed(3);
67 | document.getElementById("lat_div").className += " is-dirty";
68 |
69 | document.getElementById("lng_input").value = position.coords.longitude.toFixed(3);
70 | document.getElementById("lng_div").className += " is-dirty";
71 | componentHandler.upgradeDom();
72 | },
73 |
74 | onPullRequestReady: function(data) {
75 | if (data.err) {
76 | this.setState({
77 | requestState: 'error',
78 | err: data.err
79 | });
80 | } else {
81 | this.setState({
82 | requestState: 'ready',
83 | pullRequestUrl: data.pullRequestUrl
84 | });
85 | }
86 | },
87 |
88 | render: function() {
89 |
90 | if (!this.props.isTokenReady() && !this.props.isUrlWithCode()){
91 | return (
92 |
97 | );
98 | }
99 |
100 | return (
101 |
102 |
163 |
166 | Publish
167 |
168 |
169 | {(function(state) {
170 | if (!state.requestState) {
171 | return (
);
172 | } else if (state.requestState == 'loading') {
173 | return (
174 |
);
179 | } else if (state.requestState == 'ready') {
180 | return (
);
181 | } else if (state.requestState == 'error') {
182 | return (
{state.err}
);
183 | }
184 | })(this.state)}
185 |
186 |
187 | );
188 | }
189 | });
190 |
191 | module.exports = NewEntryForm;
--------------------------------------------------------------------------------
/web_client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_test",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "main.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "dependencies": {
12 | "github-api": "0.11.2",
13 | "react": "^0.14.7",
14 | "react-dom": "^0.14.7",
15 | "react-modal": "0.6.1",
16 | "react-select-geocoder": "0.0.4",
17 | "superagent": "1.7.2"
18 | },
19 | "devDependencies": {
20 | "babel-core": "^6.4.5",
21 | "babel-loader": "^6.2.0",
22 | "babel-preset-react": "^6.3.13",
23 | "babel-preset-es2015": "^6.3.13",
24 | "babel-preset-stage-0": "^6.3.13",
25 | "jshint": "^2.6.0",
26 | "jsxhint": "^0.15.1",
27 | "jsxhint-loader": "^0.2.0",
28 | "webpack": "^2.0.6-beta"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/web_client/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 |
3 | module.exports = {
4 | entry: "./App.js",
5 | output: {
6 | filename: "../bundle.js"
7 | },
8 | module: {
9 | preLoaders: [
10 | {
11 | test: /\.js$/,
12 | exclude: /node_modules/,
13 | loader: 'jsxhint-loader'
14 | }
15 | ],
16 | loaders: [
17 | {
18 | exclude: /node_moudles/,
19 | loader: 'babel',
20 | query: {
21 | presets:['react']
22 | }
23 | }
24 | ]
25 | },
26 | plugins: [
27 | new webpack.DefinePlugin({
28 | 'process.env': {
29 | 'NODE_ENV': JSON.stringify("production")
30 | }
31 | }),
32 | new webpack.optimize.UglifyJsPlugin({
33 | compress: {
34 | warnings: false
35 | }
36 | })
37 | ],
38 |
39 | devtool: 'source-map',
40 | jshint: {
41 | esversion: 6
42 | }
43 | };
--------------------------------------------------------------------------------