├── .flowconfig ├── .gitignore ├── README.md ├── flow-typed └── globals.js ├── package.json ├── preview.png ├── src ├── main.js ├── pages │ ├── home.js │ ├── pageNotFound.js │ └── search.js ├── redux │ ├── concerts.js │ └── index.js ├── root.js └── rpc │ └── index.js └── yarn.lock /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.*[^(package)]\.json$ 3 | 4 | [include] 5 | ./src/ 6 | 7 | [libs] 8 | ./node_modules/fusion-plugin-rpc-redux-react/flow-typed/npm/redux_v4.x.x.js 9 | ./node_modules/fusion-plugin-rpc-redux-react/flow-typed/redux-reactors_v1.x.x.js 10 | ./node_modules/fusion-plugin-react-redux/flow-typed/npm/redux_v3.x.x.js 11 | ./node_modules/fusion-plugin-redux-action-emitter-enhancer/flow-typed/npm/redux_v3.x.x.js 12 | ./node_modules/fusion-core/flow-typed/npm/koa_v2.x.x.js 13 | ./node_modules/fusion-core/flow-typed/tape-cup_v4.x.x.js 14 | 15 | [lints] 16 | 17 | [options] 18 | 19 | [strict] 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log 3 | .fusion/ 4 | coverage*/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fusion.js + Base UI Example App 2 | 3 | ## What are we going to build? 4 | 5 | ![Concerts in Iceland](preview.png) 6 | 7 | If you want to immediately get the whole application 8 | 9 | ``` 10 | git clone git@github.com:tajo/fusion-baseui.git 11 | cd fusion-baseui 12 | yarn 13 | yarn dev 14 | ``` 15 | 16 | Or you can follow the tutorial bellow with detailed description. 17 | 18 | # Step-by-step tutorial 19 | 20 | ## Assumptions 21 | 22 | - Your environment has Node.js 8.11 and the latest Yarn 23 | - Advanced knowledge of JavaScript 24 | - Intermediate knowledge of React and Redux 25 | 26 | ## Learning objectives 27 | 28 | - Bootstrap a basic Fusion.js app 29 | - Fetch data from a public REST API and store it in Redux 30 | - Pre-render the page on the server 31 | - Rehydrate the redux store on the client 32 | - Build a simple UI using Base UI components 33 | - Handle errors 34 | 35 | ## Fusion.js setup 36 | 37 | ``` 38 | yarn create fusion-app fusion-baseui 39 | cd fusion-baseui 40 | yarn dev 41 | ``` 42 | 43 | That should open [https://localhost:3000](https://localhost:3000) in your browser with "Fusion.js - Let's get started" message. 44 | 45 | Open `src/pages/home.js`, change the `Get Started` message to something else and save it. You should immediately see it in the browser because of hot reloading. 46 | 47 | ## Base UI setup 48 | 49 | Base UI is a component library based on React. We will use it to put together our user interface. Add it to your project via 50 | 51 | ``` 52 | yarn add baseui 53 | ``` 54 | 55 | Now, replace the content of `src/pages/home.js` with 56 | 57 | ```jsx 58 | // @flow 59 | import * as React from "react"; 60 | 61 | // Base UI components 62 | import { Card } from "baseui/card"; 63 | import { Block } from "baseui/block"; 64 | 65 | const CONCERTS = [ 66 | { 67 | eventDateName: "Jón Jónsson og Friðrik Dór - fjölskyldutónleikar", 68 | name: "Tónleikar", 69 | dateOfShow: "2018-12-15T14:00:00", 70 | eventHallName: "Bæjarbíó (Hafnarfirði)", 71 | imageSource: 72 | "https://d30qys758zh01z.cloudfront.net/images/medium/1.10700.jpg" 73 | }, 74 | { 75 | eventDateName: "Jón Jónsson og Friðrik Dór - fjölskyldutónleikar", 76 | name: "Tónleikar-UPPSELT", 77 | dateOfShow: "2018-12-15T16:00:00", 78 | eventHallName: "Bæjarbíó (Hafnarfirði)", 79 | imageSource: 80 | "https://d30qys758zh01z.cloudfront.net/images/medium/1.10700.jpg" 81 | }, 82 | { 83 | eventDateName: "Hera Björk - Ilmur af jólum - Í borg og bæ", 84 | name: "Hólmavík", 85 | dateOfShow: "2018-12-15T17:00:00", 86 | eventHallName: "Hólmavíkurkirkja", 87 | imageSource: 88 | "https://d30qys758zh01z.cloudfront.net/images/medium/1.10648.jpg" 89 | }, 90 | { 91 | eventDateName: "Hátíðartónleikar Eyþórs Inga", 92 | name: "Víðistaðakirkja", 93 | dateOfShow: "2018-12-15T20:00:00", 94 | eventHallName: "Víðistaðakirkja (Hafnarfirði)", 95 | imageSource: 96 | "https://d30qys758zh01z.cloudfront.net/images/medium/1.10630.jpg" 97 | }, 98 | { 99 | eventDateName: "Jólin til þín", 100 | name: "Höfn", 101 | dateOfShow: "2018-12-15T20:00:00", 102 | eventHallName: "Íþróttahúsið á Höfn", 103 | imageSource: 104 | "https://d30qys758zh01z.cloudfront.net/images/medium/1.10647.jpg" 105 | }, 106 | { 107 | eventDateName: "Jólalögin þeirra", 108 | name: "Tónleikar", 109 | dateOfShow: "2018-12-15T21:00:00", 110 | eventHallName: "Hendur í Höfn", 111 | imageSource: 112 | "https://d30qys758zh01z.cloudfront.net/images/medium/1.10687.jpg" 113 | } 114 | ]; 115 | 116 | class Home extends React.Component { 117 | render() { 118 | return ( 119 | 120 | 127 | {CONCERTS.map(concert => ( 128 | 138 | 📅 {concert.dateOfShow} 139 |
140 | 📍 {concert.eventHallName} 141 |
142 | ))} 143 |
144 |
145 | ); 146 | } 147 | } 148 | 149 | export default Home; 150 | ``` 151 | 152 | The Home component now renders a list of (hard-coded) concerts. Every concerts has a few properties: 153 | 154 | - `eventDateName: string` - the name of event 155 | - `name: string` - the name of artist 156 | - `dateOfShow: string` - the date of event 157 | - `eventHallName: string` - where the event takes place 158 | - `imageSource: string` - poster for the event 159 | 160 | We use two Base UI components: 161 | 162 | - `` - basic building block for layouts. In our example, we utilize CSS grid properties to build a responsive grid layout. 163 | - `` - to display the information about a single event. Note that we need to specify an unique `key` prop because it's React's requirement for array of components. Also, we use `overrides` to customize the styles of the root Card element (positioning and maximum width). 164 | 165 | ## Date formatting 166 | 167 | As you might notice, `2018-12-15T20:00:00` is not very human readable. We can use 3rd party library to make the formatting better 168 | 169 | ``` 170 | yarn add date-fns 171 | ``` 172 | 173 | Now import it into `home.js` 174 | 175 | ```jsx 176 | import { format } from "date-fns"; 177 | ``` 178 | 179 | and replace 180 | 181 | ``` 182 | concert.dateOfShow 183 | ``` 184 | 185 | with 186 | 187 | ``` 188 | format(concert.dateOfShow, "MM/DD/YYYY hh:mm A") 189 | ``` 190 | 191 | **Note:** You can often see usage of other library `Moment.js` We try to avoid it since it dramatically increases the size of the application. `date-fns` is much smaller, modular and tree-shakeable. 192 | 193 | ## Search 194 | 195 | First, we will create a search icon component that will be part of our search input. Create a new file `src/pages/search.js`: 196 | 197 | ```jsx 198 | // @flow 199 | import * as React from "react"; 200 | import { styled } from "fusion-plugin-styletron-react"; 201 | import SearchIcon from "baseui/icon/search"; 202 | 203 | const Icon = styled("div", { 204 | display: "flex", 205 | alignItems: "center", 206 | justifyContent: "center", 207 | height: "100%", 208 | marginRight: "1em" 209 | }); 210 | 211 | const SearchComponent = () => ( 212 | 213 | 214 | 215 | ); 216 | 217 | export default SearchComponent; 218 | ``` 219 | 220 | Now go back to `home.js` and add the imports 221 | 222 | ```jsx 223 | import { HeaderNavigation } from "baseui/header-navigation"; 224 | import { StatefulInput } from "baseui/input"; 225 | import Search from "./search"; 226 | ``` 227 | 228 | Our search is client-side only (API doesn't have a search parameter). We need to add a local search state 229 | 230 | ```jsx 231 | class Home extends React.Component<{}, { search: string }> { 232 | state = { 233 | search: "" 234 | }; 235 | // the rest of Home component.... 236 | ``` 237 | 238 | Let's add a header that will contain the search input 239 | 240 | ```jsx 241 | 242 | 243 | this.setState({ search: e.target.value })} 247 | /> 248 | 249 | {/* the rest of render method... */} 250 | ``` 251 | 252 | Now you should see the page header rendered. The last step is to filter concerts accordingly to `this.state.search` 253 | 254 | ```jsx 255 | CONCERTS.filter(concert => 256 | concert.name.toLowerCase().includes(this.state.search.toLowerCase()) 257 | ).map(/* ... */); 258 | ``` 259 | 260 | Our main UI is finished! 261 | 262 | ## Redux and fetching the data 263 | 264 | Redux is a popular state container for JavaScript apps. Fusion.js team maintains multiple plugins that make the integration easy. Let's add them and all other necessary dependencies 265 | 266 | ``` 267 | yarn add fusion-plugin-react-redux fusion-plugin-rpc-redux-react fusion-plugin-universal-events react-redux@5 redux isomorphic-fetch 268 | ``` 269 | 270 | `fusion-plugin-universal-events` is commonly required by other Fusion.js plugins and is used as an event emitter for data such as statistics and analytics. It's necessary for other redux plugins. 271 | 272 | `fusion-plugin-react-redux` adds basic integration of React-Redux into your Fusion.js application. It handles creating your store, wrapping your element tree in a provider, and serializing/deserializing your store between server and client. 273 | 274 | `fusion-plugin-rpc-redux-react` RPC is a natural way of expressing that a server-side function should be run in response to a client-side function call. It's an alternative to REST. This plugin provides a higher order component that connects RPC methods to Redux as well as React component props. It also helps to cut the typical redux boilerplate when creating action creators and reducers. 275 | 276 | But first things first, let's create a reducer in `src/redux/concerts.js` 277 | 278 | ```jsx 279 | // @flow 280 | import { createRPCReducer } from "fusion-plugin-rpc-redux-react"; 281 | 282 | export type ConcertT = { 283 | +name: string, 284 | +imageSource: string, 285 | +eventDateName: string, 286 | +dateOfShow: string, 287 | +eventHallName: string 288 | }; 289 | 290 | const initialState = { 291 | loading: false, 292 | data: [], 293 | error: null 294 | }; 295 | export default createRPCReducer< 296 | { loading: boolean, data: ConcertT[], error: ?string }, 297 | { payload: any, type: string } 298 | >( 299 | "getConcerts", 300 | { 301 | start: (state, action) => ({ ...state, loading: true }), 302 | success: (state, action) => ({ 303 | ...state, 304 | loading: false, 305 | data: action.payload 306 | }), 307 | failure: (state, action) => { 308 | return { 309 | ...state, 310 | loading: false, 311 | error: action.payload.message 312 | }; 313 | } 314 | }, 315 | initialState 316 | ); 317 | ``` 318 | 319 | And `src/redux/index.js` where we combine/re-export existing reducers so we can add even more reducers in the future 320 | 321 | ```jsx 322 | // @flow 323 | import { combineReducers } from "redux"; 324 | import concerts from "./concerts.js"; 325 | 326 | export default combineReducers({ 327 | concerts 328 | }); 329 | ``` 330 | 331 | Now we need to create an RPC handler. It's a function (endpoint) that will handle the data fetching of our concerts. It will be used by server-side rendering and it can be also called by the client. Create `src/rpc/index.js` 332 | 333 | ```jsx 334 | // @flow 335 | import { ResponseError } from "fusion-plugin-rpc-redux-react"; 336 | 337 | export default { 338 | getConcerts: async () => { 339 | try { 340 | const response = await fetch("https://apis.is/concerts"); 341 | if (response.status == 200) { 342 | const json = await response.json(); 343 | return json.results; 344 | } 345 | throw response.statusText; 346 | } catch (e) { 347 | throw new ResponseError(e); 348 | } 349 | } 350 | }; 351 | ``` 352 | 353 | The next step is to put it all together in `src/main.js` 354 | 355 | ```jsx 356 | import Redux, { ReduxToken, ReducerToken } from "fusion-plugin-react-redux"; 357 | import RPC, { RPCToken, RPCHandlersToken } from "fusion-plugin-rpc-redux-react"; 358 | import UniversalEvents, { 359 | UniversalEventsToken 360 | } from "fusion-plugin-universal-events"; 361 | import { FetchToken } from "fusion-tokens"; 362 | import reducer from "./redux/index.js"; 363 | import handlers from "./rpc/index.js"; 364 | import fetch from "isomorphic-fetch"; 365 | 366 | export default () => { 367 | /* ... */ 368 | app.register(RPCToken, RPC); 369 | app.register(UniversalEventsToken, UniversalEvents); 370 | __NODE__ 371 | ? app.register(RPCHandlersToken, handlers) 372 | : app.register(FetchToken, fetch); 373 | app.register(ReduxToken, Redux); 374 | app.register(ReducerToken, reducer); 375 | /* ... */ 376 | return app; 377 | }; 378 | ``` 379 | 380 | Finally, **let's remove the hardcoded concerts** and connect the home component to the redux (`getConcerts` store). 381 | 382 | Add these imports into `src/pages/home.js` 383 | 384 | ```jsx 385 | // redux and fusion helpers 386 | import { compose } from "redux"; 387 | import { connect } from "react-redux"; 388 | import { prepared } from "fusion-react"; 389 | import { withRPCRedux } from "fusion-plugin-rpc-redux-react"; 390 | 391 | // types 392 | import type { ConcertT } from "../redux/concerts"; 393 | ``` 394 | 395 | And this will create an HOC that connects our page to the store and it also triggers `getConcerts` fetch when doing server-side rendering 396 | 397 | ```jsx 398 | const hoc = compose( 399 | // generates Redux actions and 400 | // a React prop for the `getConcerts` RPC call 401 | withRPCRedux("getConcerts"), 402 | // expose the Redux state to React props 403 | connect(({ concerts }) => ({ concerts })), 404 | // invokes the passed in method on component hydration 405 | prepared(props => { 406 | if (props.concerts.loading || props.concerts.data.length) { 407 | return Promise.resolve(); 408 | } 409 | return props.getConcerts(); 410 | }) 411 | ); 412 | 413 | export default hoc(Home); 414 | ``` 415 | 416 | Let's update our flow types 417 | 418 | ```jsx 419 | class Home extends React.Component< 420 | { 421 | getConcerts: () => void, 422 | concerts: { data: ConcertT[], error: ?string } 423 | }, 424 | { search: string } 425 | > { 426 | /* ... */ 427 | } 428 | ``` 429 | 430 | And finally, replace `CONCERTS` with `this.props.concerts.data`. Now you should see the list of events again 🎉🎉🎉. However, this time our application fetches them from the public API! 431 | 432 | Note the client doesn't **NOT** make an XHR call to `https://apis.is/concerts` - it's all done on the server and browser already receives all events (rendered HTML elements and also as serialized redux state). **That's it!** 433 | 434 | The Home component has also an access to `this.props.getConcerts()`, so for example, you could re-fetch the data like this 435 | 436 | ```jsx 437 | componentDidMount() { 438 | // optional re-fetch on the client 439 | this.props.getConcerts(); 440 | } 441 | ``` 442 | 443 | For the full example you can clone and explore this repository. It has some extra code to gracefully handle fetch errors and display a nice user notification. 444 | 445 | ## More resources 446 | 447 | For detailed documentation please visit: 448 | 449 | - https://fusionjs.com 450 | - https://baseui.design/ 451 | -------------------------------------------------------------------------------- /flow-typed/globals.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | declare var __NODE__: boolean; 3 | declare var __BROWSER__: boolean; 4 | declare var __DEV__: boolean; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fusion-baseui-app", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "dependencies": { 6 | "baseui": "^9.24.0", 7 | "date-fns": "^2.8.1", 8 | "flow-bin": "0.110.0", 9 | "fusion-cli": "^2.5.0", 10 | "fusion-core": "^2.0.8", 11 | "fusion-plugin-i18n": "^2.3.5", 12 | "fusion-plugin-react-helmet-async": "^2.1.4", 13 | "fusion-plugin-react-redux": "^2.0.8", 14 | "fusion-plugin-react-router": "^2.1.2", 15 | "fusion-plugin-rpc": "^3.3.3", 16 | "fusion-plugin-rpc-redux-react": "^4.0.8", 17 | "fusion-plugin-styletron-react": "^3.0.11", 18 | "fusion-plugin-universal-events": "^2.0.9", 19 | "fusion-react": "^3.1.8", 20 | "fusion-tokens": "^2.0.8", 21 | "isomorphic-fetch": "^2.2.1", 22 | "prop-types": "^15.6.2", 23 | "react": "^16.12.0", 24 | "react-dom": "^16.12.0", 25 | "react-redux": "^7.1.3", 26 | "redux": "^4.0.4", 27 | "styletron-react": "^5.2.6" 28 | }, 29 | "scripts": { 30 | "dev": "fusion dev", 31 | "flow": "flow", 32 | "test": "fusion test", 33 | "build": "fusion build", 34 | "build-production": "fusion build --production", 35 | "start": "fusion start" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tajo/fusion-baseui/0a17ac4ca0afd677337e287842978f926a05931c/preview.png -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import App from "fusion-react"; 3 | import Router from "fusion-plugin-react-router"; 4 | import Styletron from "fusion-plugin-styletron-react"; 5 | import UniversalEvents, { 6 | UniversalEventsToken 7 | } from "fusion-plugin-universal-events"; 8 | import Redux, { ReduxToken, ReducerToken } from "fusion-plugin-react-redux"; 9 | import RPC, { RPCToken, RPCHandlersToken } from "fusion-plugin-rpc-redux-react"; 10 | import { FetchToken } from "fusion-tokens"; 11 | import reducer from "./redux/index.js"; 12 | import handlers from "./rpc/index.js"; 13 | import fetch from "isomorphic-fetch"; 14 | 15 | import root from "./root.js"; 16 | 17 | export default () => { 18 | const app = new App(root); 19 | app.register(Styletron); 20 | app.register(Router); 21 | app.register(RPCToken, RPC); 22 | app.register(UniversalEventsToken, UniversalEvents); 23 | __NODE__ 24 | ? app.register(RPCHandlersToken, handlers) 25 | : app.register(FetchToken, fetch); 26 | app.register(ReduxToken, Redux); 27 | app.register(ReducerToken, reducer); 28 | return app; 29 | }; 30 | -------------------------------------------------------------------------------- /src/pages/home.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | import { format, parseISO } from "date-fns"; 4 | 5 | // Base UI components 6 | import { Card } from "baseui/card"; 7 | import { Block } from "baseui/block"; 8 | import { HeaderNavigation } from "baseui/header-navigation"; 9 | import { StatefulInput } from "baseui/input"; 10 | import { Notification, KIND } from "baseui/notification"; 11 | import Search from "./search"; 12 | 13 | // redux and fusion helpers 14 | import { compose } from "redux"; 15 | import { connect } from "react-redux"; 16 | import { prepared } from "fusion-react"; 17 | import { withRPCRedux } from "fusion-plugin-rpc-redux-react"; 18 | 19 | // types 20 | import type { ConcertT } from "../redux/concerts"; 21 | 22 | const formatDate = (dateOfShow: string) => { 23 | return format(parseISO(dateOfShow), "mm/dd/yyyy hh:mm") 24 | } 25 | 26 | class Home extends React.Component< 27 | { 28 | getConcerts: () => void, 29 | concerts: { data: ConcertT[], error: ?string } 30 | }, 31 | { search: string } 32 | > { 33 | state = { 34 | search: "" 35 | }; 36 | componentDidMount() { 37 | // optional re-fetch on the client 38 | this.props.getConcerts(); 39 | } 40 | render() { 41 | const { concerts } = this.props; 42 | if (concerts.error) { 43 | return ( 44 | 45 | {concerts.error} 46 | 47 | ); 48 | } 49 | return ( 50 | 51 | 52 | this.setState({ search: e.target.value })} 56 | /> 57 | 58 | 65 | {concerts.data && 66 | concerts.data 67 | .filter(concert => 68 | concert.name 69 | .toLowerCase() 70 | .includes(this.state.search.toLowerCase()) 71 | ) 72 | .map(concert => ( 73 | 83 | 📅 {formatDate(concert.dateOfShow)} 84 |
85 | 📍 {concert.eventHallName} 86 |
87 | ))} 88 |
89 |
90 | ); 91 | } 92 | } 93 | 94 | const hoc = compose( 95 | // generates Redux actions and 96 | // a React prop for the `getConcerts` RPC call 97 | withRPCRedux("getConcerts"), 98 | // expose the Redux state to React props 99 | connect(({ concerts }) => ({ concerts })), 100 | // invokes the passed in method on component hydration 101 | prepared(props => { 102 | if (props.concerts.loading || props.concerts.data.length) { 103 | return Promise.resolve(); 104 | } 105 | return props.getConcerts(); 106 | }) 107 | ); 108 | 109 | export default hoc(Home); 110 | -------------------------------------------------------------------------------- /src/pages/pageNotFound.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from 'react'; 3 | import {NotFound} from 'fusion-plugin-react-router'; 4 | 5 | const PageNotFound = () => ( 6 | 7 |
404
8 |
9 | ); 10 | 11 | export default PageNotFound; 12 | -------------------------------------------------------------------------------- /src/pages/search.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import * as React from "react"; 3 | import { styled } from "fusion-plugin-styletron-react"; 4 | import SearchIcon from "baseui/icon/search"; 5 | 6 | const Icon = styled("div", { 7 | display: "flex", 8 | alignItems: "center", 9 | justifyContent: "center", 10 | height: "100%", 11 | marginRight: "1em" 12 | }); 13 | 14 | const SearchComponent = () => ( 15 | 16 | 17 | 18 | ); 19 | 20 | export default SearchComponent; 21 | -------------------------------------------------------------------------------- /src/redux/concerts.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { createRPCReducer } from "fusion-plugin-rpc-redux-react"; 3 | 4 | export type ConcertT = { 5 | +name: string, 6 | +imageSource: string, 7 | +eventDateName: string, 8 | +dateOfShow: string, 9 | +eventHallName: string 10 | }; 11 | 12 | const initialState = { 13 | loading: false, 14 | data: [], 15 | error: null 16 | }; 17 | export default createRPCReducer< 18 | { loading: boolean, data: ConcertT[], error: ?string }, 19 | { payload: any, type: string } 20 | >( 21 | "getConcerts", 22 | { 23 | start: (state, action) => ({ ...state, loading: true }), 24 | success: (state, action) => ({ 25 | ...state, 26 | loading: false, 27 | data: action.payload 28 | }), 29 | failure: (state, action) => { 30 | return { 31 | ...state, 32 | loading: false, 33 | error: action.payload.message 34 | }; 35 | } 36 | }, 37 | initialState 38 | ); 39 | -------------------------------------------------------------------------------- /src/redux/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { combineReducers } from "redux"; 3 | import concerts from "./concerts.js"; 4 | 5 | export default combineReducers({ 6 | concerts 7 | }); 8 | -------------------------------------------------------------------------------- /src/root.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React from "react"; 3 | import { Route, Switch } from "fusion-plugin-react-router"; 4 | 5 | import Home from "./pages/home.js"; 6 | import PageNotFound from "./pages/pageNotFound.js"; 7 | 8 | const root = ( 9 | 10 | 11 | 12 | 13 | ); 14 | 15 | export default root; 16 | -------------------------------------------------------------------------------- /src/rpc/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import { ResponseError } from "fusion-plugin-rpc-redux-react"; 3 | 4 | export default { 5 | getConcerts: async () => { 6 | try { 7 | const response = await fetch("https://apis.is/concerts"); 8 | 9 | // TEST Error: Network 10 | // const response = await fetch("http://exampleofnetworkerror.com"); 11 | 12 | // TEST Error: Status code is not 200 13 | //const response = await fetch("http://httpstat.us/500"); 14 | 15 | if (response.status == 200) { 16 | const json = await response.json(); 17 | return json.results; 18 | } 19 | throw response.statusText; 20 | } catch (e) { 21 | throw new ResponseError(e); 22 | } 23 | } 24 | }; 25 | --------------------------------------------------------------------------------