diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..00c9940fd7 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "fbjs" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0db216bfa4..951d804f67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ npm-debug.log node_modules +ui/jspm_packages diff --git a/README.md b/README.md index 5c84566eaa..020ff6eb8f 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,24 @@ -# Adapt React Coding Challenge +# My solution for `Adapt React Coding Challenge` +All steps was acomplished but not in nice way. There are much to be improvment. +Still this quiz was interesting for me and give me opportunity to remember how to work with react. -Seems like you're trying out for a position at -[Adapt](https://adapt.dk/en) or you've found this and would like to -apply. Fork this repo and go at it ;) +What should be done (what kind of improvement must be done): -Your goal is to set up a React application, where users will be able to -edit book info (and create new books, if you have enough time to -implement it). Once you are done with the challenge, please fire up a -Pull Request and we will get in touch. +* Decouple code by using flux architecture. Such pattern should seperate view from logic +* Implement error handling in case if api server will not respond +* Refactor code +* and much more ... :( -## Brief +# How to run -I am a user of the app and I want to create a book object and edit -previously provided info about it, so that my reading list can stay up -to date. The form should be split into three steps: +I'm usin SystemJS for building jspm for dependency managament and live-server for serving application -1. Choose subject (one of the two). -2. Depending on the selection in the first step, display a list of - reading material. Choose one. -3. When reading material chosen, display all the info that's available - about the book in a form (meaning that the book info can be edited). +First of all install dependencies: -## Requirements +* npm install && npm i --dev +* jspm install && jspm i --dev -* All steps should be visible on the screen and changable at all times - (when they are available -- step 1 when nothing is picked, step 1 - and 2 when step 1 is picked and step 1, 2 and 3 when step 2 is - picked). - -* You can use whatever libraries, task runners and build processes you - like. React and plain JavaScript are the only requirements (ES6 - encouraged, but no TypeScript, CoffeeScript, etc). Redux is strongly - encouraged if you see a need for it. - -### Suggested order of completion - -This depends on how much time you were given to accomplish the task. -Ideally you would provide a solution for each of the outlined steps -unless they are marked as optional. - -1. Data fetching from the api. -2. Form steps logic. -3. (optional) Saving the data. -4. (optional) Styling (minor for a 2-3 h challenge, more if there's more time). - -## API Usage - -API can be launched using `npm start`. You will need to run `npm -install` once starting working on the project to install dependencies. - -| Endpoint | Result | -|------------------------------|-----------------------------------------------------| -| /books?subjects_like=Fiction | Lists all books that contain "Fiction" as a subject | -| /subjects | Lists all available subjects | - ---- - -More info about API usage can be found at the [json-server -repo](https://github.com/typicode/json-server). +Then run application: +* npm run start-api +* npm run start-ui diff --git a/api.json b/api.json index 3df7e1b179..1c43e4588f 100644 --- a/api.json +++ b/api.json @@ -1,120 +1,100 @@ { - "subjects": ["Fiction", "Science"], + "subjects": [ + "Fiction", + "Science" + ], "books": [ - { - "id":1342, - "authors":[ - { - "birth_year":1775, - "death_year":1817, - "name":"Austen, Jane" - } - ], - "bookshelves":[ - "Best Books Ever Listings", - "Harvard Classics" - ], - "download_count":45668, - "formats":{ - "text/plain; charset=utf-8":"http://www.gutenberg.org/files/1342/1342-0.txt", - "application/pdf":"http://www.gutenberg.org/files/1342/1342-pdf.pdf", - "application/rdf+xml":"http://www.gutenberg.org/ebooks/1342.rdf", - "application/x-mobipocket-ebook":"http://www.gutenberg.org/ebooks/1342.kindle.noimages", - "application/epub+zip":"http://www.gutenberg.org/ebooks/1342.epub.images", - "text/plain; charset=us-ascii":"http://www.gutenberg.org/files/1342/1342.txt", - "text/html; charset=utf-8":"http://www.gutenberg.org/files/1342/1342-h/1342-h.htm" - }, - "languages":[ - "en" - ], - "media_type":"Text", - "subjects": ["Fiction"], - "title":"Pride and Prejudice" + { + "id": 1342, + "authors": [ + { + "birth_year": 1775, + "death_year": 1817, + "name": "Austenas" + } + ], + "bookshelves": [ + "Best Books Ever Listings", + "Harvard Classics" + ], + "download_count": 45668, + "formats": { + "text/plain; charset=utf-8": "http://www.gutenberg.org/files/1342/1342-0.txt", + "application/pdf": "http://www.gutenberg.org/files/1342/1342-pdf.pdf", + "application/rdf+xml": "http://www.gutenberg.org/ebooks/1342.rdf", + "application/x-mobipocket-ebook": "http://www.gutenberg.org/ebooks/1342.kindle.noimages", + "application/epub+zip": "http://www.gutenberg.org/ebooks/1342.epub.images", + "text/plain; charset=us-ascii": "http://www.gutenberg.org/files/1342/1342.txt", + "text/html; charset=utf-8": "http://www.gutenberg.org/files/1342/1342-h/1342-h.htm" }, - { - "id":33283, - "authors":[ - { - "birth_year":1851, - "death_year":1916, - "name":"Thompson, Silvanus P. (Silvanus Phillips)" - } - ], - "bookshelves":[ - "Mathematics" - ], - "download_count":24344, - "formats":{ - "application/prs.tex":"http://www.gutenberg.org/files/33283/33283-t.zip", - "application/rdf+xml":"http://www.gutenberg.org/ebooks/33283.rdf", - "application/pdf":"http://www.gutenberg.org/files/33283/33283-pdf.pdf" - }, - "languages":[ - "en" - ], - "media_type":"Text", - "subjects": ["Fiction"], - "title":"Calculus Made Easy: Being a very-simplest introduction to those beautiful methods which are generally called by the terrifying names of the Differential Calculus and the Integral Calculus" + "languages": [ + "en" + ], + "media_type": "Text", + "subjects": [ + "Fiction" + ], + "title": "Pride and Prejudice" + }, + { + "authors": [ + { + "birth_year": 1832, + "death_year": 1898, + "name": "Carroll, Lew" + } + ], + "bookshelves": [ + "Children's Literature" + ], + "download_count": 17866, + "formats": { + "text/plain; charset=utf-8": "http://www.gutenberg.org/files/11/11-0.zip", + "application/rdf+xml": "http://www.gutenberg.org/ebooks/11.rdf", + "application/pdf": "http://www.gutenberg.org/files/11/11-pdf.pdf", + "application/x-mobipocket-ebook": "http://www.gutenberg.org/ebooks/11.kindle.noimages", + "application/epub+zip": "http://www.gutenberg.org/ebooks/11.epub.images", + "text/plain; charset=us-ascii": "http://www.gutenberg.org/files/11/11.zip", + "application/zip": "http://www.gutenberg.org/files/11/11-h.zip", + "text/html; charset=utf-8": "http://www.gutenberg.org/files/11/11-h/11-h.htm" }, - { - "id":11, - "authors":[ - { - "birth_year":1832, - "death_year":1898, - "name":"Carroll, Lewis" - } - ], - "bookshelves":[ - "Children's Literature" - ], - "download_count":17866, - "formats":{ - "text/plain; charset=utf-8":"http://www.gutenberg.org/files/11/11-0.zip", - "application/rdf+xml":"http://www.gutenberg.org/ebooks/11.rdf", - "application/pdf":"http://www.gutenberg.org/files/11/11-pdf.pdf", - "application/x-mobipocket-ebook":"http://www.gutenberg.org/ebooks/11.kindle.noimages", - "application/epub+zip":"http://www.gutenberg.org/ebooks/11.epub.images", - "text/plain; charset=us-ascii":"http://www.gutenberg.org/files/11/11.zip", - "application/zip":"http://www.gutenberg.org/files/11/11-h.zip", - "text/html; charset=utf-8":"http://www.gutenberg.org/files/11/11-h/11-h.htm" - }, - "languages":[ - "en" - ], - "media_type":"Text", - "subjects": ["Fiction", "Science"], - "title":"Alice's Adventures in Wonderland" + "languages": [ + "en" + ], + "media_type": "Text", + "subjects": [ + "Fiction", + "Science" + ], + "title": "Alice's Adventures in Wonderland", + "id": 33288 + }, + { + "authors": [ + { + "birth_year": "1850", + "death_year": 1916, + "name": "Thompson, Silvanus P." + } + ], + "bookshelves": [ + "Mathematics" + ], + "download_count": 24344, + "formats": { + "application/prs.tex": "http://www.gutenberg.org/files/33283/33283-t.zip", + "application/rdf+xml": "http://www.gutenberg.org/ebooks/33283.rdf", + "application/pdf": "http://www.gutenberg.org/files/33283/33283-pdf.pdf" }, - { - "id":74, - "authors":[ - { - "birth_year":1835, - "death_year":1910, - "name":"Twain, Mark" - } - ], - "bookshelves":[ - "Banned Books from Anne Haight's list" - ], - "download_count":16204, - "formats":{ - "image/jpeg":"http://www.gutenberg.org/cache/epub/74/pg74.cover.medium.jpg", - "text/plain; charset=utf-8":"http://www.gutenberg.org/files/74/74-0.zip", - "application/rdf+xml":"http://www.gutenberg.org/ebooks/74.rdf", - "application/x-mobipocket-ebook":"http://www.gutenberg.org/ebooks/74.kindle.images", - "application/epub+zip":"http://www.gutenberg.org/ebooks/74.epub.noimages", - "text/html; charset=utf-8":"http://www.gutenberg.org/files/74/74-h/74-h.htm", - "text/plain; charset=iso-8859-1":"http://www.gutenberg.org/files/74/74.txt", - "application/zip":"http://www.gutenberg.org/files/74/74.zip" - }, - "languages":[ - "en" - ], - "media_type":"Text", - "subjects": ["Science"], - "title":"The Adventures of Tom Sawyer" - } + "languages": [ + "en" + ], + "media_type": "Text", + "subjects": [ + "Fiction" + ], + "title": "Calculus Made Easy: Being a very-simplest introduction to those beautiful methods which are generally called by the terrifying names of the Differential Calculus and the Integral Calculus", + "id": 33289 + } ] -} +} \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000000..314d7736f6 --- /dev/null +++ b/index.js @@ -0,0 +1,15 @@ +var liveServer = require('live-server'); +var proxy = require('http-proxy-middleware'); + +var params = { + port: 8080, + host: '0.0.0.0', + root: './ui', + ignore: 'jspm_packages', + wait: 1000, + // mount: [[]], + logLevel: 2, + // middleware: [proxy('http://127.0.0.1:3010')], +}; + +liveServer.start(params); \ No newline at end of file diff --git a/package.json b/package.json index 362aa65404..b20a30d22f 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,52 @@ "version": "1.0.0", "description": "A package containing a simple dummy book API, which should be used in the challenge", "scripts": { - "start": "json-server --port 3010 --watch api.json" + "start-api": "json-server --port 3010 --watch api.json", + "start-ui": "node index.js" }, - "keywords": ["react challenge", "tryouts", "react developer", "hiring"], + "keywords": [ + "react challenge", + "tryouts", + "react developer", + "hiring" + ], "author": "Adapt A/S", "license": "ISC", + "jspm": { + "directories": { + "baseURL": "ui" + }, + "dependencies": { + "bootstrap": "npm:bootstrap@^3.3.7", + "isomorphic-fetch": "npm:isomorphic-fetch@^2.2.1", + "jsx": "github:floatdrop/plugin-jsx@^1.2.1", + "path-to-regexp": "npm:path-to-regexp@^1.7.0", + "react": "npm:react@^15.6.1", + "react-bootstrap": "npm:react-bootstrap@^0.31.1", + "react-dom": "npm:react-dom@^15.6.1", + "react-router": "npm:react-router@^4.1.2", + "react-router-bootstrap": "npm:react-router-bootstrap@^0.24.2", + "react-router-dom": "npm:react-router-dom@^4.1.2", + "underscore": "npm:underscore@^1.8.3" + }, + "devDependencies": { + "babel": "npm:babel-core@^5.8.24", + "babel-runtime": "npm:babel-runtime@^5.8.24", + "core-js": "npm:core-js@^1.1.4" + } + }, "devDependencies": { - "json-server": "0.10.1" + "babel-eslint": "^7.2.3", + "eslint": "^4.3.0", + "eslint-config-fbjs": "^2.0.0", + "eslint-plugin-babel": "^4.1.1", + "eslint-plugin-flowtype": "^2.35.0", + "eslint-plugin-jsx-a11y": "^6.0.2", + "eslint-plugin-react": "^7.1.0", + "eslint-plugin-relay": "0.0.8", + "http-proxy-middleware": "^0.17.4", + "json-server": "0.10.1", + "jspm": "^0.16.53", + "live-server": "^1.2.0" } } diff --git a/ui/assets/logo.png b/ui/assets/logo.png new file mode 100644 index 0000000000..8a4f8bf95f Binary files /dev/null and b/ui/assets/logo.png differ diff --git a/ui/config.js b/ui/config.js new file mode 100644 index 0000000000..e46f2d49bc --- /dev/null +++ b/ui/config.js @@ -0,0 +1,717 @@ +System.config({ + baseURL: "/", + defaultJSExtensions: true, + transpiler: "babel", + babelOptions: { + "optional": [ + "runtime", + "optimisation.modules.system" + ] + }, + paths: { + "github:*": "jspm_packages/github/*", + "npm:*": "jspm_packages/npm/*" + }, + + map: { + "babel": "npm:babel-core@5.8.38", + "babel-runtime": "npm:babel-runtime@5.8.38", + "bootstrap": "npm:bootstrap@3.3.7", + "core-js": "npm:core-js@1.2.7", + "isomorphic-fetch": "npm:isomorphic-fetch@2.2.1", + "jsx": "github:floatdrop/plugin-jsx@1.2.1", + "path-to-regexp": "npm:path-to-regexp@1.7.0", + "react": "npm:react@15.6.1", + "react-bootstrap": "npm:react-bootstrap@0.31.1", + "react-dom": "npm:react-dom@15.6.1", + "react-router": "npm:react-router@4.1.2", + "react-router-bootstrap": "npm:react-router-bootstrap@0.24.2", + "react-router-dom": "npm:react-router-dom@4.1.2", + "underscore": "npm:underscore@1.8.3", + "github:floatdrop/plugin-jsx@1.2.1": { + "react-tools": "npm:react-tools@0.13.3" + }, + "github:jspm/nodelibs-assert@0.1.0": { + "assert": "npm:assert@1.4.1" + }, + "github:jspm/nodelibs-buffer@0.1.1": { + "buffer": "npm:buffer@5.0.6" + }, + "github:jspm/nodelibs-constants@0.1.0": { + "constants-browserify": "npm:constants-browserify@0.0.1" + }, + "github:jspm/nodelibs-crypto@0.1.0": { + "crypto-browserify": "npm:crypto-browserify@3.11.1" + }, + "github:jspm/nodelibs-domain@0.1.0": { + "domain-browser": "npm:domain-browser@1.1.7" + }, + "github:jspm/nodelibs-events@0.1.1": { + "events": "npm:events@1.0.2" + }, + "github:jspm/nodelibs-http@1.7.1": { + "Base64": "npm:Base64@0.2.1", + "events": "github:jspm/nodelibs-events@0.1.1", + "inherits": "npm:inherits@2.0.1", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "url": "github:jspm/nodelibs-url@0.1.0", + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "github:jspm/nodelibs-https@0.1.0": { + "https-browserify": "npm:https-browserify@0.0.0" + }, + "github:jspm/nodelibs-os@0.1.0": { + "os-browserify": "npm:os-browserify@0.1.2" + }, + "github:jspm/nodelibs-path@0.1.0": { + "path-browserify": "npm:path-browserify@0.0.0" + }, + "github:jspm/nodelibs-process@0.1.2": { + "process": "npm:process@0.11.10" + }, + "github:jspm/nodelibs-stream@0.1.0": { + "stream-browserify": "npm:stream-browserify@1.0.0" + }, + "github:jspm/nodelibs-string_decoder@0.1.0": { + "string_decoder": "npm:string_decoder@0.10.31" + }, + "github:jspm/nodelibs-url@0.1.0": { + "url": "npm:url@0.10.3" + }, + "github:jspm/nodelibs-util@0.1.0": { + "util": "npm:util@0.10.3" + }, + "github:jspm/nodelibs-vm@0.1.0": { + "vm-browserify": "npm:vm-browserify@0.0.4" + }, + "github:jspm/nodelibs-zlib@0.1.0": { + "browserify-zlib": "npm:browserify-zlib@0.1.4" + }, + "npm:acorn@4.0.13": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:amdefine@1.0.1": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "module": "github:jspm/nodelibs-module@0.1.0", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:asap@2.0.6": { + "domain": "github:jspm/nodelibs-domain@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:asn1.js@4.9.1": { + "bn.js": "npm:bn.js@4.11.7", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "inherits": "npm:inherits@2.0.1", + "minimalistic-assert": "npm:minimalistic-assert@1.0.0", + "vm": "github:jspm/nodelibs-vm@0.1.0" + }, + "npm:assert@1.4.1": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "process": "github:jspm/nodelibs-process@0.1.2", + "util": "npm:util@0.10.3" + }, + "npm:babel-runtime@5.8.38": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:babel-runtime@6.23.0": { + "core-js": "npm:core-js@2.4.1", + "regenerator-runtime": "npm:regenerator-runtime@0.10.5" + }, + "npm:bn.js@4.11.7": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1" + }, + "npm:bootstrap@3.3.7": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:brace-expansion@1.1.8": { + "balanced-match": "npm:balanced-match@1.0.0", + "concat-map": "npm:concat-map@0.0.1" + }, + "npm:browserify-aes@1.0.6": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "buffer-xor": "npm:buffer-xor@1.0.3", + "cipher-base": "npm:cipher-base@1.0.4", + "create-hash": "npm:create-hash@1.1.3", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "evp_bytestokey": "npm:evp_bytestokey@1.0.0", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "inherits": "npm:inherits@2.0.1", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:browserify-cipher@1.0.0": { + "browserify-aes": "npm:browserify-aes@1.0.6", + "browserify-des": "npm:browserify-des@1.0.0", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "evp_bytestokey": "npm:evp_bytestokey@1.0.0" + }, + "npm:browserify-des@1.0.0": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "cipher-base": "npm:cipher-base@1.0.4", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "des.js": "npm:des.js@1.0.0", + "inherits": "npm:inherits@2.0.1" + }, + "npm:browserify-rsa@4.0.1": { + "bn.js": "npm:bn.js@4.11.7", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "constants": "github:jspm/nodelibs-constants@0.1.0", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "randombytes": "npm:randombytes@2.0.5" + }, + "npm:browserify-sign@4.0.4": { + "bn.js": "npm:bn.js@4.11.7", + "browserify-rsa": "npm:browserify-rsa@4.0.1", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "create-hash": "npm:create-hash@1.1.3", + "create-hmac": "npm:create-hmac@1.1.6", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "elliptic": "npm:elliptic@6.4.0", + "inherits": "npm:inherits@2.0.1", + "parse-asn1": "npm:parse-asn1@5.1.0", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:browserify-zlib@0.1.4": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "pako": "npm:pako@0.2.9", + "process": "github:jspm/nodelibs-process@0.1.2", + "readable-stream": "npm:readable-stream@2.3.3", + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:buffer-xor@1.0.3": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:buffer@5.0.6": { + "base64-js": "npm:base64-js@1.2.1", + "ieee754": "npm:ieee754@1.1.8" + }, + "npm:cipher-base@1.0.4": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "inherits": "npm:inherits@2.0.1", + "safe-buffer": "npm:safe-buffer@5.1.1", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "string_decoder": "github:jspm/nodelibs-string_decoder@0.1.0" + }, + "npm:commander@2.11.0": { + "child_process": "github:jspm/nodelibs-child_process@0.1.0", + "events": "github:jspm/nodelibs-events@0.1.1", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:commoner@0.10.8": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "child_process": "github:jspm/nodelibs-child_process@0.1.0", + "commander": "npm:commander@2.11.0", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "detective": "npm:detective@4.5.0", + "events": "github:jspm/nodelibs-events@0.1.1", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "glob": "npm:glob@5.0.15", + "graceful-fs": "npm:graceful-fs@4.1.11", + "iconv-lite": "npm:iconv-lite@0.4.18", + "mkdirp": "npm:mkdirp@0.5.1", + "path": "github:jspm/nodelibs-path@0.1.0", + "private": "npm:private@0.1.7", + "process": "github:jspm/nodelibs-process@0.1.2", + "q": "npm:q@1.5.0", + "recast": "npm:recast@0.11.23", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:constants-browserify@0.0.1": { + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:core-js@1.2.7": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:core-js@2.4.1": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:core-util-is@1.0.2": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1" + }, + "npm:create-ecdh@4.0.0": { + "bn.js": "npm:bn.js@4.11.7", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "elliptic": "npm:elliptic@6.4.0" + }, + "npm:create-hash@1.1.3": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "cipher-base": "npm:cipher-base@1.0.4", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "inherits": "npm:inherits@2.0.1", + "ripemd160": "npm:ripemd160@2.0.1", + "sha.js": "npm:sha.js@2.4.8" + }, + "npm:create-hmac@1.1.6": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "cipher-base": "npm:cipher-base@1.0.4", + "create-hash": "npm:create-hash@1.1.3", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "inherits": "npm:inherits@2.0.1", + "ripemd160": "npm:ripemd160@2.0.1", + "safe-buffer": "npm:safe-buffer@5.1.1", + "sha.js": "npm:sha.js@2.4.8" + }, + "npm:create-react-class@15.6.0": { + "fbjs": "npm:fbjs@0.8.12", + "loose-envify": "npm:loose-envify@1.3.1", + "object-assign": "npm:object-assign@4.1.1", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:crypto-browserify@3.11.1": { + "browserify-cipher": "npm:browserify-cipher@1.0.0", + "browserify-sign": "npm:browserify-sign@4.0.4", + "create-ecdh": "npm:create-ecdh@4.0.0", + "create-hash": "npm:create-hash@1.1.3", + "create-hmac": "npm:create-hmac@1.1.6", + "diffie-hellman": "npm:diffie-hellman@5.0.2", + "inherits": "npm:inherits@2.0.1", + "pbkdf2": "npm:pbkdf2@3.0.12", + "public-encrypt": "npm:public-encrypt@4.0.0", + "randombytes": "npm:randombytes@2.0.5" + }, + "npm:des.js@1.0.0": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "inherits": "npm:inherits@2.0.1", + "minimalistic-assert": "npm:minimalistic-assert@1.0.0" + }, + "npm:detective@4.5.0": { + "acorn": "npm:acorn@4.0.13", + "defined": "npm:defined@1.0.0", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:diffie-hellman@5.0.2": { + "bn.js": "npm:bn.js@4.11.7", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "miller-rabin": "npm:miller-rabin@4.0.0", + "randombytes": "npm:randombytes@2.0.5", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:domain-browser@1.1.7": { + "events": "github:jspm/nodelibs-events@0.1.1" + }, + "npm:elliptic@6.4.0": { + "bn.js": "npm:bn.js@4.11.7", + "brorand": "npm:brorand@1.1.0", + "hash.js": "npm:hash.js@1.1.3", + "hmac-drbg": "npm:hmac-drbg@1.0.1", + "inherits": "npm:inherits@2.0.1", + "minimalistic-assert": "npm:minimalistic-assert@1.0.0", + "minimalistic-crypto-utils": "npm:minimalistic-crypto-utils@1.0.1", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:encoding@0.1.12": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "iconv-lite": "npm:iconv-lite@0.4.18" + }, + "npm:esprima-fb@13001.1001.0-dev-harmony-fb": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:esprima@3.1.3": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:evp_bytestokey@1.0.0": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "create-hash": "npm:create-hash@1.1.3", + "crypto": "github:jspm/nodelibs-crypto@0.1.0" + }, + "npm:fbjs@0.8.12": { + "core-js": "npm:core-js@1.2.7", + "isomorphic-fetch": "npm:isomorphic-fetch@2.2.1", + "loose-envify": "npm:loose-envify@1.3.1", + "object-assign": "npm:object-assign@4.1.1", + "process": "github:jspm/nodelibs-process@0.1.2", + "promise": "npm:promise@7.3.1", + "setimmediate": "npm:setimmediate@1.0.5", + "ua-parser-js": "npm:ua-parser-js@0.7.14" + }, + "npm:glob@5.0.15": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "events": "github:jspm/nodelibs-events@0.1.1", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "inflight": "npm:inflight@1.0.6", + "inherits": "npm:inherits@2.0.1", + "minimatch": "npm:minimatch@3.0.4", + "once": "npm:once@1.4.0", + "path": "github:jspm/nodelibs-path@0.1.0", + "path-is-absolute": "npm:path-is-absolute@1.0.1", + "process": "github:jspm/nodelibs-process@0.1.2", + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:graceful-fs@4.1.11": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "constants": "github:jspm/nodelibs-constants@0.1.0", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "process": "github:jspm/nodelibs-process@0.1.2", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:hash-base@2.0.2": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "inherits": "npm:inherits@2.0.1", + "stream": "github:jspm/nodelibs-stream@0.1.0" + }, + "npm:hash.js@1.1.3": { + "inherits": "npm:inherits@2.0.3", + "minimalistic-assert": "npm:minimalistic-assert@1.0.0" + }, + "npm:history@4.6.3": { + "invariant": "npm:invariant@2.2.2", + "loose-envify": "npm:loose-envify@1.3.1", + "resolve-pathname": "npm:resolve-pathname@2.1.0", + "value-equal": "npm:value-equal@0.2.1", + "warning": "npm:warning@3.0.0" + }, + "npm:hmac-drbg@1.0.1": { + "hash.js": "npm:hash.js@1.1.3", + "minimalistic-assert": "npm:minimalistic-assert@1.0.0", + "minimalistic-crypto-utils": "npm:minimalistic-crypto-utils@1.0.1", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:https-browserify@0.0.0": { + "http": "github:jspm/nodelibs-http@1.7.1" + }, + "npm:iconv-lite@0.4.18": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "process": "github:jspm/nodelibs-process@0.1.2", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "string_decoder": "github:jspm/nodelibs-string_decoder@0.1.0", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:inflight@1.0.6": { + "once": "npm:once@1.4.0", + "process": "github:jspm/nodelibs-process@0.1.2", + "wrappy": "npm:wrappy@1.0.2" + }, + "npm:inherits@2.0.1": { + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:inherits@2.0.3": { + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:invariant@2.2.2": { + "loose-envify": "npm:loose-envify@1.3.1", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:isomorphic-fetch@2.2.1": { + "node-fetch": "npm:node-fetch@1.7.1", + "whatwg-fetch": "npm:whatwg-fetch@2.0.3" + }, + "npm:jstransform@10.1.0": { + "base62": "npm:base62@0.1.1", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "esprima-fb": "npm:esprima-fb@13001.1001.0-dev-harmony-fb", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "process": "github:jspm/nodelibs-process@0.1.2", + "source-map": "npm:source-map@0.1.31" + }, + "npm:loose-envify@1.3.1": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "js-tokens": "npm:js-tokens@3.0.2", + "process": "github:jspm/nodelibs-process@0.1.2", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:miller-rabin@4.0.0": { + "bn.js": "npm:bn.js@4.11.7", + "brorand": "npm:brorand@1.1.0" + }, + "npm:minimatch@3.0.4": { + "brace-expansion": "npm:brace-expansion@1.1.8", + "path": "github:jspm/nodelibs-path@0.1.0" + }, + "npm:mkdirp@0.5.1": { + "fs": "github:jspm/nodelibs-fs@0.1.2", + "minimist": "npm:minimist@0.0.8", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:node-fetch@1.7.1": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "encoding": "npm:encoding@0.1.12", + "http": "github:jspm/nodelibs-http@1.7.1", + "https": "github:jspm/nodelibs-https@0.1.0", + "is-stream": "npm:is-stream@1.1.0", + "process": "github:jspm/nodelibs-process@0.1.2", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "url": "github:jspm/nodelibs-url@0.1.0", + "util": "github:jspm/nodelibs-util@0.1.0", + "zlib": "github:jspm/nodelibs-zlib@0.1.0" + }, + "npm:once@1.4.0": { + "wrappy": "npm:wrappy@1.0.2" + }, + "npm:os-browserify@0.1.2": { + "os": "github:jspm/nodelibs-os@0.1.0" + }, + "npm:pako@0.2.9": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:parse-asn1@5.1.0": { + "asn1.js": "npm:asn1.js@4.9.1", + "browserify-aes": "npm:browserify-aes@1.0.6", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "create-hash": "npm:create-hash@1.1.3", + "evp_bytestokey": "npm:evp_bytestokey@1.0.0", + "pbkdf2": "npm:pbkdf2@3.0.12", + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:path-browserify@0.0.0": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:path-is-absolute@1.0.1": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:path-to-regexp@1.7.0": { + "isarray": "npm:isarray@0.0.1" + }, + "npm:pbkdf2@3.0.12": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "create-hash": "npm:create-hash@1.1.3", + "create-hmac": "npm:create-hmac@1.1.6", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2", + "ripemd160": "npm:ripemd160@2.0.1", + "safe-buffer": "npm:safe-buffer@5.1.1", + "sha.js": "npm:sha.js@2.4.8" + }, + "npm:process-nextick-args@1.0.7": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:process@0.11.10": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "vm": "github:jspm/nodelibs-vm@0.1.0" + }, + "npm:promise@7.3.1": { + "asap": "npm:asap@2.0.6", + "fs": "github:jspm/nodelibs-fs@0.1.2" + }, + "npm:prop-types-extra@1.0.1": { + "react": "npm:react@15.6.1", + "warning": "npm:warning@3.0.0" + }, + "npm:prop-types@15.5.10": { + "fbjs": "npm:fbjs@0.8.12", + "loose-envify": "npm:loose-envify@1.3.1", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:public-encrypt@4.0.0": { + "bn.js": "npm:bn.js@4.11.7", + "browserify-rsa": "npm:browserify-rsa@4.0.1", + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "create-hash": "npm:create-hash@1.1.3", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "parse-asn1": "npm:parse-asn1@5.1.0", + "randombytes": "npm:randombytes@2.0.5" + }, + "npm:punycode@1.3.2": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:q@1.5.0": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:randombytes@2.0.5": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "crypto": "github:jspm/nodelibs-crypto@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2", + "safe-buffer": "npm:safe-buffer@5.1.1" + }, + "npm:react-bootstrap@0.31.1": { + "babel-runtime": "npm:babel-runtime@6.23.0", + "classnames": "npm:classnames@2.2.5", + "dom-helpers": "npm:dom-helpers@3.2.1", + "invariant": "npm:invariant@2.2.2", + "keycode": "npm:keycode@2.1.9", + "process": "github:jspm/nodelibs-process@0.1.2", + "prop-types": "npm:prop-types@15.5.10", + "prop-types-extra": "npm:prop-types-extra@1.0.1", + "react": "npm:react@15.6.1", + "react-dom": "npm:react-dom@15.6.1", + "react-overlays": "npm:react-overlays@0.7.0", + "react-prop-types": "npm:react-prop-types@0.4.0", + "uncontrollable": "npm:uncontrollable@4.1.0", + "warning": "npm:warning@3.0.0" + }, + "npm:react-dom@15.6.1": { + "fbjs": "npm:fbjs@0.8.12", + "loose-envify": "npm:loose-envify@1.3.1", + "object-assign": "npm:object-assign@4.1.1", + "process": "github:jspm/nodelibs-process@0.1.2", + "prop-types": "npm:prop-types@15.5.10", + "react": "npm:react@15.6.1" + }, + "npm:react-overlays@0.7.0": { + "classnames": "npm:classnames@2.2.5", + "dom-helpers": "npm:dom-helpers@3.2.1", + "prop-types": "npm:prop-types@15.5.10", + "react": "npm:react@15.6.1", + "react-dom": "npm:react-dom@15.6.1", + "react-prop-types": "npm:react-prop-types@0.4.0", + "warning": "npm:warning@3.0.0" + }, + "npm:react-prop-types@0.4.0": { + "react": "npm:react@15.6.1", + "warning": "npm:warning@3.0.0" + }, + "npm:react-router-bootstrap@0.24.2": { + "prop-types": "npm:prop-types@15.5.10", + "react": "npm:react@15.6.1", + "react-router-dom": "npm:react-router-dom@4.1.2" + }, + "npm:react-router-dom@4.1.2": { + "history": "npm:history@4.6.3", + "loose-envify": "npm:loose-envify@1.3.1", + "prop-types": "npm:prop-types@15.5.10", + "react": "npm:react@15.6.1", + "react-router": "npm:react-router@4.1.2" + }, + "npm:react-router@4.1.2": { + "history": "npm:history@4.6.3", + "hoist-non-react-statics": "npm:hoist-non-react-statics@1.2.0", + "invariant": "npm:invariant@2.2.2", + "loose-envify": "npm:loose-envify@1.3.1", + "path-to-regexp": "npm:path-to-regexp@1.7.0", + "prop-types": "npm:prop-types@15.5.10", + "react": "npm:react@15.6.1", + "warning": "npm:warning@3.0.0" + }, + "npm:react-tools@0.13.3": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "commoner": "npm:commoner@0.10.8", + "jstransform": "npm:jstransform@10.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:react@15.6.1": { + "create-react-class": "npm:create-react-class@15.6.0", + "fbjs": "npm:fbjs@0.8.12", + "loose-envify": "npm:loose-envify@1.3.1", + "object-assign": "npm:object-assign@4.1.1", + "process": "github:jspm/nodelibs-process@0.1.2", + "prop-types": "npm:prop-types@15.5.10" + }, + "npm:readable-stream@1.1.14": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "core-util-is": "npm:core-util-is@1.0.2", + "events": "github:jspm/nodelibs-events@0.1.1", + "inherits": "npm:inherits@2.0.1", + "isarray": "npm:isarray@0.0.1", + "process": "github:jspm/nodelibs-process@0.1.2", + "stream-browserify": "npm:stream-browserify@1.0.0", + "string_decoder": "npm:string_decoder@0.10.31" + }, + "npm:readable-stream@2.3.3": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "core-util-is": "npm:core-util-is@1.0.2", + "events": "github:jspm/nodelibs-events@0.1.1", + "inherits": "npm:inherits@2.0.3", + "isarray": "npm:isarray@1.0.0", + "process": "github:jspm/nodelibs-process@0.1.2", + "process-nextick-args": "npm:process-nextick-args@1.0.7", + "safe-buffer": "npm:safe-buffer@5.1.1", + "stream": "github:jspm/nodelibs-stream@0.1.0", + "string_decoder": "npm:string_decoder@1.0.3", + "util-deprecate": "npm:util-deprecate@1.0.2" + }, + "npm:recast@0.11.23": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "ast-types": "npm:ast-types@0.9.6", + "esprima": "npm:esprima@3.1.3", + "os": "github:jspm/nodelibs-os@0.1.0", + "private": "npm:private@0.1.7", + "process": "github:jspm/nodelibs-process@0.1.2", + "source-map": "npm:source-map@0.5.6" + }, + "npm:regenerator-runtime@0.10.5": { + "path": "github:jspm/nodelibs-path@0.1.0" + }, + "npm:ripemd160@2.0.1": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "hash-base": "npm:hash-base@2.0.2", + "inherits": "npm:inherits@2.0.1" + }, + "npm:safe-buffer@5.1.1": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1" + }, + "npm:setimmediate@1.0.5": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:sha.js@2.4.8": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "inherits": "npm:inherits@2.0.1", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:source-map@0.1.31": { + "amdefine": "npm:amdefine@1.0.1", + "fs": "github:jspm/nodelibs-fs@0.1.2", + "path": "github:jspm/nodelibs-path@0.1.0", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:source-map@0.5.6": { + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:stream-browserify@1.0.0": { + "events": "github:jspm/nodelibs-events@0.1.1", + "inherits": "npm:inherits@2.0.1", + "readable-stream": "npm:readable-stream@1.1.14" + }, + "npm:string_decoder@0.10.31": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1" + }, + "npm:string_decoder@1.0.3": { + "buffer": "github:jspm/nodelibs-buffer@0.1.1", + "safe-buffer": "npm:safe-buffer@5.1.1" + }, + "npm:ua-parser-js@0.7.14": { + "systemjs-json": "github:systemjs/plugin-json@0.1.2" + }, + "npm:uncontrollable@4.1.0": { + "invariant": "npm:invariant@2.2.2", + "process": "github:jspm/nodelibs-process@0.1.2", + "react": "npm:react@15.6.1" + }, + "npm:url@0.10.3": { + "assert": "github:jspm/nodelibs-assert@0.1.0", + "punycode": "npm:punycode@1.3.2", + "querystring": "npm:querystring@0.2.0", + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:util-deprecate@1.0.2": { + "util": "github:jspm/nodelibs-util@0.1.0" + }, + "npm:util@0.10.3": { + "inherits": "npm:inherits@2.0.1", + "process": "github:jspm/nodelibs-process@0.1.2" + }, + "npm:vm-browserify@0.0.4": { + "indexof": "npm:indexof@0.0.1" + }, + "npm:warning@3.0.0": { + "loose-envify": "npm:loose-envify@1.3.1", + "process": "github:jspm/nodelibs-process@0.1.2" + } + } +}); diff --git a/ui/index.html b/ui/index.html new file mode 100644 index 0000000000..3160df9936 --- /dev/null +++ b/ui/index.html @@ -0,0 +1,46 @@ + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/ui/src/app/about-comp.js b/ui/src/app/about-comp.js new file mode 100644 index 0000000000..3f3172b68c --- /dev/null +++ b/ui/src/app/about-comp.js @@ -0,0 +1,33 @@ +import React from 'react'; + + +const About = () => { + return ( +
+

Simple page application for getting info about books.

+

+ What was used for creating this application: +

+ +

+ The intemtion of this application was to remember how to work with react.
+ What would be nice to accomplish: +

+ +
+ ); +}; + +export default About; diff --git a/ui/src/app/app-comp.js b/ui/src/app/app-comp.js new file mode 100644 index 0000000000..371da43f9f --- /dev/null +++ b/ui/src/app/app-comp.js @@ -0,0 +1,48 @@ +import React, { Component } from 'react'; +import { Navbar, Nav, NavItem, Row, Image } from 'react-bootstrap'; +import { LinkContainer } from 'react-router-bootstrap'; + +import { RoutesRenderer } from './route-utils'; +import { routes } from './config'; + + +class App extends Component { + constructor(props) { + super(props); + } + + render() { + return ( +
+
+ + + + + + + + +
+ + {routes.map((route, i) => ( + + ))} + +
+ ); + } +} + +export default App; diff --git a/ui/src/app/book-comp.js b/ui/src/app/book-comp.js new file mode 100644 index 0000000000..a910c95ff0 --- /dev/null +++ b/ui/src/app/book-comp.js @@ -0,0 +1,98 @@ +import React, { PropTypes, Component } from 'react'; +import pathToRegexp from 'path-to-regexp'; +import { ListGroup, ListGroupItem, Col, Row } from 'react-bootstrap'; +import { LinkContainer } from 'react-router-bootstrap'; + +import { get } from './fetch-utils'; +import { RoutesRenderer } from './route-utils'; +import { + BOOKS_API_PATH, + BOOK_DETAIL_ROUTE_PATH, +} from './constants'; + + +const bookPath = pathToRegexp.compile(BOOKS_API_PATH); +const bookDetailPath = pathToRegexp.compile(BOOK_DETAIL_ROUTE_PATH); + +const defaultState = { + books: [ + { + id: null, + authors: [], + bookshelves: [], + download_count: null, + formats: {}, + languages: [], + media_type: null, + subjects:[], + title: null, + }, + ], +}; + +export class Book extends Component { + constructor(props) { + super(props); + this.state = { ...defaultState }; + } + + componentDidMount() { + const { subject } = this.props; + this.fetchBooks(subject); + } + + fetchBooks(bookId) { + get(bookPath({id: bookId})).then((res) => { + this.setState({ ...{ books: res } }); + }); + } + + onDetailsChange() { + // Update component if books details info was changed + const { subject } = this.props; + this.fetchBooks(subject); + } + + render() { + const { routes, subject } = this.props; + const { books } = this.state; + const onDetailsChange = this.onDetailsChange.bind(this); + + return ( + + + + {books.map((book, idx) => { + const bookId = book.id || '#'; + return ( + + {book.title} + + ); + })} + + + + {routes.map((route, i) => { + return ( + + ); + })} + + + ); + } +} + +Book.propTypes = { + subject: PropTypes.string.isRequired, + routes: PropTypes.array.isRequired, +}; + +export default Book; diff --git a/ui/src/app/config.js b/ui/src/app/config.js new file mode 100644 index 0000000000..c1a2ccba20 --- /dev/null +++ b/ui/src/app/config.js @@ -0,0 +1,31 @@ +import React from 'react'; + +import { RootRedirecter } from './route-utils'; +import About from './about-comp'; +import Subject from './subject-comp'; +import Details from './details-comp'; + + +export const routes = [ + { + path: '/', + exact: true, + component: () => (), + }, + { + path: '/books', + exact: true, + component: Subject, + routes: [ + { + path: '/books/:subject/:id', + component: Details, + }, + ], + }, + { + path: '/about', + exact: true, + component: About, + }, +]; \ No newline at end of file diff --git a/ui/src/app/constants.js b/ui/src/app/constants.js new file mode 100644 index 0000000000..2958de1c07 --- /dev/null +++ b/ui/src/app/constants.js @@ -0,0 +1,10 @@ +export const BASE_API_URL = 'http://127.0.0.1:3010'; +export const SUBJECTS_API_PATH = '/subjects'; +export const BOOKS_API_PATH = '/books?subjects_like=:id'; +export const BOOK_DETAIL_API_PATH = '/books?subjects_like=:subject&id=:id'; +export const BOOK_DETAIL_UPDATE_API_PATH = '/books/:id'; +export const BOOK_DELETE_API_PATH = BOOK_DETAIL_UPDATE_API_PATH; +export const BOOK_DETAIL_CREATE_API_PATH = '/books'; + +export const BOOK_DETAIL_ROUTE_PATH = '/books/:subject/:id'; +export const BOOKS_ROUTE_PATH = '/books'; diff --git a/ui/src/app/details-comp.js b/ui/src/app/details-comp.js new file mode 100644 index 0000000000..83f32c2158 --- /dev/null +++ b/ui/src/app/details-comp.js @@ -0,0 +1,349 @@ +import React, { PropTypes, Component } from 'react'; +import pathToRegexp from 'path-to-regexp'; +import { + Form, + FormGroup, + Col, + FormControl, + Button, + Jumbotron, + ButtonToolbar, +} from 'react-bootstrap'; +import _ from 'underscore'; + +import { get, put, post, del } from './fetch-utils'; +import { renderInputText, renderInputNumber } from './ui-utils'; +import { + BOOK_DETAIL_API_PATH, + BOOK_DETAIL_UPDATE_API_PATH, + BOOK_DETAIL_CREATE_API_PATH, + BOOK_DELETE_API_PATH, + BOOKS_ROUTE_PATH, +} from './constants'; + +const bookDetailsPath = pathToRegexp.compile(BOOK_DETAIL_API_PATH); +const bookUpdatePath = pathToRegexp.compile(BOOK_DETAIL_UPDATE_API_PATH); +const bookDeletePath = pathToRegexp.compile(BOOK_DELETE_API_PATH); + +const defaultState = { + details: { + id: null, + authors: [], + bookshelves: [], + download_count: null, + formats: {}, + languages: [], + media_type: null, + subjects:[], + title: null, + }, +}; + +export class Details extends Component { + constructor(props) { + super(props); + this.state = { + ...defaultState, + ...{ renderDetails: true }, + ...{ selectedItems: [] }, + }; + } + + componentDidMount() { + const { params } = this.props.match; + this.fetchBookDetails(params); + } + + componentWillReceiveProps(nextProps) { + const { params } = this.props.match; + const { params: nextParams } = nextProps.match; + + if (params.id !== nextParams.id || params.subject !== nextParams.subject) { + this.fetchBookDetails(nextParams); + } + + if (nextParams.subject !== nextProps.context.subject) { + this.setState({ ...defaultState, ...{ renderDetails: false } }); + } + } + + fetchBookDetails(params) { + get(bookDetailsPath(params)).then((res) => { + this.setState({ ...{ details: res[0] }, ...{ renderDetails: true } }); + }); + } + + textHandler(event) { + const fieldVal = event.target.value; + let fieldName = event.target.name; + let tokens = fieldName.split('['); + let detailsToUpdate; + + if (tokens.length > 1) { + fieldName = tokens[0]; + + const idx = tokens[1].split(']')[0]; + const key = tokens[1].split(']')[1].replace('.', ''); + + detailsToUpdate = {details: {...this.state.details}}; + key ? (detailsToUpdate.details[fieldName][idx][key] = fieldVal) : (detailsToUpdate.details[fieldName][idx] = fieldVal); + } else { + detailsToUpdate = { + details: {...this.state.details, [fieldName]: fieldVal}, + }; + } + + + this.setState({ ...detailsToUpdate }); + } + + numberHandler(event) { + const fieldVal = event.target.value; + const fieldName = event.target.name; + + const detailsToUpdate = { + details: {...this.state.details, [fieldName]: fieldVal}, + }; + + this.setState({ ...detailsToUpdate }); + } + + updateHandler(event) { + event.preventDefault(); + const { params } = this.props.match; + const { details } = this.state; + put(bookUpdatePath({ id: params.id }), details) + .then(() => {/* display nice message */}) + .catch(() => {/* display ugly message */}); + } + + createHandler(event) { + event.preventDefault(); + const { history, context: { onDetailsChange } } = this.props; + const { details } = { ...this.state }; + delete details.id; + + post(BOOK_DETAIL_CREATE_API_PATH, details) + .then(() => { + const location = { + pathname: BOOKS_ROUTE_PATH, + context: { update: true }, + }; + + onDetailsChange(); + + history.replace(location); + }) + .catch(() => {/* display ugly message */}); + } + + deleteHandler(event) { + event.preventDefault(); + const { params } = this.props.match; + const { history, context: { onDetailsChange } } = this.props; + const { details } = this.state; + del(bookDeletePath({ id: params.id }), details) + .then(() => { + const location = { + pathname: BOOKS_ROUTE_PATH, + context: { update: true }, + }; + + onDetailsChange(); + + history.replace(location); + }) + .catch(() => {/* display ugly message */}); + } + + addFormatHandler(event) { + event.preventDefault(); + let { details: detailsToUpdate } = this.state; + + if (this.formatKeyInput && this.formatValueInput) { + detailsToUpdate.formats[this.formatKeyInput.value] = this.formatValueInput.value; + this.formatKeyInput.value = ''; + this.formatValueInput.value = ''; + this.setState({ ...detailsToUpdate }); + } + } + + deleteFormatHandler(event) { + event.preventDefault(); + const { selectedItems, details } = this.state; + const formats = _.omit(details.formats, selectedItems); + let { details: detailsToUpdate } = this.state; + detailsToUpdate.formats = formats; + this.setState({ ...detailsToUpdate }); + } + + selectHandler(event) { + const target = event.target; + const options = [].slice.call(target.querySelectorAll('option')); + const items = options.filter((option) => (option.selected)).map((option) => (option.value)); + this.setState({ ...{ selectedItems: items } }); + } + + render() { + const { details, renderDetails } = this.state; + + return ( +
+ {/* + Ugly form code. But as I remember it's + always difficult to work with forms in react. + I'm to much tired to start using react-redux with redux-form. + Flux architecture (redux) can help to seperate all logic, + but I don't want to investigate to much in this for such simple app. + */} + {(details.id ? renderDetails : false) ? ( +
+ { + renderInputText( + 'title', + 'Title', + details.title + )(this.textHandler.bind(this)) + } + { + details.authors.map((author, idx) => { + return ( + + { + renderInputText( + `authors[${idx}].name`, + 'Author name', + author.name + )(this.textHandler.bind(this)) + } + { + renderInputText( + `authors[${idx}].birth_year`, + 'Author birth year', + author.birth_year + )(this.textHandler.bind(this)) + } + { + renderInputText( + `authors[${idx}].death_year`, + 'Author death year', + author.death_year + )(this.textHandler.bind(this)) + } + + ); + }) + } + {details.bookshelves.map((bookshelve, idx) => { + return ( + + { + renderInputText( + `bookshelves[${idx}]`, + 'Bookshelve', + bookshelve + )(this.textHandler.bind(this)) + } + + ); + })} + { + renderInputNumber( + 'download_count', + 'Download count', + details.download_count + )(this.numberHandler.bind(this)) + } + + + + + Formats + + + + {Object.keys(details.formats).map((key, idx) => { + return ; + })} + + + + + + + + + + + Format key + + + {/* + Not very nice solution but don't want to play + with other libraries (redux-form, ...) + */} + {this.formatKeyInput = ref;}} + defaultValue="" + /> + + + + + Format value + + + {/* + The same as above. Ugly solution + */} + {this.formatValueInput = ref;}} + defaultValue="" + /> + + + + + + + + + + + + + + + + + +
+ ) : ''} +
+ ); + } +} + +Details.propTypes = { + match: PropTypes.object.isRequired, +}; + +export default Details; diff --git a/ui/src/app/fetch-utils.js b/ui/src/app/fetch-utils.js new file mode 100644 index 0000000000..4b7d9df08c --- /dev/null +++ b/ui/src/app/fetch-utils.js @@ -0,0 +1,114 @@ +import 'isomorphic-fetch'; + +import { NO_DATA, BASE_API_URL } from './constants'; + + +export function checkStatus(response) { + if (response.ok) { + if (response.status === 204) { + return Promise.resolve({ + status: response.status, + statusText: response.statusText, + }); + } + // Workaround for API 200 status, when 'no data' has been returned + if (response.status === 200) { + return response.json() + // parsing json to get codeNumber, etc. + .then(json => { + if (json.codeNumber && json.codeNumber === NO_DATA) { + return Promise.resolve({ + noData: json + }); + } + return Promise.resolve(json); + }) + // if json parsing fails, return original response + .catch(() => { + return Promise.resolve(response); + }) + // return either json response or original + // response as rejected promise result + .then(result => { + if (result.noData) { + return Promise.reject(result.noData); + } + return Promise.resolve(result); + }); + } + return Promise.resolve(response.json()); + } + if (response.status === 400 + || response.status === 409 + || response.status === 404) { + + return response.json() + // parsing json to get codeNumber, etc. + .then(json => Promise.resolve(json)) + // if json parsing fails, return original error response + .catch(() => Promise.resolve(response)) + // return either json response or original + // error response as rejected promise result + .then(result => Promise.reject(result)); + } + return Promise.reject(response); +} + +export default function customFetch(url, options) { + return fetch(`${BASE_API_URL}${url}`, options); +} + +export function enhancedFetch(url, options = {}) { + options.credentials = 'same-origin'; + options.headers = Object.assign({ + Accept: 'application/json', + }, options.headers); + if (typeof options.body !== 'string') { + options.body = JSON.stringify(options.body); + } + return customFetch(url, options).then(checkStatus); +} + +export function get(url, headers) { + return enhancedFetch(url, { + method: 'get', + headers, + }); +} + +export function put(url, data) { + return enhancedFetch(url, { + method: 'put', + body: data, + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +export function post(url, data) { + return enhancedFetch(url, { + method: 'post', + body: data, + headers: { + 'Content-Type': 'application/json', + }, + }); +} + +export function xform(url, data) { + return enhancedFetch(url, { + method: 'post', + body: data, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8', + }, + }); +} + +export function del(url) { + return enhancedFetch(url, { + method: 'delete', + }); +} diff --git a/ui/src/app/route-utils.js b/ui/src/app/route-utils.js new file mode 100644 index 0000000000..7a5cb70bc4 --- /dev/null +++ b/ui/src/app/route-utils.js @@ -0,0 +1,33 @@ +import React, { Component, PropTypes } from 'react'; +import { Route, Redirect } from 'react-router-dom'; + +export const RoutesRenderer = (route) => ( + ( + React.cloneElement( + + ) + )}/> +); + +export class RootRedirecter extends Component { + constructor(props) { + super(props); + } + + render() { + const { to } = this.props; + + return ; + } +} + +RootRedirecter.propTypes = { + to: PropTypes.string.isRequired, +}; diff --git a/ui/src/app/subject-comp.js b/ui/src/app/subject-comp.js new file mode 100644 index 0000000000..2daec51da5 --- /dev/null +++ b/ui/src/app/subject-comp.js @@ -0,0 +1,61 @@ +import React, { Component, PropTypes } from 'react'; +import { Tabs, Tab, Alert } from 'react-bootstrap'; + +import Book from './book-comp'; +import { get } from './fetch-utils'; +import { + SUBJECTS_API_PATH, +} from './constants'; + + +const BookTabs = ({ subjects, routes }) => { + const createTabs = () => (subjects.map((subject, idx) => { + return ( + + + + ); + })); + + return ( + + {createTabs()} + + ); +}; + +BookTabs.propTypes = { + subjects: PropTypes.array.isRequired, + routes: PropTypes.array.isRequired, +}; + +class Subject extends Component { + constructor(props) { + super(props); + this.state = { subjects: [] }; + } + + componentDidMount() { + get(SUBJECTS_API_PATH).then((res) => { + this.setState({ ...{ subjects: res } }); + }); + } + + render() { + const { routes } = this.props; + const { subjects } = this.state; + + return ( +
+ {subjects.length >= 0 ? + : + +

Oh snap! You got an error!

+
+ } +
+ ); + } +} + +export default Subject; \ No newline at end of file diff --git a/ui/src/app/ui-utils.js b/ui/src/app/ui-utils.js new file mode 100644 index 0000000000..2e56a53767 --- /dev/null +++ b/ui/src/app/ui-utils.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { FormGroup, Col, FormControl } from 'react-bootstrap'; + +export const renderInputText = (name, label, value) => (callback) => { + return ( + + + {label} + + + + + + ); +}; + +export const renderInputNumber = (name, label, value) => (callback) => { + return ( + + + {label} + + + + + + ); +} \ No newline at end of file diff --git a/ui/src/main.js b/ui/src/main.js new file mode 100644 index 0000000000..2674cc4490 --- /dev/null +++ b/ui/src/main.js @@ -0,0 +1,12 @@ +import React from 'react'; +import { render } from 'react-dom'; +import { HashRouter as Router } from 'react-router-dom'; + +import App from './app/app-comp'; + + +render(( + + + +), document.getElementById('root'));