├── .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 |
24 | 25 |

Made by thomas.collardeau.com 26 |
Data from openweathermap.org 27 |

28 | 29 |
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 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
High{ high }
Low{ low }
Sun{ sun }%
Wind{ wind }
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 | --------------------------------------------------------------------------------