├── .gitignore
├── Procfile
├── app
├── app.js
├── components
│ ├── city.js
│ └── labeledSlider.js
├── intent.js
├── main.js
├── mockResponse.json
├── model.js
├── view.js
└── views
│ ├── cities.js
│ ├── filters.js
│ └── forecast.js
├── cities.js
├── heroku.js
├── package.json
├── public
├── img
│ ├── arrowdown.gif
│ ├── black_logo.png
│ └── white_logo.png
└── index.html
├── server.js
├── test
└── app_spec.js
├── webpack-prod.config.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | public/bundle.js
3 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: node server.js
2 |
--------------------------------------------------------------------------------
/app/app.js:
--------------------------------------------------------------------------------
1 | /** @jsx hJSX */
2 | import {hJSX} from '@cycle/dom';
3 | import {Rx} from '@cycle/core';
4 | import intent from './intent'
5 | import model from './model'
6 | import view from './view';
7 | let Ob$ = Rx.Observable;
8 |
9 | export default function app ({DOM, HTTP}) {
10 |
11 | let actions = intent(DOM);
12 | let state$ = model(actions, HTTP);
13 | let vtree$ = view(state$);
14 |
15 | //state$.subscribe(x => console.log(x));
16 | return {
17 | DOM: vtree$,
18 | HTTP: Ob$.just('cities')
19 | };
20 | }
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/components/city.js:
--------------------------------------------------------------------------------
1 | /** @jsx hJSX */
2 | import Cycle from '@cycle/core';
3 | import {hJSX} from '@cycle/dom';
4 | import renderForecast from '../views/forecast';
5 |
6 | let Ob$ = Cycle.Rx.Observable;
7 |
8 | export default function city(responses) {
9 |
10 | function intent(DOM){
11 | return {
12 | toggleDetails$ : DOM.select('#toggle').events('click')
13 | };
14 | }
15 |
16 | function model(context, actions) {
17 | // action (toggle) is not used
18 | return Ob$.combineLatest(
19 | actions.toggleDetails$.startWith('none').scan((x, y) => {
20 | return x === 'flex' ? 'none' : 'flex';
21 | }),
22 | context.props.getAll(),
23 | (toggle, props) => ({toggle,props})
24 | );
25 | }
26 |
27 | function view(state$){
28 |
29 | return state$.map(state => {
30 |
31 | let { forecasts, name, minSun, minLow, maxHigh, minHigh, timespan } = state.props;
32 | let deg = '$ordm;';
33 |
34 | if(name === "Trevi") { name = "Rome"; }
35 | if(name === "San Nicolas") { name = "Buenos Aires"; }
36 | if(name === "Britanski trg") { name = "Zagreb"; }
37 | if(name === "Floriana") { name = "Malta"; }
38 |
39 | return (
40 |
41 |
{name}
42 |
From { timespan }
43 |
44 | { maxHigh !== minHigh ?
45 |
46 | Day Highs from { minHigh }ºC to { maxHigh }ºC
47 |
:
48 |
Highs of { maxHigh }ºC
49 | }
50 |
51 |
Lowest at night: { minLow }ºC
52 |
53 | { minSun > 29 ?
54 |
At least {minSun}% clear sky each day.
:
55 |
At least one overcast day with only {minSun}% clear sky.
56 | }
57 |
58 |
59 | );
60 | });
61 | }
62 |
63 | let actions = intent(responses.DOM);
64 | let state$ = model(responses, actions);
65 | let vtree$ = view(state$)
66 |
67 | return {
68 | DOM: vtree$
69 | }
70 | }
71 |
72 | let sty = {
73 | 'container': {
74 | 'flex': '0 0 250px',
75 | 'marginTop': '1em',
76 | 'backgroundColor': '#C2EAE9',
77 | 'padding': '1.3em',
78 | 'borderRadius': '25px'
79 | //'border': '3px dotted #F8EFB6'
80 | },
81 | 'name': {
82 | 'margin': '0',
83 | 'marginBottom': '0.3em'
84 | },
85 | 'dim': {
86 | 'color': '#777'
87 | }
88 | };
89 |
90 |
91 |
--------------------------------------------------------------------------------
/app/components/labeledSlider.js:
--------------------------------------------------------------------------------
1 | /** @jsx hJSX */
2 | import Cycle from '@cycle/core';
3 | import {hJSX} from '@cycle/dom';
4 |
5 | let Ob$ = Cycle.Rx.Observable;
6 |
7 | export default function labeledSlider(responses) {
8 |
9 | function intent(DOM) {
10 | return {
11 | changeValue$ : DOM.select('.slider').events('input')
12 | .debounce(10)
13 | .map(e => e.target.value)
14 | }
15 | }
16 |
17 | function model(context, actions) {
18 | let initialValue$ = context.props.get('initial').first();
19 | let value$ = initialValue$.concat(actions.changeValue$);
20 | let props$ = context.props.getAll();
21 | return Ob$.combineLatest(props$, value$,
22 | (props, value) => ({props, value})
23 | );
24 | }
25 |
26 | function view(state$) {
27 | return state$.map(state => {
28 | let { label, min, max, mea } = state.props;
29 | let value = state.value;
30 | return (
31 |
32 | at least { value }{ mea } each day
33 |
40 |
41 | );
42 | });
43 | }
44 | let actions = intent(responses.DOM);
45 | let vtree$ = view(model(responses, actions))
46 |
47 | return {
48 | DOM: vtree$,
49 | events: {
50 | newValue: actions.changeValue$
51 | }
52 | }
53 | };
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/intent.js:
--------------------------------------------------------------------------------
1 | export default function intent(DOM){
2 | return {
3 | changeMinHigh$ : DOM.select('#minHigh').events('newValue')
4 | .map(e => e.detail)
5 | .debounce(10),
6 | changeMinSun$ : DOM.select('#minSun').events('newValue')
7 | .map(e => e.detail)
8 | .debounce(10),
9 | changeDuration$ : DOM.select('#forecastUntil').events('change')
10 | .map(e => e.target.value),
11 | changeStartDay$ : DOM.select('#forecastFrom').events('change')
12 | .map(e => e.target.value)
13 | };
14 | }
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/main.js:
--------------------------------------------------------------------------------
1 | import { run } from '@cycle/core';
2 | import { makeDOMDriver } from '@cycle/dom';
3 | import { makeHTTPDriver } from '@cycle/http'
4 | import labeledSlider from './components/labeledSlider';
5 | import city from './components/city';
6 | import app from './app'
7 |
8 | const main = app;
9 |
10 | run(main, {
11 | DOM: makeDOMDriver('#app', {
12 | 'labeled-slider': labeledSlider,
13 | 'city': city
14 | }),
15 | HTTP: makeHTTPDriver()
16 | });
17 |
--------------------------------------------------------------------------------
/app/mockResponse.json:
--------------------------------------------------------------------------------
1 | [{"city":{"id":3128760,"name":"Barcelona","coord":{"lon":2.15899,"lat":41.38879},"country":"ES","population":0},"cod":"200","message":0.0314,"cnt":7,"list":[{"dt":1444388400,"temp":{"day":291.74,"min":291.74,"max":292.68,"night":292.68,"eve":292.05,"morn":291.74},"pressure":1027.18,"humidity":100,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"01d"}],"speed":2.62,"deg":191,"clouds":0},{"dt":1444474800,"temp":{"day":293.33,"min":293.21,"max":294.11,"night":294.11,"eve":293.83,"morn":293.21},"pressure":1026.26,"humidity":100,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"02d"}],"speed":1.21,"deg":174,"clouds":8},{"dt":1444561200,"temp":{"day":295.11,"min":294.75,"max":295.44,"night":294.97,"eve":294.91,"morn":294.75},"pressure":1023.35,"humidity":100,"weather":[{"id":801,"main":"Clouds","description":"few clouds","icon":"02d"}],"speed":7.02,"deg":208,"clouds":24},{"dt":1444647600,"temp":{"day":295.79,"min":295.41,"max":295.79,"night":295.44,"eve":295.54,"morn":295.68},"pressure":1023.68,"humidity":96,"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04d"}],"speed":3.58,"deg":195,"clouds":92},{"dt":1444734000,"temp":{"day":292.03,"min":290.51,"max":292.57,"night":290.51,"eve":292.57,"morn":290.88},"pressure":1007.81,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":7.85,"deg":304,"clouds":87,"rain":1.13},{"dt":1444820400,"temp":{"day":293.1,"min":289.18,"max":293.62,"night":291.44,"eve":293.62,"morn":289.18},"pressure":1010.12,"humidity":0,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"01d"}],"speed":4.52,"deg":307,"clouds":0},{"dt":1444906800,"temp":{"day":293.48,"min":289.76,"max":293.95,"night":292.25,"eve":293.95,"morn":289.76},"pressure":1011.39,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":4.66,"deg":250,"clouds":39,"rain":0.61}]},{"city":{"id":2618425,"name":"Copenhagen","coord":{"lon":12.56553,"lat":55.675941},"country":"DK","population":0},"cod":"200","message":0.0221,"cnt":7,"list":[{"dt":1444384800,"temp":{"day":281.22,"min":280.1,"max":281.22,"night":280.1,"eve":281.22,"morn":281.22},"pressure":1035.08,"humidity":73,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04n"}],"speed":3.86,"deg":114,"clouds":76},{"dt":1444471200,"temp":{"day":283.21,"min":276.65,"max":284,"night":276.65,"eve":283.56,"morn":279.14},"pressure":1036.7,"humidity":75,"weather":[{"id":803,"main":"Clouds","description":"broken clouds","icon":"04d"}],"speed":2.52,"deg":93,"clouds":68},{"dt":1444557600,"temp":{"day":282.68,"min":275.5,"max":284.43,"night":276.94,"eve":283.84,"morn":275.89},"pressure":1036.95,"humidity":74,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"01d"}],"speed":3.31,"deg":65,"clouds":0},{"dt":1444644000,"temp":{"day":282.99,"min":277.43,"max":284.61,"night":277.81,"eve":284.24,"morn":277.43},"pressure":1035.75,"humidity":92,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":3.02,"deg":83,"clouds":32,"rain":0.4},{"dt":1444730400,"temp":{"day":285.49,"min":277.5,"max":285.49,"night":277.61,"eve":281.01,"morn":277.5},"pressure":1032.49,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":2.56,"deg":260,"clouds":14},{"dt":1444816800,"temp":{"day":286.34,"min":276.77,"max":286.34,"night":281.62,"eve":282.51,"morn":276.77},"pressure":1032.69,"humidity":0,"weather":[{"id":800,"main":"Clear","description":"sky is clear","icon":"01d"}],"speed":2.69,"deg":252,"clouds":5},{"dt":1444903200,"temp":{"day":286.25,"min":282.53,"max":286.25,"night":282.79,"eve":283.11,"morn":282.53},"pressure":1024.44,"humidity":0,"weather":[{"id":500,"main":"Rain","description":"light rain","icon":"10d"}],"speed":7.3,"deg":269,"clouds":37,"rain":2.06}]}]
--------------------------------------------------------------------------------
/app/model.js:
--------------------------------------------------------------------------------
1 | import {Rx} from '@cycle/core';
2 | let Ob$ = Rx.Observable;
3 |
4 | export default function model(actions, HTTP){
5 |
6 | let { changeDuration$, changeStartDay$,
7 | changeMinSun$, changeMinHigh$ } = actions;
8 |
9 | let cities$;
10 | if(__PROD__){
11 | cities$ = HTTP
12 | .filter(re$ => re$.request.indexOf('cities') > -1)
13 | .mergeAll()
14 | .map(res => res.body);
15 | }else{
16 | let citiesReq = require('json!./mockResponse.json');
17 | cities$ = Ob$.just(citiesReq).delay(1000 * 2);
18 | }
19 |
20 | return Ob$.combineLatest(
21 |
22 | // user action streams
23 | changeMinHigh$.startWith(15),
24 | changeMinSun$.startWith(60),
25 | changeStartDay$.startWith("0"),
26 | changeDuration$.startWith("3"),
27 |
28 | // api data prettied up with only forecast days
29 |
30 | Ob$.combineLatest(
31 |
32 | // sieved responses stream
33 |
34 | cities$.map(cities => {
35 |
36 | // sieve the raw streams
37 |
38 | return cities.map(cityRaw => {
39 | if (!cityRaw) return null;
40 | let { city, list } = cityRaw;
41 | return {
42 | name: city.name ,
43 | forecasts: list.map(forecast => {
44 | let date = new Date(forecast.dt * 1000);
45 | return {
46 | date: date.getDayName() + ' ' + date.getDate(),
47 | desc: forecast.weather[0].description,
48 | high: Math.round(forecast.temp.max),
49 | low: Math.round(forecast.temp.min),
50 | sun: 100 - forecast.clouds,
51 | humidity: forecast.humidity,
52 | wind: Math.round(forecast.speed)
53 | }
54 | }).slice(1) // dismiss today
55 | }
56 | });
57 | }).startWith([]),
58 |
59 | // selected days to forecast stream
60 | changeStartDay$.startWith("0"),
61 | changeDuration$.startWith("3"),
62 |
63 | // combine to make stream with only selected days to forecast
64 | (cities, startDay, selectedDuration) => {
65 | return cities.map(city => {
66 | if (!city) return null;
67 | return {
68 | ...city,
69 | forecasts: city.forecasts.slice(+startDay, +startDay + +selectedDuration),
70 | };
71 | });
72 | }
73 |
74 | ).map(cities => {
75 |
76 | // add derived data based on selected forecast
77 |
78 | if (!cities) return null;
79 | return cities.map(city => {
80 |
81 | if (!city) return null;
82 | let {forecasts} = city;
83 | let startDate = city.forecasts[0].date;
84 | let endDate = city.forecasts[city.forecasts.length-1].date
85 |
86 | return {
87 | ...city,
88 | minHigh: forecasts.reduce((min, next) => {
89 | return next.high < min ? next.high : min;
90 | }, forecasts[0].high),
91 | maxHigh: forecasts.reduce((max, next) => {
92 | return next.high > max ? next.high : max;
93 | }, forecasts[0].high),
94 | minLow: forecasts.reduce((min, next) => {
95 | return next.low < min ? next.low : min;
96 | }, forecasts[0].low),
97 | minSun: forecasts.reduce((min, next) => {
98 | return next.sun < min ? next.sun : min;
99 | }, forecasts[0].sun),
100 | timespan: startDate + ' to ' + endDate
101 | }
102 |
103 | });
104 |
105 | }).startWith([]),
106 |
107 | // combine ready cities and filters
108 |
109 | (selectedMinHigh, selectedMinSun, startDay, selectedDuration, cities) => ({
110 | filteredCities: cities.filter(city => {
111 | if(!city) return null;
112 | return city.minHigh >= selectedMinHigh
113 | && city.minSun >= selectedMinSun;
114 | }),
115 | citiesAvail: cities.length ? true : false,
116 | selectedMinHigh,
117 | selectedMinSun,
118 | startDay,
119 | selectedDuration
120 | })
121 |
122 | );
123 | }
124 |
125 | // date helper
126 | let days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
127 | months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
128 | 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
129 |
130 | Date.prototype.getMonthName = function() { return months[this.getMonth() ]; }
131 | Date.prototype.getDayName = function() { return days[this.getDay() ]; }
132 |
133 |
134 |
--------------------------------------------------------------------------------
/app/view.js:
--------------------------------------------------------------------------------
1 | /** @jsx hJSX */
2 | import {hJSX} from '@cycle/dom';
3 | import renderFilters from './views/filters';
4 | import renderCities from './views/cities';
5 |
6 | var whiteLogo = require('../public/img/white_logo.png');
7 | var blackLogo = require('../public/img/black_logo.png');
8 |
9 | export default function view(state$) {
10 |
11 | return state$.map(({citiesAvail, selectedMinHigh, selectedMinSun,
12 | selectedDuration, startDay, filteredCities}) => {
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | { renderFilters({selectedMinSun, selectedMinHigh, startDay, selectedDuration }) }
20 |
21 | { renderCities(citiesAvail, filteredCities) }
22 |
23 |
30 |
31 |
32 | )
33 | });
34 | }
35 |
36 | let styles = {
37 | 'app': {
38 | 'margin': '0 auto',
39 | 'marginTop': '0.3em',
40 | 'width': '85%'
41 | },
42 | 'header': {
43 | 'padding': '0.5em 0',
44 | 'textAlign': 'center'
45 | },
46 | 'footer': {
47 | 'paddingTop': '1.3em',
48 | 'textAlign': 'center'
49 | }
50 | }
51 |
52 |
53 |
--------------------------------------------------------------------------------
/app/views/cities.js:
--------------------------------------------------------------------------------
1 | /** @jsx hJSX */
2 | import {hJSX} from '@cycle/dom';
3 |
4 | export default function renderCities(avail, cities) {
5 |
6 | if(!avail){
7 | return Fetching...
8 | }
9 |
10 | if(!cities.length) {
11 | return No matches
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 | {cities.length } {cities.length > 1 ? 'Results' : 'Result'}
19 |
20 |
21 |
22 | {
23 |
24 | cities.map(city => {
25 | let { name, minSun, minLow, maxHigh, minHigh, forecasts, timespan } = city;
26 | return
;
31 | })
32 |
33 | }
34 |
35 |
36 | );
37 |
38 | }
39 |
40 | let styles = {
41 | 'cities': {
42 | 'display': 'flex',
43 | 'flexWrap': 'wrap',
44 | 'justifyContent': 'space-around'
45 | },
46 | 'results': {
47 | 'width': '100%'
48 | },
49 | 'heading': {
50 | 'margin': '0 auto',
51 | 'marginTop': '0.5em',
52 | 'backgroundColor': 'white',
53 | 'padding': '0.5em',
54 | 'borderRadius': '25px',
55 | 'height': '30px',
56 | 'lineHeight': '30px',
57 | 'textAlign': 'center',
58 | 'width': '200px'
59 | }
60 | }
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/views/filters.js:
--------------------------------------------------------------------------------
1 | /** @jsx hJSX */
2 | import {hJSX} from '@cycle/dom';
3 |
4 | export default function renderFilters({selectedMinHigh, selectedMinSun}) {
5 |
6 | return (
7 |
8 |
FIND YOUR WEATHER
9 |
10 |
Starting
11 |
12 |
16 |
17 |
For:
18 |
19 |
26 |
27 |
28 |
Temperature
29 |
30 |
31 |
35 |
36 |
37 |
Sunny
38 |
39 |
40 |
44 |
45 |
46 |
47 | )
48 | }
49 |
50 | let styles = {
51 | 'container': {
52 | 'padding': '0.8em',
53 | 'borderRadius': '25px',
54 | 'marginTop': '0.5em',
55 | 'backgroundColor': '#F8EFB6',
56 | 'textAlign': 'center'
57 | //'border': '5px dotted #FEBAC5'
58 | },
59 | 'header': {
60 | 'textAlign': 'center',
61 | 'margin': '0',
62 | 'marginBottom': '0.5em'
63 | },
64 | 'filters': {
65 | 'display': 'block',
66 | 'flexWrap': 'wrap'
67 | },
68 | 'heading': {
69 | 'margin': '0 auto',
70 | 'marginTop': '1.3em',
71 | 'marginBottom': '0.5em',
72 | 'backgroundColor': '#6CD1EA',
73 | 'width': '140px',
74 | 'padding': '5px 8px',
75 | 'color': '#fff',
76 | 'fontWeight': 'normal'
77 | },
78 | 'selectBox': {
79 | 'padding': '0',
80 | 'margin': '0 auto',
81 | 'border': '1px solid #6CD1EA',
82 | 'width': '200px',
83 | 'borderRadius': '3px',
84 | 'overflow': 'hidden',
85 | 'backgroundColor': '#fff',
86 | 'background': '#fff url("img/arrowdown.gif") no-repeat 90% 50%'
87 | },
88 |
89 | 'select': {
90 | 'padding': '5px 8px',
91 | 'width': '130%',
92 | 'border': 'none',
93 | 'boxShadow': 'none',
94 | 'backgroundColor': 'transparent',
95 | 'backgroundImage': 'none',
96 | 'appearance': 'none'
97 | },
98 |
99 | 'half': {
100 | 'flex': '1 0 40%',
101 | 'marginBottom': '1.3em',
102 | 'padding': '0 0.3em'
103 | },
104 | 'padRight': {
105 | 'paddingRight': '1.3em'
106 | }
107 | }
108 |
109 |
110 |
--------------------------------------------------------------------------------
/app/views/forecast.js:
--------------------------------------------------------------------------------
1 | /** @jsx hJSX */
2 | import Cycle from '@cycle/core';
3 | import {hJSX} from '@cycle/dom';
4 |
5 | export default function renderForecast(state$) {
6 | return state$.map(state => {
7 | let { date, desc, high, low, humidity, sun, wind } = state;
8 |
9 | let sty = {
10 | 'forecast': {
11 | 'flex': '0 0 20%'
12 | },
13 | 'date': {
14 | //'textAlign': 'center'
15 | 'margin': '0.2em'
16 | }
17 | };
18 |
19 | return (
20 |
21 | {date}
22 | { desc }
23 |
24 |
25 | High |
26 | { high } |
27 |
28 |
29 | Low |
30 | { low } |
31 |
32 |
33 | Sun |
34 | { sun }% |
35 |
36 |
37 | Wind |
38 | { wind } |
39 |
40 |
41 |
42 |
43 | );
44 | })
45 | }
46 |
47 |
48 |
--------------------------------------------------------------------------------
/cities.js:
--------------------------------------------------------------------------------
1 | var RSVP = require('rsvp');
2 | var superagent = require('superagent');
3 | var Cycle = require('@cycle/core');
4 | var Ob$ = Cycle.Rx.Observable;
5 |
6 | function fetchCity(cityName) {
7 | return new RSVP.Promise(function(resolve, rej) {
8 | superagent
9 | .get('http://api.openweathermap.org/data/2.5/forecast/daily')
10 | .set('x-api-key','5ba09e308c10daeb4737c29d3fef2907')
11 | .query({
12 | q: cityName,
13 | cnt: 7,
14 | units: 'metric'
15 | })
16 | .end(function(err, res) {
17 | resolve(res.body);
18 | });
19 | })
20 | }
21 |
22 | var oneHour = 1000 * 60 * 60;
23 |
24 | var bar$ = Ob$.timer(0, oneHour).flatMap(x => fetchCity('barcelona'));
25 | var ber$ = Ob$.timer(10, oneHour).flatMap(x => fetchCity('berlin'));
26 | var bue$ = Ob$.timer(200, oneHour).flatMap(x => fetchCity('buenosaires, arg'));
27 | var cap$ = Ob$.timer(90, oneHour).flatMap(x => fetchCity('rome,it'));
28 | var cop$ = Ob$.timer(80, oneHour).flatMap(x => fetchCity('copenhagen'));
29 | var ist$ = Ob$.timer(100, oneHour).flatMap(x => fetchCity('istanbul'));
30 | var lis$ = Ob$.timer(20, oneHour).flatMap(x => fetchCity('lisbon'));
31 | var lon$ = Ob$.timer(120, oneHour).flatMap(x => fetchCity('london'));
32 | var mad$ = Ob$.timer(80, oneHour).flatMap(x => fetchCity('madrid'));
33 | var mal$ = Ob$.timer(80, oneHour).flatMap(x => fetchCity('valetta, malta'));
34 | var mia$ = Ob$.timer(150, oneHour).flatMap(x => fetchCity('miami'));
35 | var myk$ = Ob$.timer(50, oneHour).flatMap(x => fetchCity('mykonos'));
36 | var nic$ = Ob$.timer(60, oneHour).flatMap(x => fetchCity('nice,fr'));
37 | var nyc$ = Ob$.timer(130, oneHour).flatMap(x => fetchCity('newyork'));
38 | var par$ = Ob$.timer(110, oneHour).flatMap(x => fetchCity('paris'));
39 | var sev$ = Ob$.timer(140, oneHour).flatMap(x => fetchCity('sevilla,es'));
40 | var sfc$ = Ob$.timer(30, oneHour).flatMap(x => fetchCity('sanfrancisco, us'));
41 | var sin$ = Ob$.timer(170, oneHour).flatMap(x => fetchCity('singapore'));
42 | var spl$ = Ob$.timer(180, oneHour).flatMap(x => fetchCity('split,croatia'));
43 | var syd$ = Ob$.timer(25, oneHour).flatMap(x => fetchCity('sydney,aus'));
44 | var tok$ = Ob$.timer(160, oneHour).flatMap(x => fetchCity('tokyo,jp'));
45 |
46 | var cities$ = Ob$.combineLatest(
47 | bar$, ber$, bue$, cap$, cop$, ist$, lis$, lon$, mad$, mal$, mia$, myk$,
48 | nic$, nyc$, par$, sev$, sfc$, sin$, spl$, syd$, tok$,
49 | (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) => (
50 | [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v]
51 | ))
52 |
53 | var cities= [];
54 | cities$.subscribe(latestCities => {
55 | cities = latestCities;
56 | });
57 |
58 | module.exports = cities$;
59 |
60 |
--------------------------------------------------------------------------------
/heroku.js:
--------------------------------------------------------------------------------
1 | if(process.env.NODE_ENV === 'production') {
2 | var child_process = require('child_process');
3 | var proc = "webpack -p --config webpack-prod.config.js";
4 | child_process.exec(proc, function(error, stdout, stderr) {
5 | console.log('stdout: ' + stdout);
6 | console.log('stderr: ' + stderr);
7 | if (error !== null){
8 | console.log('exec error: ' + error);
9 | }
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "kairos",
3 | "version": "0.0.1",
4 | "description": "Weather app that filters the forecast",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "build": "webpack -p --config webpack-prod.config.js",
9 | "postinstall": "node heroku.js",
10 | "dev": "webpack-dev-server --inline --progress --colors --content-base public",
11 | "test": "mocha --compilers js:babel/register --recursive",
12 | "test:watch": "npm run test -- --watch"
13 | },
14 | "author": "Thomas Collardeau (http://thomas.collardeau.com/)",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "webpack-dev-server": "1.12.0"
18 | },
19 | "dependencies": {
20 | "@cycle/core": "3.1.0",
21 | "@cycle/dom": "5.3.0",
22 | "@cycle/http": "4.0.0",
23 | "babel": "5.8.23",
24 | "babel-loader": "5.3.2",
25 | "chai": "3.3.0",
26 | "express": "4.13.3",
27 | "file-loader": "0.8.4",
28 | "firebase": "2.3.1",
29 | "json-loader": "0.5.3",
30 | "mocha": "2.3.3",
31 | "rsvp": "3.1.0",
32 | "superagent": "1.4.0",
33 | "url-loader": "0.5.6",
34 | "webpack": "1.12.2"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/public/img/arrowdown.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collardeau/kairos-cycle/88782b1a3f823f884c34d959a686921765010289/public/img/arrowdown.gif
--------------------------------------------------------------------------------
/public/img/black_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collardeau/kairos-cycle/88782b1a3f823f884c34d959a686921765010289/public/img/black_logo.png
--------------------------------------------------------------------------------
/public/img/white_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/collardeau/kairos-cycle/88782b1a3f823f884c34d959a686921765010289/public/img/white_logo.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Kairos
8 |
9 |
10 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var Firebase = require('firebase');
3 | var cities$ = require('./cities');
4 |
5 | var app = express();
6 |
7 | var cities= [];
8 | cities$.subscribe(latestCities => {
9 | cities = latestCities;
10 | });
11 |
12 | app.get('/cities', function(req, res) {
13 | res.json(cities);
14 | save(req.ip);
15 | });
16 |
17 | var ref = new Firebase('http://kairos.firebaseio.com');
18 | function save(ip){
19 | ref.child('connects').push({
20 | ip: ip,
21 | stamp: Firebase.ServerValue.TIMESTAMP
22 | })
23 | }
24 |
25 | app.use(express.static('public'));
26 |
27 | var isProd = process.env.NODE_ENV === 'production';
28 | var port = isProd ? process.env.PORT : 3000;
29 |
30 | app.listen(port, function(){
31 | console.log('listening on port ' + port);
32 | });
33 |
34 |
--------------------------------------------------------------------------------
/test/app_spec.js:
--------------------------------------------------------------------------------
1 | import { expect } from 'chai';
2 |
3 | describe('testing', () => {
4 | it('works', () => {
5 | expect(1).to.equal(1);
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/webpack-prod.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 |
6 | entry: {
7 | app: [
8 | path.resolve(__dirname, 'app', 'main.js')
9 | ]
10 | },
11 |
12 | output: {
13 | path: path.resolve(__dirname, 'public'),
14 | filename: 'bundle.js'
15 | },
16 |
17 | module: {
18 | loaders: [{
19 | test: /\.js$/,
20 | loaders: ['babel'],
21 | exclude: path.resolve(__dirname, 'node_modules'),
22 | }, {
23 | test: /\.(png|gif|jpg)$/,
24 | loader: 'url-loader?limit=12500'
25 | }]
26 | },
27 |
28 | plugins: [
29 | new webpack.DefinePlugin({
30 | __PROD__: 'true'
31 | })
32 | ]
33 |
34 |
35 | };
36 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | module.exports = {
5 |
6 | entry: {
7 |
8 | app: [
9 | 'webpack-dev-server/client?http://localhost:8080',
10 | path.resolve(__dirname, 'app', 'main.js')
11 | ]
12 | },
13 |
14 | output: {
15 | path: path.resolve(__dirname, 'public'),
16 | filename: 'bundle.js'
17 | },
18 |
19 | module: {
20 | loaders: [{
21 | test: /\.js$/,
22 | loaders: ['babel'],
23 | exclude: path.resolve(__dirname, 'node_modules'),
24 | }, {
25 | test: /\.(png|gif|jpg)$/,
26 | loader: 'url-loader?limit=12500'
27 | }]
28 | },
29 |
30 | plugins: [
31 | new webpack.DefinePlugin({
32 | __PROD__: 'false'
33 | })
34 | ]
35 |
36 | };
37 |
--------------------------------------------------------------------------------