├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── README.md ├── Todo.txt ├── app ├── actions │ ├── actionTypes.js │ └── libraryActions.js ├── components │ ├── App.js │ ├── Banner.js │ ├── Book.js │ ├── BookAction.js │ ├── BookDetailsPage.js │ ├── BookHistory.js │ ├── BooksForm.js │ ├── BooksPage.js │ ├── Dashboard.js │ ├── Header.js │ ├── Home.js │ ├── LMSAuth.js │ ├── Loader.js │ ├── Main.js │ ├── OwnerDetails.js │ ├── RateBook.js │ ├── RequestAuthentication.js │ ├── SearchBook.js │ ├── notifications │ │ ├── NotificationTypes.js │ │ ├── NotifyMe.js │ │ └── status.js │ └── utils │ │ ├── BookInfo.js │ │ ├── BookRecord.js │ │ ├── CommentList.js │ │ ├── Image.js │ │ ├── LoginButton.js │ │ └── Ratings.js ├── config.js ├── css │ └── style.css ├── data │ └── library.js ├── img │ ├── Triad-01.png │ ├── book-unavailable.jpg │ ├── favicon.ico │ └── logo_pramati_trans_152.png ├── index.html ├── index.js ├── reducers │ ├── helpers.js │ ├── index.js │ └── libraryReducer.js ├── store │ └── index.js ├── test │ └── app │ │ ├── __mocks__ │ │ └── fileMock.js │ │ ├── actions │ │ └── libraryActions.test.js │ │ ├── components │ │ ├── App.test.js │ │ ├── Banner.test.js │ │ ├── Book.test.js │ │ ├── BookActions.test.js │ │ ├── BookDetailsPage.test.js │ │ ├── BookHistory.test.js │ │ ├── BooksForm.test.js │ │ ├── BooksPage.test.js │ │ ├── Dashboard.test.js │ │ ├── Header.test.js │ │ ├── Home.test.js │ │ ├── LMSAuth.test.js │ │ ├── Loader.test.js │ │ ├── OwnerDetails.test.js │ │ ├── RateBook.test.js │ │ ├── SearchBook.test.js │ │ ├── notifications │ │ │ └── NotificationTypes.test.js │ │ └── utils │ │ │ ├── BookInfo.test.js │ │ │ ├── BookRecord.test.js │ │ │ ├── CommentList.test.js │ │ │ ├── Image.test.js │ │ │ ├── LoginButton.test.js │ │ │ └── Ratings.test.js │ │ └── reducers │ │ └── libraryReducer.test.js └── web3.js ├── build └── contracts │ ├── ConvertLib.json │ ├── Killable.json │ ├── MetaCoin.json │ ├── Migrations.json │ ├── Ownable.json │ ├── StringLib.json │ └── strings.json ├── contracts ├── BooksLibrary.sol ├── DataStore.sol ├── LMS.sol ├── MembersLibrary.sol ├── Migrations.sol ├── OrgLibrary.sol ├── Organisation.sol ├── OrganisationInterface.sol ├── Parent.sol ├── StringLib.sol ├── helper_contracts │ ├── StringLib.sol │ ├── strings.sol │ └── zeppelin │ │ ├── lifecycle │ │ └── Killable.sol │ │ └── ownership │ │ └── Ownable.sol └── strings.sol ├── index.js ├── migrations ├── 1_initial_migration.js ├── 2_deploy_contracts.js ├── 3_deploy_parent.js └── 4_deploy_organisation.js ├── package-lock.json ├── package.json ├── scripts ├── add_bulk_books.js ├── build.sh ├── cron.sh ├── mine_on_tx.js ├── mock_data │ └── books.json └── overdue_books_reminder.js ├── server ├── config.js └── routes.js ├── test ├── helpers │ └── expectThrow.js ├── testBooksLibrary.js ├── testDataStore.js ├── testLMS.js ├── testMembersLibrary.js ├── testOrgLibrary.js ├── testOrganisation.js └── testParent.js ├── truffle.js ├── webpack.config.js ├── workflow.md └── zeppelin ├── lifecycle └── Killable.sol └── ownership └── Ownable.sol /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "es2015", 5 | "react" 6 | ], 7 | "plugins": [ 8 | "transform-object-rest-spread" 9 | ] 10 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | tmp/** 2 | build/** 3 | node_modules/** 4 | contracts/** 5 | coverage/** 6 | migrations/1_initial_migration.js 7 | migrations/2_deploy_contracts.js 8 | test/metacoin.js 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "eslint:recommended", 6 | "plugin:react/recommended" 7 | ], 8 | "plugins": [ 9 | "babel", 10 | "react" 11 | ], 12 | "rules": { 13 | "key-spacing" : 0, 14 | "jsx-quotes" : [2, "prefer-single"], 15 | "max-len" : [2, 120, 2], 16 | "object-curly-spacing" : [2, "always"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .idea 3 | .vscode 4 | build 5 | coverage 6 | dist/** 7 | installed_contracts 8 | node_modules 9 | npm-debug.log 10 | yarn-error.log 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | group: beta 4 | language: node_js 5 | node_js: 6 | - '6' 7 | notifications: 8 | slack: 9 | # Encrypted through Travis CLI 10 | secure: OQ+HKEdBjxkznrEb9wa19831hVwSkUeNX73LVDHYk3OMM6Lvo0N+4W05VrHcbkeu3ZkaW3GljfGR7sbsaNT8gzT3HhmynKl0Z7LCTMSWdDV0q55KoT/n3jMa9CaS6h93SB806sCvXnKrdf5npWQiNg6tpbZ0CF9GI+rsVa5LpPnK7sdpC1wX4E9ueG2S5E25LszZ9x+Pcocgk8N0XkIhtiQma9vboz8bO0J+ozm5sHvSUa1fDvDD/OiN7guXOoGIKk3EgKeuBRgVyAW6P9lRds4WSEMEXHu3cH67G/EhuxDxqQ0bjq/v9HenW8UqTxvH7XUujvDbcXb3mhXoYWFZIyYIyOktz3a0jXxLEExanb4jSpgTCu8CdVXfOyrUgqZDIAwnWstVdh4eidJg43yfOsFxmLoI3ROKwbm45dk7+O6LIVU8G6yyZv7lAYjCDuovnjXJG488BofdOOq0rnXaah7sh7s+CgPzkSgEzJu22W+Db0D0z8hRx9/l31IYvg3AQO1jEajP6v9YCPjfgC5mIl/h+oOeMFhNZyoZQUYgQtsIqiTKQTri2sXIs936CeupaiR0cAZIXbiVHS5BCjcqI2itsRPl3WRnQMnImSSbpV5uYnHjYWzF2D4HKMAOmSkc+SVos/9LmzsyTCzDlOQWg/aO4za4EC0Fpm48eaWgqxg= 11 | before_install: 12 | - npm install truffle@3.1.9 -g 13 | - npm i -g ethereumjs-testrpc 14 | - npm install 15 | script: 16 | - testrpc > /dev/null & 17 | - truffle compile 18 | - truffle test 19 | - npm run build 20 | - npm test 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LMS: A community library management system 2 | [![Build Status](https://travis-ci.org/Imaginea/lms.svg?branch=master)](https://travis-ci.org/Imaginea/lms) 3 | 4 | Pool your books to create a virtual library. Implemented on the Ethereum blockchain using Solidity, Truffle, Zeppelin and others. 5 | 6 | ## Usage 7 | 8 | TBD 9 | 10 | ## Building and the frontend 11 | 12 | 1. First run `truffle compile`, then run `truffle migrate` to deploy the contracts onto your network of choice (default "development"). 13 | 1. Then run `npm start` to build the app and serve it on http://localhost:8080 14 | 15 | 16 | ## Debugging 17 | 18 | ``` 19 | $ truffle console 20 | truffle(development)> compile 21 | truffle(development)> lms.new('Owner name') 22 | ``` 23 | If getOwner is a constant function, you will get the output immediately on the console 24 | ``` 25 | truffle(development)> lms.at("contract address").getOwner() 26 | ``` 27 | if getOwner is not a constant function, use events (say Owner is an event). 28 | ``` 29 | truffle(development)> lms.at('contract address').getOwner() 30 | truffle(development)> lms.at('contract address').Owner(function (e, result) { if (!e) {console.log(result)}}) 31 | ``` 32 | Note: Don't forget to add any new contracts to the migration file. 33 | -------------------------------------------------------------------------------- /Todo.txt: -------------------------------------------------------------------------------- 1 | solidity --- memory vs storage, type casting, test cases in solidity, zeppelin 2 | 3 | 4 | SPEED UP 5 | ******** 6 | 7 | constructor - ✓ 8 | 9 | getOwner - function done, test pending 10 | 11 | addMember - ✓ (test case pending for duplicate and owner) and Reactivate if member is deactivated 12 | 13 | deactivateMember - ✓ (test pending) 14 | 15 | getOwnerDetails - ✓ (test case pending) 16 | 17 | getMemberDetails - ✓ (test case pending) 18 | 19 | addBook - ✓ (test case completed) 20 | 21 | addMemberWithBooks - ✓ (test case pending) 22 | 23 | getMemberList - Function not working (test case pending) 24 | 25 | getBookList - Function not working (test case pending) 26 | 27 | getBook - Pending 28 | 29 | getBooks - pending 30 | 31 | getMyBooks - Pending 32 | 33 | borrowBook - ✓ (test case pending) 34 | 35 | returnBook - ✓ (test case pending) 36 | 37 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 38 | issues faced - 39 | 40 | memory to storage 41 | truffle installation 42 | 43 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 44 | Go through 45 | 46 | Travis 47 | Ethereum Alarm Clock 48 | 49 | Timeout in event test case 50 | Optimization in AddBooks method 51 | 52 | -------------------------------------------------------------------------------- /app/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | const actionType = { 2 | GET_ACCOUNTS_LOADING: 'GET_ACCOUNTS_LOADING', 3 | GET_ACCOUNTS_ERROR: 'GET_ACCOUNTS_ERROR', 4 | GET_ACCOUNTS_SUCCESS: 'GET_ACCOUNTS_SUCCESS', 5 | GET_OWNERDETAILS_LOADING: 'GET_OWNERDETAILS_LOADING', 6 | GET_OWNERDETAILS_ERROR: 'GET_OWNERDETAILS_ERROR', 7 | GET_OWNERDETAILS_SUCCESS: 'GET_OWNERDETAILS_SUCCESS', 8 | GET_ALL_BOOKS_LOADING: 'GET_ALL_BOOKS_LOADING', 9 | GET_ALL_BOOKS_ERROR: 'GET_ALL_BOOKS_ERROR', 10 | GET_ALL_BOOKS_SUCCESS: 'GET_ALL_BOOKS_SUCCESS', 11 | GET_MY_BOOKS_LOADING: 'GET_MY_BOOKS_LOADING', 12 | GET_MY_BOOKS_ERROR: 'GET_MY_BOOKS_ERROR', 13 | GET_MY_BOOKS_SUCCESS: 'GET_MY_BOOKS_SUCCESS', 14 | GET_ADD_BOOKS_LOADING: 'GET_ADD_BOOKS_LOADING', 15 | GET_ADD_BOOKS_ERROR: 'GET_ADD_BOOKS_ERROR', 16 | GET_ADD_BOOKS_SUCCESS: 'GET_ADD_BOOKS_SUCCESS', 17 | GET_RETURN_BOOKS_LOADING: 'GET_RETURN_BOOKS_LOADING', 18 | GET_RETURN_BOOKS_ERROR: 'GET_RETURN_BOOKS_ERROR', 19 | GET_RETURN_BOOKS_SUCCESS: 'GET_RETURN_BOOKS_SUCCESS', 20 | GET_BORROW_BOOKS_LOADING: 'GET_BORROW_BOOKS_LOADING', 21 | GET_BORROW_BOOKS_ERROR: 'GET_BORROW_BOOKS_ERROR', 22 | GET_BORROW_BOOKS_SUCCESS: 'GET_BORROW_BOOKS_SUCCESS', 23 | SEARCH_BOOK: 'SEARCH_BOOK', 24 | RATE_BOOK_LOADING: 'RATE_BOOK_LOADING', 25 | RATE_BOOK_ERROR: 'RATE_BOOK_ERROR', 26 | RATE_BOOK_SUCCESS: 'RATE_BOOK_SUCCESS', 27 | GET_MEMBER_DETAILS_EMAIL_LOADING: 'GET_MEMBER_DETAILS_EMAIL_LOADING', 28 | GET_MEMBER_DETAILS_EMAIL_ERROR: 'GET_MEMBER_DETAILS_EMAIL_ERROR', 29 | GET_MEMBER_DETAILS_EMAIL_SUCCESS: 'GET_MEMBER_DETAILS_EMAIL_SUCCESS', 30 | GET_MEMBER_DETAILS_LOADING: 'GET_MEMBER_DETAILS_LOADING', 31 | GET_MEMBER_DETAILS_ERROR: 'GET_MEMBER_DETAILS_ERROR', 32 | GET_MEMBER_DETAILS_SUCCESS: 'GET_MEMBER_DETAILS_SUCCESS', 33 | GET_RATE_BOOK_LOADING: 'GET_RATE_BOOK_LOADING', 34 | GET_RATE_BOOK_ERROR: 'GET_RATE_BOOK_ERROR', 35 | GET_RATE_BOOK_SUCCESS: 'GET_RATE_BOOK_SUCCESS', 36 | CREATE_ACCOUNT_LOADING: 'CREATE_ACCOUNT_LOADING', 37 | CREATE_ACCOUNT_SUCCESS: 'CREATE_ACCOUNT_SUCCESS', 38 | CREATE_ACCOUNT_ERROR: 'CREATE_ACCOUNT_ERROR', 39 | ADD_MEMBER_LOADING: 'ADD_MEMBER_LOADING', 40 | ADD_MEMBER_ERROR: 'ADD_MEMBER_ERROR', 41 | ADD_MEMBER_SUCCESS: 'ADD_MEMBER_SUCCESS', 42 | UPDATE_BOOK_LOADING: 'UPDATE_BOOK_LOADING', 43 | UPDATE_BOOK_ERROR: 'UPDATE_BOOK_ERROR', 44 | UPDATE_BOOK_SUCCESS: 'UPDATE_BOOK_SUCCESS', 45 | UNLOCK_ACCOUNT_LOADING: 'UNLOCK_ACCOUNT_LOADING', 46 | LOGOUT: 'LOGOUT', 47 | LOGIN_ERROR: 'LOGIN_ERROR', 48 | UNLOCK_ACCOUNT_ERROR: 'UNLOCK_ACCOUNT_ERROR', 49 | GET_USER_BALANCE_LOADING: 'GET_USER_BALANCE_LOADING', 50 | GET_USER_BALANCE_SUCCESS: 'GET_USER_BALANCE_SUCCESS', 51 | GET_USER_BALANCE_ERROR: 'GET_USER_BALANCE_ERROR', 52 | SHUFFLE_ALL_BOOKS: 'SHUFFLE_ALL_BOOKS', 53 | GET_ALL_MEMBERS_LOADING: 'GET_ALL_MEMBERS_LOADING', 54 | GET_ALL_MEMBERS_ERROR: 'GET_ALL_MEMBERS_ERROR', 55 | GET_ALL_MEMBERS_SUCCESS: 'GET_ALL_MEMBERS_SUCCESS', 56 | BORROW_EVENT_LOADING: 'BORROW_EVENT_LOADING', 57 | BORROW_EVENT_ERROR: 'BORROW_EVENT_ERROR', 58 | BORROW_EVENT_SUCCESS: 'BORROW_EVENT_SUCCESS', 59 | RETURN_EVENT_LOADING: 'RETURN_EVENT_LOADING', 60 | RETURN_EVENT_ERROR: 'RETURN_EVENT_ERROR', 61 | RETURN_EVENT_SUCCESS: 'RETURN_EVENT_SUCCESS' 62 | } 63 | 64 | export default actionType 65 | -------------------------------------------------------------------------------- /app/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import * as libraryActions from '../actions/libraryActions' 4 | import Dashboard from './Dashboard' 5 | import Header from './Header' 6 | 7 | export const mapStateToProps = (state, ownProps) => { 8 | return { 9 | session: state.session, 10 | accounts: state.accounts 11 | } 12 | } 13 | 14 | export class App extends React.Component { 15 | componentDidMount () { 16 | // disabling the Loader screen screen 17 | const loader = document.getElementById('loader') 18 | if (loader) { 19 | loader.style.display = 'none' 20 | } 21 | } 22 | componentWillReceiveProps (nextProps) { 23 | if(!nextProps.accounts && nextProps.session.authenticated && nextProps.session.user.account) { 24 | this.props.getBalance(nextProps.session.user) 25 | } 26 | } 27 | render () { 28 | return ( 29 |
30 |
this.props.getMemberDetailsByEmail(response) 33 | } 34 | loginFailure = { 35 | (response) => { console.log(response) } 36 | } 37 | session={ this.props.session } 38 | accounts={ this.props.accounts } 39 | logout = { 40 | () => this.props.logout() 41 | } /> 42 |
43 | { 44 | this.props.session.authenticated 45 | ? 46 | :
Logged out
47 | } 48 |
49 |
) 50 | } 51 | } 52 | 53 | export default connect(mapStateToProps, libraryActions)(App) 54 | -------------------------------------------------------------------------------- /app/components/Banner.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BannerImage from '../img/Triad-01.png' 3 | 4 | const Banner = () => ( 5 |
6 |
7 |
8 | 14 |
15 |
16 |
17 |
18 |

Yoga for the mind

19 |
20 |
21 |

Get Rewarded for Reading

22 |
23 |
24 |
25 | ) 26 | 27 | export default Banner 28 | -------------------------------------------------------------------------------- /app/components/Book.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Modal from 'react-modal' 3 | import RateBook from './RateBook' 4 | import BookAction from './BookAction' 5 | import BookInfo from './utils/BookInfo' 6 | 7 | const style = { 8 | row: { 9 | display: 'flex', 10 | flexWrap: 'wrap' 11 | } 12 | } 13 | 14 | const modalStyle = { 15 | content : { 16 | top : '50%', 17 | left : '50%', 18 | right : 'auto', 19 | bottom : 'auto', 20 | width : '30%', 21 | transform : 'translate(-50%, -50%)' 22 | } 23 | } 24 | 25 | export const getUserRating = (selectedBook,ownerDetails) => { 26 | if(typeof selectedBook !== "undefined" && typeof selectedBook.ratings !== "undefined" && typeof selectedBook.reviewers !== "undefined"){ 27 | return selectedBook.ratings[selectedBook.reviewers.indexOf(ownerDetails.account)] 28 | } 29 | return 0 30 | } 31 | const Book = ({ 32 | title, 33 | books, 34 | members, 35 | ownerDetails, 36 | selectedBook, 37 | btnTitle, 38 | loading, 39 | rateBook, 40 | openModal, 41 | closeModal, 42 | rateModalIsOpen, 43 | bookModalIsOpen, 44 | authenticated, 45 | getMemberDetailsByEmail 46 | }) => ( 47 |
48 |
{title}
49 |
50 | {books.map((book, i) => { 51 | return ( 52 | 61 | ) 62 | })} 63 |
64 | closeModal('rateBook')} 67 | role='dialog' 68 | style={modalStyle} 69 | shouldCloseOnOverlayClick={false} 70 | contentLabel='Rate a Book'> 71 | closeModal('rateBook') 73 | } 74 | rateBook = { 75 | (rating, comment) => rateBook(rating, comment) 76 | } 77 | loading = { 78 | loading.rateBookLoading 79 | } 80 | presetRate={getUserRating(selectedBook,ownerDetails)} 81 | selectedBook = {selectedBook} 82 | /> 83 | 84 | closeModal('bookModal')} 87 | role='dialog' 88 | style={modalStyle} 89 | shouldCloseOnOverlayClick={false} 90 | contentLabel='Book Action'> 91 | closeModal('bookModal')} 97 | /> 98 | 99 |
100 | ) 101 | 102 | export default Book 103 | -------------------------------------------------------------------------------- /app/components/BookAction.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import * as libraryActions from '../actions/libraryActions' 4 | 5 | export class BookAction extends React.Component { 6 | constructor (props) { 7 | super(props) 8 | this.handleSubmit = this.handleSubmit.bind(this) 9 | } 10 | handleSubmit (e) { 11 | e.preventDefault(); 12 | const actionType = this.checkActionType(); 13 | if(actionType === 1) { this.props.borrowBook(this.props.book, this.props.ownerDetails) } 14 | else if(actionType === 2) { this.props.returnBook(this.props.book) } 15 | this.props.closeModal() 16 | } 17 | checkActionType () { 18 | if (this.props.book.state === '0') { 19 | return 1 // User is borrowing book 20 | } else if(this.props.book.owner === this.props.ownerDetails.account) { 21 | return 2 // Owner is returning book 22 | } else { 23 | return 3 // Borrower is returning book 24 | } 25 | } 26 | render () { 27 | const actionType = this.checkActionType(); 28 | const info = actionType === 1 29 | ? 'Please contact the book owner for pick up.' 30 | : actionType === 2 31 | ? 'Click "Return" to confirm that the book has been returned to you.' 32 | : 'Please return book to the owner.' 33 | const buttonText = actionType === 1 34 | ? 'Borrow' 35 | : actionType === 2 36 | ? 'Return' 37 | : 'Close' 38 | const { book, members } = this.props 39 | const borrowed_date = book.state === '0' ? Date.now() : (+book.dateIssued)*1000 40 | const due_date = new Date(borrowed_date + 10*24*60*60*1000) // increment date by 10 days 41 | return ( 42 |
43 |
44 | 45 |

46 | {actionType === 1 ? "Borrow Book" : "Return Book"} - {book.title} 47 |

48 | this.props.closeModal()}> 49 |
50 |
51 |

{info}

52 | 53 | { 54 | actionType === 2 55 | ? 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | { 69 | members[book.borrower].name !== '' && 70 | 71 | 72 | 73 | 74 | } 75 | { 76 | members[book.borrower].email !== '' && 77 | 78 | 79 | 80 | 81 | } 82 | { 83 | 84 | 85 | 86 | 87 | } 88 | 89 | : 90 | { 91 | members[book.owner].name !== '' && 92 | 93 | 94 | 95 | 96 | } 97 | { 98 | members[book.owner].email !== '' && 99 | 100 | 101 | 102 | 103 | } 104 | { 105 | 106 | 107 | 108 | 109 | } 110 | 111 | } 112 |
Book Title{book.title}
Author{book.author}
Publisher{book.publisher}
Borrowed By{members[book.borrower].name}
Email{members[book.borrower].email}
Due Date{due_date.toDateString()}
Name{members[book.owner].name}
Email{members[book.owner].email}
Due Date{due_date.toDateString()}
113 |
114 |
115 |
116 | 119 |
120 | { 121 | (actionType === 1 || actionType === 2) && 122 |
123 | 126 |
127 | } 128 |
129 |
130 |
131 | ) 132 | } 133 | } 134 | 135 | export default connect(null, libraryActions)(BookAction) 136 | -------------------------------------------------------------------------------- /app/components/BookDetailsPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import Modal from 'react-modal' 4 | import * as libraryActions from '../actions/libraryActions' 5 | import BookInfo from './utils/BookInfo' 6 | import RateBook from './RateBook' 7 | import BookAction from './BookAction' 8 | import CommentList from './utils/CommentList' 9 | import BooksForm from './BooksForm' 10 | import BookHistory from './BookHistory' 11 | 12 | const modalStyle = { 13 | content : { 14 | top : '50%', 15 | left : '50%', 16 | right : 'auto', 17 | bottom : 'auto', 18 | width : '30%', 19 | transform : 'translate(-50%, -50%)' 20 | } 21 | } 22 | 23 | export const mapStateToProps = (state, ownProps) => { 24 | return { 25 | accounts: state.accounts, 26 | books: state.books, 27 | session: state.session, 28 | isExistingMember: state.isExistingMember, 29 | loading: state.loading, 30 | book_history: state.book_history 31 | } 32 | } 33 | 34 | export class BookDetailsPage extends React.Component { 35 | constructor (props) { 36 | super(props) 37 | this.state = { 38 | rateModalIsOpen: false, 39 | bookModalIsOpen: false, 40 | editBookModalIsOpen: false, 41 | book: {} 42 | } 43 | } 44 | componentWillReceiveProps (nextProps) { 45 | if(nextProps.session.authenticated && nextProps.session.user.account && nextProps.isExistingMember.callbackFn) { 46 | nextProps.isExistingMember.callbackFn.apply(this, nextProps.isExistingMember.argsArr) 47 | } 48 | } 49 | toggleModal (modal, book) { 50 | switch (modal) { 51 | case 'rateBook' : { 52 | this.setState({ rateModalIsOpen: !this.state.rateModalIsOpen, book }) 53 | break; 54 | } 55 | case 'bookModal' : { 56 | this.setState({ bookModalIsOpen: !this.state.bookModalIsOpen, book }) 57 | break; 58 | } 59 | case 'editBook' : { 60 | this.setState({ editBookModalIsOpen: !this.state.editBookModalIsOpen, book }) 61 | break; 62 | } 63 | } 64 | } 65 | getUserRating (selectedBook,ownerDetails) { 66 | if(typeof selectedBook !== "undefined" && typeof selectedBook.ratings !== "undefined" && typeof selectedBook.reviewers !== "undefined"){ 67 | return selectedBook.ratings[selectedBook.reviewers.indexOf(ownerDetails.account)] 68 | } 69 | return 0 70 | } 71 | render () { 72 | const book = this.props.books.allBooks.filter((book) => book.id === this.props.match.params.id) 73 | const members = (this.props.accounts && this.props.accounts.members) ? this.props.accounts.members : '' 74 | return ( 75 |
76 |
77 | { 78 | book.length !== 0 && 79 |
80 | this.toggleModal(modalName, book)} 86 | getMemberDetailsByEmail={ 87 | (response, callbackFn, argsArray) => this.props.getMemberDetailsByEmail(response, callbackFn, argsArray) 88 | } 89 | ownerDetails={this.props.session.user} /> 90 |
91 | } 92 | { 93 | book.length !==0 && book[0].comments !== undefined && book[0].comments.length !==0 && 94 | 96 | } 97 |
98 | { 99 | book.length !== 0 && 100 | 104 | } 105 | this.toggleModal('rateBook')} 108 | role='dialog' 109 | style={modalStyle} 110 | shouldCloseOnOverlayClick={false} 111 | contentLabel='Rate a Book'> 112 | this.toggleModal('rateBook') 114 | } 115 | rateBook = { 116 | (rating, comment) => this.props.rateBook(rating, comment, this.state.book, this.props.session.user) 117 | } 118 | loading = { 119 | this.props.loading.rateBookLoading 120 | } 121 | presetRate={ this.getUserRating(this.state.book, this.props.session.user) } 122 | selectedBook = { this.state.book } 123 | /> 124 | 125 | this.toggleModal('bookModal')} 128 | role='dialog' 129 | style={modalStyle} 130 | shouldCloseOnOverlayClick={false} 131 | contentLabel='Book Action'> 132 | this.toggleModal('bookModal')} 139 | /> 140 | 141 | this.toggleModal('editBook')} 144 | shouldCloseOnOverlayClick={false} 145 | role='dialog' 146 | style={modalStyle} 147 | contentLabel='Edit book'> 148 | this.toggleModal('editBook')} book={this.state.book} /> 149 | 150 |
151 | ) 152 | } 153 | } 154 | 155 | export default connect(mapStateToProps, libraryActions)(BookDetailsPage) 156 | -------------------------------------------------------------------------------- /app/components/BookHistory.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import * as libraryActions from '../actions/libraryActions' 4 | import BookRecord from './utils/BookRecord' 5 | 6 | export const mapStateToProps = (state=[]) => { 7 | return { 8 | book_history: state.book_history 9 | } 10 | } 11 | 12 | export class BookHistory extends React.Component { 13 | componentDidMount () { 14 | if(!this.props.book_history || !this.props.book_history.borrow_history[+this.props.book.id]) { 15 | this.props.borrowEvent(this.props.book.id) 16 | } 17 | if(!this.props.book_history || !this.props.book_history.return_history[+this.props.book.id]) { 18 | this.props.returnEvent(this.props.book.id) 19 | } 20 | } 21 | render () { 22 | const borrow_history = this.props.book_history && this.props.book_history.borrow_history[+this.props.book.id] 23 | const return_history = this.props.book_history && this.props.book_history.return_history[+this.props.book.id] 24 | return ( 25 |
26 | { 27 | borrow_history !== undefined && 28 | 32 | } 33 | { 34 | return_history !== undefined && 35 | 39 | } 40 |
41 | ) 42 | } 43 | } 44 | 45 | export default connect(mapStateToProps, libraryActions)(BookHistory) 46 | -------------------------------------------------------------------------------- /app/components/BooksForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import * as libraryActions from '../actions/libraryActions' 4 | 5 | export const mapStateToProps = (state) => { 6 | return { 7 | ownerDetails: state.session.user 8 | } 9 | } 10 | 11 | export class BooksForm extends React.Component { 12 | constructor (props) { 13 | super(props) 14 | this.state = { 15 | title: this.props.book ? this.props.book.title : '', 16 | author: this.props.book ? this.props.book.author : '', 17 | publisher: this.props.book ? this.props.book.publisher : '', 18 | description: this.props.book ? this.props.book.description : '', 19 | imageUrl: this.props.book ? this.props.book.imageUrl : '', 20 | genre: this.props.book ? this.props.book.genre : '' 21 | } 22 | this.handleSubmit = this.handleSubmit.bind(this) 23 | this.handleChange = this.handleChange.bind(this) 24 | } 25 | isBookUpdated (book) { 26 | return (this.props.book.title !== book.title 27 | || this.props.book.author !== book.author 28 | || this.props.book.publisher !== book.publisher 29 | || this.props.book.description !== book.description 30 | || this.props.book.imageUrl !== book.imageUrl 31 | || this.props.book.genre !== book.genre) 32 | } 33 | handleSubmit (e) { 34 | e.preventDefault() 35 | const book = { 36 | title : this.state.title, 37 | author : this.state.author, 38 | publisher : this.state.publisher, 39 | description : this.state.description, 40 | imageUrl : this.state.imageUrl, 41 | genre : this.state.genre, 42 | owner: this.props.ownerDetails 43 | } 44 | if(this.props.book) { 45 | if(this.isBookUpdated(book)) { 46 | this.props.updateBook(this.props.book.id, book) 47 | } 48 | } else { 49 | this.props.addBook(book) 50 | } 51 | this.props.closeModal() 52 | } 53 | handleChange (e) { 54 | this.setState({ 55 | [e.target.id] : e.target.value 56 | }) 57 | } 58 | render () { 59 | return ( 60 |
61 |
62 | 63 | { 64 | this.props.book 65 | ?

{'Edit - '+ this.props.book.title }

66 | :

Add a book

67 | } 68 | this.props.closeModal()}> 69 |
70 |
71 | 72 |
73 | < input type = 'text' 74 | className = 'form-control' 75 | id = 'title' 76 | placeholder = 'Title' 77 | defaultValue = {this.state.title} 78 | onChange= {this.handleChange} 79 | required / > 80 |
81 |
82 |
83 | 84 |
85 | < input type = 'text' 86 | className = 'form-control' 87 | id = 'author' 88 | placeholder = 'Author' 89 | defaultValue = {this.state.author} 90 | onChange= {this.handleChange} 91 | required / > 92 |
93 |
94 |
95 | 96 |
97 | < input type = 'text' 98 | className = 'form-control' 99 | id = 'publisher' 100 | placeholder = 'Publisher' 101 | defaultValue = {this.state.publisher} 102 | onChange= {this.handleChange} 103 | required / > 104 |
105 |
106 |
107 | 108 |
109 | < input type = 'url' 110 | className = 'form-control' 111 | id = 'imageUrl' 112 | placeholder = 'Image URL' 113 | defaultValue = {this.state.imageUrl} 114 | onChange= {this.handleChange} 115 | required / > 116 |
117 |
118 |
119 | 120 |
121 |