Jorge Lucic (jvlucic)
Brand New Booking
Harder Better Faster Stronger
Room Selector
Image Carousel
Booking Form
Inventory Line (40%)
Footer
Hotel Card
Boilerplate
Routing handler
i18n Support
Calendar Component
Services Selector (75%)
Promotion List
Language Selector
Reservation Receipt
Cancelation
Booking Cart
TPV
Breadcrumb
SIPAY
Save as PDF
Messages
Collapsable Text
Currency Selector
Print Confirmation
Putting together the boilerplate with the stack used
Connecting the Model with Selectors
Selectors and Immutable
Decoupling components
Booking form responsive design
Flex styling for certain components
Handling SSR
| 18/03/2016 | Specification Done | 1 week design delay |
|---|---|---|
| 21/03/2016 | Estimation Done | |
| 28/03/2016 | Development Starts | 2 days external delay |
| 11/04/2016 | First Deliverable | Unexpected hardships |
| 03/05/2016 | Second Deliverable (demo) | On Time but expected 75% progress |
| 16/05/2016 | Final Delivery | Delayed |
| 23/05/2016 | Tests Done | |
| 06/06/2016 | Pilot | |
| 20/06/2016 | Production Release |
| 29/03/2016 | Specification Done | Semi-final Design Delivered |
|---|---|---|
| 30/03/2016 | Estimation Done | |
| 01/04/2016 | Development Starts | 2 days external delay |
| 16/04/2016 | First Deliverable | |
| 03/05/2016 | Second Deliverable (demo) | |
| 31/05/2016 | Final Delivery | Delayed |
| 06/06/2016 | Tests Done | |
| 20/06/2016 | Pilot | |
| 04/08/2016 | Production Release |
| Estimated Total Hours | 311.5 |
| Weekly Hours (External 15, WBK 20) |
35 |
| Standard Duration | 9 Weeks |
| Boost Duration | 6 Weeks |
| Estimated Total Hours | 311.5 |
| Weekly Hours (External 15, WBK 15) |
30 |
| Standard Duration | 10 Weeks |
| Boost Duration | 8 Weeks |
SIDE-EFFECTS
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, initialState, enhancer) => {
var store = createStore(reducer, initialState, enhancer)
var dispatch = store.dispatch
var chain = []
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
export function* getMovies() {
yield* takeLatest(LOAD_MOVIES, fetchMovies);
}
yield call(mdb.searchMovie, query);
// Effect -> call the function mdb.searchMovie with query obj as argument
{
CALL: {
fn: mdb.searchMovie,
args: [query]
}
}
//Effects are like actions to the saga middleware!export function* fetchMovies(getState) {
...
queryObj = { ...queryObj, ...( yield buildPeopleQuery(peopleQuery) ) };
...
}
function* buildPeopleQuery(people){
...
for (let i = 0; i < people.length; i++) {
...
let castId = yield cps([mdb, mdb.searchPerson], {query: person.query});
...
}
...
}function *main(){
var search_terms = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" ),
request( "http://some.url.3" )
] );
var search_results = yield request(
"http://some.url.4?search=" + search_terms.join( "+" )
);
var resp = JSON.parse( search_results );
} Q.spawn(function* () {
let result = yield asyncFunc('http://example.com');
console.log(result);
});
co(function* getResults(){
var a = read('index.js', 'utf8');
var b = request('http://google.ca');
var res = yield [a, b];
}).catch(function (ex) {
});//PARALLEL
const [users, repos] = yield [
call(fetch, '/users'),
call(fetch, '/repos')
]
//RACE
function* fetchPostsWithTimeout() {
const {posts, timeout} = yield race({
posts : call(fetchApi, '/posts'),
timeout : call(delay, 1000)
})
}
//CANCELLATION
export function* getMovies() {
yield* takeLatest(LOAD_MOVIES, fetchMovies);
}
Sagas allow implementing complex flows
Sagas embrace generators, hiding callback abstractions
Sagas are easier to test
If your side-effects are simple, stick to thunks
{
"name": "Season 1",
"overview": "The first season of the American television drama series Breaking Bad premiered...",
"id": 3572,
"season_number": 1,
"episodes": [
{
"name": "Pilot",
"overview": "When an unassuming high school chemistry...",
"id": 62085,
"season_number": 1,
"vote_average": 8.5,
"crew": [
{
"id": 2483,
"name": "John Toll",
"department": "Camera",
"job": "Director of Photography",
"profile_path": null
}
...
],
"episode_number": 1,
"guest_stars": [
{
"id": 92495,
"name": "John Koyama",
"character": "Emilio Koyama",
"order": 1,
"profile_path": "/uh4g85qbQGZZ0HH6IQI9fM9VUGS.jpg"
}
...
]
},
{"name": "Other Episode 1"},
{"name": "Other Episode 2"},
{"name": "Other Episode 3"},
{"name": "Other Episode 4"},
{"name": "Other Episode 5"}
]
}const {Schema, arrayOf, } = require('normalizr');
const season = new Schema('seasons');
const episode = new Schema('episodes');
const crewMember = new Schema('crew');
const guestStar = new Schema('guestStars');
season.define({
episodes: arrayOf(episode)
});
episode.define({
crew: arrayOf(crewMember),
guest_stars: arrayOf(guestStar)
});
exports.season = season;{
"result": 3572,
"entities": {
"seasons": {
"3572": {
"air_date": "2008-01-19",
"episodes": [62085,62086,62087,62088,62089,62090],
"name": "Season 1",
"overview": "The first season of the American television drama series Breaking Bad premiered ...",
"id": 3572,
"season_number": 1
}
},
"crew": {
"2483": {
"id": 2483,
"credit_id": "52b7029219c29533d00dd2c1",
"name": "John Toll",
"department": "Camera",
"job": "Director of Photography",
"profile_path": null
},
...
}
"guest_stars":{
"92495": {
"id": 92495,
"name": "John Koyama",
"credit_id": "52542273760ee3132800068e",
"character": "Emilio Koyama",
"order": 1,
"profile_path": "/uh4g85qbQGZZ0HH6IQI9fM9VUGS.jpg"
},
...
}
"episodes": {
"62085": {
"episode_number": 1,
"name": "Pilot",
"overview": "When an unassuming high school chemistry ...",
"id": 62085,
"season_number": 1,
"vote_average": 8.5,
"crew": [ 2483, 66633, 66633, 1280071 ],
"guest_stars": [92495,1223192,1216132,161591,1046460,1223197,61535,115688]
},
"62086": {"name": "Other Episode 1"},
"62087": {"name": "Other Episode 2"},
"62088": {"name": "Other Episode 3"},
"62089": {"name": "Other Episode 4"},
"62090": {"name": "Other Episode 5"},
},
}
}
const mapStateToProps = (state) => {
const {
home:{ movies: allMovieIds, selectedMovies, loading, error } = {},
form: { syncFilters: {genre, score, year} = { } } = {},
entities: {
movies: movieEntities
} = {}
} = state.toJS() ; // ANTI-PATTERN!
const allMovies = allMovieIds && allMovieIds.map( id => movieEntities[id]);
const movies = allMovies && allMovies
.filter(byGenre(genre.value || null))
.filter(byScore(score.value || null))
.filter(byYear(year.value || null));
return { movies, selectedMovies, loading, error}
};"filters": {
"asyncFilters": { "movieQuery": "lego",}
},
"form": {
"syncFilters": {
"genre": { "value": "35"},
"year": { "value": "2015",}
}
}const homeSelector = (state) => state.get('home');
export const moviesResultsSelector = createSelector(
homeSelector,
movieEntitiesSelector,
(home, movieEntities) => home.get('movies')
&& home.get('movies').map( id => movieEntities[id] )
);
export const filteredMoviesResultsSelector = createSelector(
moviesResultsSelector, genreFilterSelector,scoreFilterSelector,
searchTextFilterSelector,yearFilterSelector,
(movies, genre, score, search, year) => (
movies && movies
.filter(byGenre(genre))
.filter(byScore(score))
.filter(byYear(year))
)
);"filters": {
"asyncFilters": { "movieQuery": "lego",}
},
"form": {
"syncFilters": {
"genre": { "value": "35"},
"year": { "value": "2015",}
}
}...
export const genreFilterSelector = createSelector(
syncFiltersSelector,
(syncFilters) => ( syncFilters && parseInt(syncFilters.genre.value) || null )
);
...
export const filteredMoviesResultsSelector = createSelector(
moviesResultsSelector, genreFilterSelector,scoreFilterSelector,searchTextFilterSelector,yearFilterSelector,
(movies, genre, score, search, year) => (
movies && movies
.filter(byGenre(genre))
.filter(byScore(score))
.filter(byYear(year))
)
);"filters": {
"asyncFilters": { "movieQuery": "lego",}
},
"form": {
"syncFilters": {
"genre": { "value": "35"},
"year": { "value": "2015",}
}
}var map1 = Immutable.Map({a:1, b:2, c:3});
var map2 = map1.set('b', 2);
assert(map1.equals(map2) === true);
var map3 = map1.set('b', 50);
assert(map1.equals(map3) === false);function AsyncFilters(props) {
return (
...
<TextField hintText="Movie name, plot" onChange={props.onChangeMovieQuery}/>
...
)
}
<AsyncFilters onSearch={onSearch} onChangeMovieQuery={onChangeMovieQuery} ... />
function mapDispatchToProps(dispatch) {
return {
...
onChangeMovieQuery: (evt) => {
dispatch(changeMovieQueryFilter(evt.target.value))
},
...
};
}
export function changeMovieQueryFilter(text) {
return {
type: CHANGE_MOVIE_QUERY_FILTER,
text
};
}
function filtersReducer(state = initialState, action) {
switch (action.type) {
case CHANGE_MOVIE_QUERY_FILTER:
return state
.setIn(['asyncFilters', 'movieQuery'], action.text);
...
}
}class SyncFilters extends Component {
render() {
const {fields: {searchText, genre, year, score}, handleSubmit, resetForm} = this.props;
return (
<SearchBar label="Name" placeholder={"Filter"} {...searchText}/>
<select type="select" label="Genre" placeholder="Pick a Genre"
value={genre.value || ''} {...genre}>
<option>Pick a Genre</option>
{ ... }
</select>
<select type="select" label="Year" placeholder="Pick a Year"
value={year.value || ''} {...year}>
<option >Pick a Year</option>
{ ... }
</select>
);
}
}
SyncFilters = reduxForm({
form: 'syncFilters',
fields: ['searchText', 'genre', 'year', 'score']
})(SyncFilters);{
"form": {
"syncFilters": {
"genre": {
"visited": true,
"value": "16",
"touched": true
},
"year": {
"visited": true,
"value": "2013",
"touched": true
}
}
}
}Tools are a big help for perfomance and DX
Normalizr simplifies your nested data models helping with one source of thruth and easy updates
Selectors are great for reusability and optimization
ImmutableJS helps to keep mutations at bay
Redux-form obliterate lots of boilerplate code