├── src ├── assets │ └── css │ │ ├── index.css │ │ └── App.css ├── index.js ├── ProductListings │ ├── ProductListing.js │ ├── SearchField.js │ ├── SortSelection.js │ ├── Categories.js │ ├── ProductListingContainer.js │ ├── ProductListingsCollection.js │ └── ProductListingDetails.js ├── reducers │ └── index.js ├── adapter.js ├── actions │ └── index.js ├── d ├── NavBar │ └── NavBar.js ├── Login │ └── LoginForm.js ├── PurchasedProducts │ └── PurchasedProducts.js ├── App.js ├── Matches │ └── MatchingProductListings.js ├── PrivateListings │ └── PrivateProductListings.js ├── Register │ └── UserRegisterForm.js └── NewListing │ └── ProductListingForm.js ├── public ├── assets │ └── images │ │ ├── banner.png │ │ ├── footer.png │ │ └── sold-out-png-19.png ├── manifest.json └── index.html ├── .gitignore ├── package.json └── README.md /src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /public/assets/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamedali95/BarterOnly/HEAD/public/assets/images/banner.png -------------------------------------------------------------------------------- /public/assets/images/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamedali95/BarterOnly/HEAD/public/assets/images/footer.png -------------------------------------------------------------------------------- /public/assets/images/sold-out-png-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ahamedali95/BarterOnly/HEAD/public/assets/images/sold-out-png-19.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | keys.js 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./assets/css/index.css"; 4 | import "semantic-ui-css/semantic.min.css"; 5 | import App from "./App"; 6 | import {createStore} from "redux"; 7 | import {Provider} from "react-redux"; 8 | import reducer from "./reducers/index.js"; 9 | import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom'; 10 | 11 | const store = createStore(reducer); 12 | 13 | ReactDOM.render( 14 | 15 | 16 | } /> 17 | 18 | , 19 | document.getElementById("root") 20 | ); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barter", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "chart.js": "^2.7.2", 7 | "dotenv": "^6.0.0", 8 | "moment": "^2.22.2", 9 | "react": "^16.4.1", 10 | "react-bootstrap": "^0.32.1", 11 | "react-chart.js": "^1.0.0", 12 | "react-chartjs": "^1.2.0", 13 | "react-chartkick": "^0.2.1", 14 | "react-dom": "^16.4.1", 15 | "react-dropzone": "^4.2.13", 16 | "react-moment": "^0.7.9", 17 | "react-redux": "^5.0.7", 18 | "react-router-dom": "^4.3.1", 19 | "react-scripts": "1.1.4", 20 | "redux": "^4.0.0", 21 | "semantic-ui-css": "^2.3.2", 22 | "semantic-ui-react": "^0.81.3", 23 | "superagent": "^3.8.3" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "test": "react-scripts test --env=jsdom", 29 | "eject": "react-scripts eject" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | React App 13 | 14 | 15 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ProductListings/ProductListing.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { selectProductListing} from "../actions/index.js"; 3 | import { Card, Image, Button } from "semantic-ui-react"; 4 | import { connect } from "react-redux"; 5 | 6 | const ProductListing = (props) => { 7 | console.log("INSIDE PRODUCTLISTING", props); 8 | return ( 9 | 10 | 11 | 12 | {props.productListing.name} 13 | {`$${props.productListing.value}`} 14 | {props.productListing.description} 15 | 16 | 17 |
18 | 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | const mapDispatchToProps = (dispatch) => { 28 | return { 29 | selectProductListing: (productListing) => { 30 | dispatch(selectProductListing(productListing)); 31 | } 32 | }; 33 | } 34 | 35 | export default connect(null, mapDispatchToProps)(ProductListing); 36 | -------------------------------------------------------------------------------- /src/ProductListings/SearchField.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { updateSearchTerm } from "../actions/index.js"; 3 | import { connect } from "react-redux"; 4 | import { Search } from "semantic-ui-react"; 5 | 6 | const SearchField = (props) => { 7 | console.log("INSIDE SEARCH FIELD", props); 8 | //Conditional rendering to decide whether to show the select dropdown menu 9 | //or not. 10 | //Select menu will render only if there is a currentProductListing - meaning that 11 | //a product is selected by the user to view its details 12 | 13 | //onChange synthetic event is called onSearchChange in semantic. 14 | return ( 15 |
16 | { 20 | props.updateSearchTerm(event.target.value) 21 | }} 22 | /> 23 |
24 | ); 25 | } 26 | 27 | function mapStateToProps(state) { 28 | return { 29 | searchTerm: state.searchTerm 30 | }; 31 | } 32 | 33 | function mapDispatchToProps(dispatch) { 34 | return { 35 | updateSearchTerm: (searchTerm) => { 36 | dispatch(updateSearchTerm(searchTerm)); 37 | } 38 | }; 39 | } 40 | 41 | export default connect(mapStateToProps, mapDispatchToProps)(SearchField); 42 | -------------------------------------------------------------------------------- /src/ProductListings/SortSelection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { updateSortByOption } from "../actions/index.js"; 3 | import { connect } from "react-redux"; 4 | import { Select } from "semantic-ui-react"; 5 | 6 | const SortSelection = (props) => { 7 | //Conditional rendering to decide whether to show the select dropdown menu 8 | //or not. 9 | //Select menu will render only if there is a currentProductListing - meaning that 10 | //a product is selected by the user to view its details 11 | return ( 12 |
13 | 22 |
23 | ); 24 | } 25 | 26 | function mapStateToProps(state) { 27 | return { 28 | sortByOption: state.sortByOption 29 | }; 30 | } 31 | 32 | function mapDispatchToProps(dispatch) { 33 | return { 34 | updateSortByOption: (option) => { 35 | dispatch(updateSortByOption(option)); 36 | } 37 | }; 38 | } 39 | 40 | export default connect(mapStateToProps, mapDispatchToProps)(SortSelection); 41 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | categories: [], 3 | productListings: [], 4 | users: [], 5 | purchases: [], 6 | searchTerm: "", 7 | sortByOption: "Relevance", 8 | categorySelected: "All", 9 | currentProductListing: null, 10 | }; 11 | 12 | function reducer(state = initialState, action) { 13 | console.log(action.type) 14 | switch(action.type) { 15 | case "SET_CATEGORY_AND_RESET_SEARCH_TERM_AND_SORT_OPTION": 16 | return {...state, searchTerm: "", sortByOption: "Relevance", categorySelected: action.payload, currentProductListing: null}; 17 | case "SET_PRODUCT_LISTINGS_AND_CATEGORIES_AND_USERS": 18 | return {...state, productListings: action.payload.productListings, categories: action.payload.categories, users: action.payload.users}; 19 | case "UPDATE_SEARCH_TERM": 20 | return {...state, searchTerm: action.payload}; 21 | case "UPDATE_SORT_BY_OPTION": 22 | return {...state, sortByOption: action.payload}; 23 | case "UPDATE_PRODUCT_LISTINGS": 24 | return {...state, productListings: action.payload}; 25 | case "SELECT_PRODUCT_LISTING": 26 | return {...state, currentProductListing: action.payload}; 27 | case "REMOVE_CURRENT_PRODUCT_LISTING": 28 | return {...state, currentProductListing: action.payload}; 29 | case "SET_PURCHASES": 30 | return {...state, purchases: action.payload} 31 | default: 32 | return state; 33 | } 34 | } 35 | 36 | export default reducer; 37 | -------------------------------------------------------------------------------- /src/adapter.js: -------------------------------------------------------------------------------- 1 | const URI = "http://localhost:3001/api/v1/"; 2 | 3 | const adapter = { 4 | getToken: function() { 5 | return localStorage.getItem("token"); 6 | }, 7 | setToken: function(token) { 8 | localStorage.setItem("token", token); 9 | }, 10 | getUserId: function() { 11 | return localStorage.getItem("userId"); 12 | }, 13 | setUserId: function(userId) { 14 | localStorage.setItem("userId", userId); 15 | }, 16 | clearLocalStorage: function() { 17 | localStorage.clear(); 18 | }, 19 | get: function(items) { 20 | const config = { 21 | headers: { 22 | "Content-Type": "application/json", 23 | "Authorization": this.getToken() 24 | } 25 | }; 26 | 27 | return fetch(URI + items, config); 28 | }, 29 | post: function(items, body) { 30 | const config = { 31 | method: "POST", 32 | headers: { 33 | "Content-Type": "application/json" 34 | }, 35 | body: JSON.stringify(body) 36 | }; 37 | 38 | return fetch(URI + items, config); 39 | }, 40 | patch: function(item, body) { 41 | const config = { 42 | method: "PATCH", 43 | headers: { 44 | "Content-Type": "application/json" 45 | }, 46 | body: JSON.stringify(body) 47 | }; 48 | 49 | return fetch(URI + item, config); 50 | }, 51 | delete: function(item) { 52 | const config = { 53 | method: "DELETE", 54 | }; 55 | 56 | return fetch(URI + item, config); 57 | } 58 | } 59 | 60 | export default adapter; 61 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | const setProductListingsAndCategoriesAndUsers = (productListings, categories, users) => { 2 | return { 3 | type: "SET_PRODUCT_LISTINGS_AND_CATEGORIES_AND_USERS", 4 | payload: { 5 | productListings: productListings, 6 | categories: categories, 7 | users: users 8 | }}; 9 | } 10 | 11 | const setCategoryAndResetSearchTermAndSortOption = (categoryId) => { 12 | return { 13 | type: "SET_CATEGORY_AND_RESET_SEARCH_TERM_AND_SORT_OPTION", 14 | payload: categoryId 15 | }; 16 | } 17 | 18 | const updateSearchTerm = (searchTerm) => { 19 | return { 20 | type: "UPDATE_SEARCH_TERM", 21 | payload: searchTerm 22 | }; 23 | } 24 | 25 | const updateSortByOption = (option) => { 26 | return { 27 | type: "UPDATE_SORT_BY_OPTION", 28 | payload: option 29 | }; 30 | } 31 | 32 | const updateProductListings = (newProductListings) => { 33 | return { 34 | type: "UPDATE_PRODUCT_LISTINGS", 35 | payload: newProductListings 36 | }; 37 | } 38 | 39 | const selectProductListing = (productListing) => { 40 | return { 41 | type: "SELECT_PRODUCT_LISTING", 42 | payload: productListing 43 | }; 44 | } 45 | 46 | const removeCurrentProductListing = () => { 47 | return { 48 | type: "REMOVE_CURRENT_PRODUCT_LISTING", 49 | payload: null 50 | } 51 | } 52 | 53 | const setPurchases = (purchases) => { 54 | return { 55 | type: "SET_PURCHASES", 56 | payload: purchases 57 | } 58 | } 59 | 60 | module.exports = { 61 | setProductListingsAndCategoriesAndUsers, 62 | setCategoryAndResetSearchTermAndSortOption, 63 | updateSearchTerm, 64 | updateSortByOption, 65 | updateProductListings, 66 | selectProductListing, 67 | removeCurrentProductListing, 68 | setPurchases 69 | }; 70 | -------------------------------------------------------------------------------- /src/ProductListings/Categories.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { setCategoryAndResetSearchTermAndSortOption } from "../actions/index.js"; 3 | import { connect } from "react-redux"; 4 | import { List } from "semantic-ui-react"; 5 | 6 | const Categories = (props) => { 7 | console.log("inside categories", props) 8 | return ( 9 | 10 | props.setCategoryAndResetSearchTermAndSortOption("All")}>All 11 | { 12 | props.categories.map((categoryObj) => { 13 | return ( 14 | { 19 | //Convert the name of the category to id so that when rerender 20 | //happens we can find the product listings based on this id 21 | //REMEMBER - product listing belongs to a category so it must have 22 | //category_id 23 | const categoryId = props.categories.find((categoryObj) => { 24 | return categoryObj.name === event.target.innerText; 25 | }).id 26 | 27 | props.setCategoryAndResetSearchTermAndSortOption(categoryId); 28 | }} 29 | > 30 | {categoryObj.name} 31 | 32 | ); 33 | }) 34 | } 35 | 36 | ); 37 | } 38 | 39 | function mapStateToProps(state) { 40 | return { 41 | categories: state.categories 42 | } 43 | } 44 | 45 | function mapDispatchToProps(dispatch) { 46 | return { 47 | setCategoryAndResetSearchTermAndSortOption: (categoryId) => { 48 | dispatch(setCategoryAndResetSearchTermAndSortOption(categoryId)); 49 | } 50 | }; 51 | } 52 | 53 | export default connect(mapStateToProps, mapDispatchToProps)(Categories); 54 | -------------------------------------------------------------------------------- /src/d: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | 4 | const NavBar = (props) => { 5 | return ( 6 |
7 | { 8 | !localStorage.getItem("token") ? 9 | 10 | 21 | Register 22 | 23 | 24 | 35 | Login 36 | 37 | 38 | : 39 | 40 | 41 | 42 | 53 | Create a Product Listing 54 | 55 | 56 | 67 | My Listings 68 | 69 | 70 | 81 | Matching Listings 82 | 83 | 84 | } 85 | 86 | 97 | Product Listings 98 | 99 |
100 | ); 101 | }; 102 | 103 | export default NavBar; 104 | -------------------------------------------------------------------------------- /src/NavBar/NavBar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { NavLink } from "react-router-dom"; 3 | import { Menu } from "semantic-ui-react" 4 | 5 | class NavBar extends Component { 6 | constructor() { 7 | super(); 8 | 9 | this.state = { 10 | menuItem: null 11 | }; 12 | } 13 | 14 | handleClick = (event, { name }) => { 15 | this.setState({ 16 | menuItem: name 17 | }); 18 | } 19 | 20 | render() { 21 | return ( 22 | 23 | 30 | Product Listings 31 | 32 | { 33 | !localStorage.getItem("token") ? 34 | 35 | 42 | Register 43 | 44 | 45 | 52 | Login 53 | 54 | 55 | : 56 | 57 | 64 | Create a Product Listing 65 | 66 | 67 | 74 | Private Listings 75 | 76 | 77 | 84 | Matching Listings 85 | 86 | 87 | 94 | My Purchases 95 | 96 | 97 | 98 | 99 | 100 | } 101 | 102 | ); 103 | } 104 | } 105 | 106 | 107 | export default NavBar; 108 | -------------------------------------------------------------------------------- /src/Login/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import adapter from "../adapter.js"; 3 | import { Form, Input, Button } from "semantic-ui-react"; 4 | import { Message } from "semantic-ui-react"; 5 | 6 | class LoginForm extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.state = { 11 | username: "", 12 | password: "", 13 | isError: false, 14 | errorMessages: [] 15 | } 16 | } 17 | 18 | handleSubmit = (event) => { 19 | event.preventDefault(); 20 | 21 | const bodyForLogin = { 22 | username: this.state.username, 23 | password: this.state.password 24 | }; 25 | 26 | adapter.post("sessions", bodyForLogin) 27 | .then(response => response.json()) 28 | .then(data => { 29 | 30 | //Here we are verifying the json that is returned back from the server. 31 | //If there is no token or userId, then we know that credentials did not 32 | //match so will issue an error. Otherwise, set local storage and push 33 | //history 34 | if(!!data.errors) { 35 | this.setState({ 36 | isError: true, 37 | errorMessages: [...[], data.errors] 38 | }); 39 | } else { 40 | adapter.setToken(data.token); 41 | adapter.setUserId(data.userId); 42 | this.props.history.push("/product-listings"); 43 | window.location.reload(); 44 | } 45 | }); 46 | } 47 | 48 | handleChange = (event) => { 49 | this.setState({ 50 | [event.target.name]: event.target.value 51 | }, () => console.log(this.state)); 52 | } 53 | 54 | loadErrors = () => { 55 | return ( 56 | 62 | ); 63 | } 64 | 65 | loadForm = () => { 66 | return ( 67 |
68 |

Login

69 |
70 | 80 | 91 | 92 | 93 |
94 | ); 95 | } 96 | 97 | render() { 98 | return ( 99 |
100 | { 101 | this.state.isError ? 102 | 103 | {this.loadErrors()} 104 | {this.loadForm()} 105 | 106 | : 107 | 108 | {this.loadForm()} 109 | 110 | } 111 |
112 | ); 113 | } 114 | } 115 | 116 | export default LoginForm; 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BarterOnly 2 | 3 | BarterOnly is *single-page*, *full-stack* web application built using **ReactJS** and **Ruby on Rails**. It is an ecommerce platform where customers can list items they no longer need in exchange for cash or an exchange item. Customers can search the marketplace with hundreds of items, sort products based on certain attributes(date, price, rating) and categorize items. After acquiring an account, customers can either make a purchase for the full value of product or acquire item by making an exchange. A key feature of this platform is that it automatically matches a customer with other customers who are looking for items that they have. In combination with ReactJS, BarterOnly uses **Redux** - a state management React library to hold the state of the front-end application in a single store so that any component can have access or modify the global state, effectively eliminates passing of props from parent to child. Additionally, BarterOnly utilizes **dynamic routing using React Routers**, **RESTful JSON API using Rails** to give a feel of a dynamic web application. Moreover, this application implements the core concept of relational database as a foundation to relate data to one another so that information can be efficiently retrieved. The relationships are formed using ActiveRecord associations. 4 | 5 | Following is the Entity Relationship Diagram that describes the entities/models and associations between these entities: 6 | 7 | ![imageedit_8_6919085673](https://user-images.githubusercontent.com/24445922/42916049-3a2a0bc2-8ad1-11e8-96ae-2e9f9190f662.png) 8 | 9 | Following is the tree-like React Component Hierarchy that describes the component structure of the application's front-end: 10 | 11 |
12 |  ┬  
13 |  ├ App
14 |      ┬  
15 |      ├  NavBar
16 |      ├  LoginForm
17 |      ├  UserRegisterForm
18 |      ├  ProductListingContainer
19 |          ┬  
20 |          ├  Categories
21 |          ├  SearchField
22 |          ├  SortSelection
23 |          └  ProductListingsCollection
24 |      ├  PrivateProductListings
25 |      ├  MatchingProductListings
26 |      └  PurchasedProducts
27 | 
28 | 29 | ### Notable Tools: 30 | - **Semantic UI React** - a React framework to add responsive web design to the front-end 31 | - **JSON Web Token** - a token-based system used for authenticating users and authorizing certain API routes 32 | - **React Dropzone** - a React library used for managing client-side file upload 33 | - **Rack Cors** - a Rails gem that allows support for Cross-Origin Resource Sharing(CORS) to allow the resources of a Rails web server to be accessed by a web page from a different domain 34 | - **Serializer** - a Rails gem that allows to build JSON APIs through serializer objects. This provides a dedicated place to fully customize the JSON output 35 | - **Bcrypt** - a Ruby gem that allows to store sensitive information such as passwords in the back-end after hashing 36 | 37 | **Link to back-end application:** https://github.com/ahamedali95/BarterOnly-back-end 38 | 39 | ### Demo: 40 | 41 | [![watch this video](https://img.youtube.com/vi/eLFKI7i8_Xw/0.jpg)](https://youtu.be/eLFKI7i8_Xw) 42 | 43 | ### Instructions 44 | To start, run ```npm install``` && ```npm start``` to get the app up and running. This will start the app at PORT 3000. To designate a different port number for the app, modify the scripts in package.json from ```"start": "react-scripts start"``` to ```"start": "PORT={PORT NO.} react-scripts start"``` 45 | -------------------------------------------------------------------------------- /src/ProductListings/ProductListingContainer.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Categories from "./Categories.js"; 3 | import ProductListingsCollection from "./ProductListingsCollection.js"; 4 | import SearchField from "./SearchField.js"; 5 | import SortSelection from "./SortSelection.js"; 6 | import adapter from "../adapter.js"; 7 | import { connect } from "react-redux"; 8 | import { removeCurrentProductListing, setProductListingsAndCategoriesAndUsers, updateProductListings } from "../actions/index.js"; 9 | 10 | class ProductListingContainer extends Component { 11 | componentDidMount() { 12 | //This is important because if the comes back to this page from viewing 13 | //the product details, we would like to reset the currently seelcted 14 | //product listing to null so that the details page won't render; 15 | this.props.removeCurrentProductListing(); 16 | Promise.all([ 17 | adapter.get("categories"), 18 | adapter.get("product_listings"), 19 | adapter.get("users") 20 | ]) 21 | .then(([response1, response2, response3]) => Promise.all([response1.json(), response2.json(), response3.json()])) 22 | .then(([categories, productListings, users]) => { 23 | 24 | //Filter the product listings so that we can products which are not sold. 25 | // productListings = productListings.filter((productListingObj) => { 26 | // return !productListingObj.isSold 27 | // }); 28 | //We are doing this in ProductListingsCollection.js because if I do this here, 29 | //then myListings will show only products which are sold, not ALL products. 30 | this.props.setProductListingsAndCategoriesAndUsers(productListings, categories, users); 31 | }); 32 | } 33 | 34 | //This is important because once we mount we are setting the product listings to 35 | //an empty array so that our private listings won't render all product listings 36 | //and then go on to fetch private listings in the componentDidMount 37 | componentWillUnmount() { 38 | console.log("I am called") 39 | this.props.updateProductListings([]); 40 | } 41 | 42 | render() { 43 | return ( 44 |
45 | 46 | { 47 | this.props.currentProductListing === null ? 48 |
49 |

Search Here For Whats Wanted:

50 | 51 | 52 |
53 | : 54 | null 55 | } 56 | 57 |
58 | ); 59 | } 60 | } 61 | 62 | function mapStateToProps(state) { 63 | return { 64 | currentProductListing: state.currentProductListing 65 | }; 66 | } 67 | 68 | function mapDispatchToProps(dispatch) { 69 | return { 70 | setProductListingsAndCategoriesAndUsers: (product_listings, categories, users, purchases) => { 71 | dispatch(setProductListingsAndCategoriesAndUsers(product_listings, categories, users, purchases)); 72 | }, 73 | removeCurrentProductListing: () => { 74 | dispatch(removeCurrentProductListing()); 75 | }, 76 | updateProductListings: (productListings) => { 77 | dispatch(updateProductListings(productListings)); 78 | } 79 | // setCategoryAndResetSearchTermAndSortOption: (categoryId) => { 80 | // dispatch(setCategoryAndResetSearchTermAndSortOption(categoryId)); 81 | // }, 82 | // updateSearchTerm: (searchTerm) => { 83 | // dispatch(updateSearchTerm(searchTerm)); 84 | // }, 85 | // updateSortByOption: (option) => { 86 | // dispatch(updateSortByOption(option)); 87 | // } 88 | }; 89 | } 90 | 91 | export default connect(mapStateToProps, mapDispatchToProps)(ProductListingContainer); 92 | -------------------------------------------------------------------------------- /src/PurchasedProducts/PurchasedProducts.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Table } from "semantic-ui-react"; 3 | import { connect } from "react-redux"; 4 | import { BrowserRouter, Route } from "react-router-dom"; 5 | import Moment from "react-moment"; 6 | import { setPurchases, selectProductListing, removeCurrentProductListing } from "../actions/index.js"; 7 | import adapter from "../adapter.js"; 8 | import ProductListingDetails from "../ProductListings/ProductListingDetails.js"; 9 | 10 | class PurchasedProducts extends Component { 11 | constructor(props) { 12 | super(props); 13 | } 14 | 15 | componentDidMount() { 16 | this.props.removeCurrentProductListing(); 17 | 18 | adapter.get(`users/${adapter.getUserId()}/purchases`) 19 | .then(response => response.json()) 20 | .then((data) => this.props.setPurchases(data)); 21 | } 22 | 23 | productListingsRow = () => { 24 | const rows = this.props.purchases.map((purchaseObj) => { 25 | return ( 26 | 27 | 28 | 29 |

this.props.selectProductListing(purchaseObj)}>{purchaseObj.name}

30 |

{purchaseObj.description}

31 |
32 | ${purchaseObj.value} 33 | 34 | { 35 | purchaseObj.exchange_item === null ? 36 | "Cash" 37 | : 38 | purchaseObj.exchange_item 39 | } 40 | 41 | 42 | {purchaseObj.created_at} 43 | 44 |
45 | ); 46 | }); 47 | 48 | return rows; 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | { 55 | this.props.currentProductListing === null ? 56 | 57 | 58 | 59 | Content 60 | Value 61 | Paid With 62 | Date Purchased 63 | 64 | 65 | 66 | 67 | {this.productListingsRow()} 68 | 69 |
70 | : 71 | 72 | } 74 | /> 75 | 76 | } 77 |
78 | ); 79 | } 80 | } 81 | 82 | const mapStateToProps = (state) => { 83 | return { 84 | purchases: state.purchases, 85 | currentProductListing: state.currentProductListing 86 | }; 87 | } 88 | 89 | const mapDispatchToProps = (dispatch) => { 90 | return { 91 | setPurchases: (purchases) => { 92 | dispatch(setPurchases(purchases)); 93 | }, 94 | selectProductListing: (productListing) => { 95 | dispatch(selectProductListing(productListing)) 96 | }, 97 | removeCurrentProductListing: () => { 98 | dispatch(removeCurrentProductListing()) 99 | } 100 | }; 101 | } 102 | 103 | export default connect(mapStateToProps, mapDispatchToProps)(PurchasedProducts); 104 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./assets/css/App.css"; 3 | import adapter from "./adapter.js"; 4 | import { connect } from "react-redux"; 5 | import { removeCurrentProductListing } from "./actions/index.js"; 6 | import UserRegisterForm from "./Register/UserRegisterForm.js"; 7 | import LoginForm from "./Login/LoginForm.js"; 8 | import ProductListingContainer from "./ProductListings/ProductListingContainer.js"; 9 | import ProductListingForm from "./NewListing/ProductListingForm.js"; 10 | import PrivateProductListings from "./PrivateListings/PrivateProductListings.js"; 11 | import MatchingProductListings from "./Matches/MatchingProductListings.js"; 12 | import ModifyProductListing from "./ModifyListing/ModifyProductListing.js"; 13 | import PurchasedProducts from "./PurchasedProducts/PurchasedProducts.js"; 14 | import NavBar from "./NavBar/NavBar.js"; 15 | import { BrowserRouter as Router, Route, Redirect, Switch } from 'react-router-dom'; 16 | 17 | class App extends Component { 18 | 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | isNavigationChanged: false 24 | } 25 | } 26 | 27 | changeNavigation = () => { 28 | this.setState({ 29 | isNavigationChanged: !this.isNavigationChanged 30 | }) 31 | } 32 | 33 | handleClick = () => { 34 | //After clicking logout, if the user was in the product listing details page, 35 | //then we must show all product listings. how do we do it aagain? 36 | //Remove the currentProductListing from the global store. 37 | localStorage.clear(); 38 | this.props.removeCurrentProductListing() 39 | this.setState({ 40 | isNavigationChanged: !this.state.isNavigationChanged 41 | }, () => this.props.history.push("/login")); 42 | } 43 | 44 | render() { 45 | return ( 46 |
47 | 48 |
49 |
50 |
51 | 52 |
53 |
54 | 55 | { 56 | !localStorage.getItem("token") ? 57 | 58 | }> 59 | }> 60 | 61 | : 62 | 63 | } 64 | { 65 | !!localStorage.getItem("token") ? 66 | 67 | 68 | 69 | 70 | 71 | 72 | : 73 | 74 | } 75 | }> 76 | 77 | 78 | 79 | {/*} 80 | 81 | 82 | */} 83 |
84 | footer 85 |
86 |
87 |
88 |
89 | ); 90 | } 91 | } 92 | 93 | const mapDispatchToProps = (dispatch) => { 94 | return { 95 | removeCurrentProductListing: () => { 96 | dispatch(removeCurrentProductListing()) 97 | } 98 | }; 99 | } 100 | 101 | 102 | export default connect(null, mapDispatchToProps)(App); 103 | -------------------------------------------------------------------------------- /src/assets/css/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-header { 6 | background-color: #222; 7 | height: 200px; 8 | } 9 | 10 | #banner { 11 | width: 1550px; 12 | height: 200px; 13 | } 14 | 15 | #menu { 16 | margin-top: 0px; 17 | border-style: solid; 18 | border-color: green; 19 | border-bottom-width: 5px; 20 | } 21 | 22 | #categories { 23 | float: left; 24 | margin-top: 20px; 25 | } 26 | 27 | .category { 28 | border-style: solid; 29 | margin: 2px; 30 | width: 150px; 31 | color: white !important; 32 | background-color: #453D3C; 33 | border-radius: 7px; 34 | font-family: Arimo; 35 | font-weight: bold 36 | } 37 | 38 | #search { 39 | border-style: solid; 40 | border-color: green; 41 | box-sizing: border-box; 42 | width: 300px; 43 | margin: 10px 44 | } 45 | 46 | #sortBy { 47 | border-style: solid; 48 | border-color: green; 49 | width: 100px; 50 | } 51 | 52 | body { 53 | background-color: #D2691E; 54 | } 55 | 56 | #heading-for-search-sort-by { 57 | color: white; 58 | } 59 | 60 | #wrapper-for-search-sort-by { 61 | width: 1350px; 62 | border: 1px solid black; 63 | overflow: hidden; 64 | padding: 20px; 65 | background-color: #453D3C; 66 | border-radius: 7px; 67 | display: inline-block; 68 | margin-bottom: 30px; 69 | margin-top: 20px; 70 | } 71 | 72 | .form { 73 | margin: 50px auto; 74 | width: 250px; 75 | } 76 | 77 | .form-input { 78 | width: 300px; 79 | } 80 | 81 | #product-details-image { 82 | width: 400px; 83 | height: 200px; 84 | margin: 20px auto; 85 | display: block; 86 | } 87 | 88 | #details { 89 | /* margin-top: 200px; 90 | margin-left: 200px; */ 91 | margin: 0px auto !important; 92 | border-width: 3px; 93 | border-color: green; 94 | border-style: solid; 95 | border-radius: 5px; 96 | } 97 | 98 | 99 | #wanted { 100 | margin: 0px auto; 101 | border-width: 3px; 102 | border-color: green; 103 | border-style: solid; 104 | border-radius: 5px; 105 | } 106 | 107 | /* 108 | #wrapper-for-product-details { 109 | margin: 0px auto; 110 | width: 100px; 111 | } 112 | 113 | #product-details-name { 114 | font-size: 50px; 115 | float: left; 116 | } 117 | 118 | #details-description { 119 | font-style: italic; 120 | font-size: 70px; 121 | } 122 | 123 | #details-location { 124 | font-size: 30px; 125 | } */ 126 | 127 | /* #details { 128 | font-size: 20px; 129 | border: 1px solid black; 130 | overflow: hidden; 131 | width: 650px; 132 | float: left; 133 | border-color: green; 134 | border-width: 2px; 135 | border-bottom-width: 5px; 136 | border-right-width: 5px; 137 | font-family: Arimo; 138 | } 139 | 140 | #details-description { 141 | font-style: italic; 142 | font-size: 20px; 143 | word-wrap: break-word; 144 | } 145 | 146 | #details-description-heading { 147 | font-size: 30px; 148 | border: 1px solid black; 149 | overflow: hidden; 150 | width: 650px; 151 | float: left; 152 | border-bottom-width: 2px; 153 | border-color: green; 154 | background-color: #453D3C; 155 | color: white; 156 | font-family: Arimo; 157 | } */ 158 | 159 | /* #wanted { 160 | font-size: 20px; 161 | border: 1px solid black; 162 | overflow: hidden; 163 | width: 650px; 164 | height: 100px; 165 | float: left; 166 | border-color: green; 167 | border-width: 2px; 168 | border-bottom-width: 5px; 169 | border-right-width: 5px; 170 | font-family: Arimo; 171 | } */ 172 | 173 | .form-select { 174 | width: 300px; 175 | } 176 | 177 | .uploaded-image { 178 | width: 250px; 179 | height: auto; 180 | border-radius: 7px; 181 | margin: 20px; 182 | } 183 | 184 | .image-upload-area { 185 | border-color: green !important; 186 | float: right !important; 187 | width: 200px !important; 188 | height: 100px !important; 189 | font-size: 20px; 190 | font-family: Arimo; 191 | font-style: italic; 192 | border-bottom-width: 5px !important; 193 | } 194 | 195 | .error-messages { 196 | margin-top: 50px; 197 | float: center; 198 | font-family: Arimo; 199 | font-weight: bold; 200 | color: red !important; 201 | } 202 | 203 | .product-listings-table-header { 204 | background-color: #453D3C !important; 205 | color: white !important; 206 | font-family: Arimo !important; 207 | font-weight: bold !important; 208 | font-size: 20px; 209 | } 210 | 211 | .private-listing-details { 212 | font-size: 20px !important; 213 | font-style: italic; 214 | } 215 | 216 | .loader { 217 | margin: 0 auto !important; 218 | width: 100% !important; 219 | color: red !important; 220 | font-family: Arimo !important; 221 | font-size: weight; 222 | font-size: 20px; 223 | } 224 | 225 | #footer { 226 | width: 1550px; 227 | height: 200px; 228 | bottom: -100px; 229 | position: relative; 230 | } 231 | 232 | #card-image { 233 | width: 300px; 234 | height: 180px; 235 | } 236 | 237 | .private-image { 238 | width: 100px; 239 | height: 100px; 240 | } 241 | .results { 242 | background-color: #453D3C !important; 243 | border: #453D3C; 244 | color: #453D3C !important; 245 | } 246 | 247 | .listings-table { 248 | border-width: 5px !important; 249 | border-color: green !important; 250 | } 251 | -------------------------------------------------------------------------------- /src/ProductListings/ProductListingsCollection.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import ProductListing from "./ProductListing.js"; 3 | import ProductListingDetails from "./ProductListingDetails.js"; 4 | import { connect } from "react-redux"; 5 | import { BrowserRouter, Route } from 'react-router-dom' 6 | 7 | const ProductListingsCollection = (props) => { 8 | console.log("INSIDE ProductListingsCollection", props) 9 | 10 | let p = null; 11 | 12 | //The very first time we visit the product listings page, we want to see all the 13 | //products from all categories 14 | if(props.categorySelected !== "All") { 15 | //Filter product listings based on the category 16 | const productListingsByCategory = props.productListings.filter((productListingObj) => { 17 | return productListingObj.category_id === props.categorySelected; 18 | }); 19 | 20 | //Filter product listings based on the searchTerm so that we can prepare it 21 | //for sorting 22 | const filterProductListings = productListingsByCategory.filter((productListingObj) => { 23 | return productListingObj.name.toLowerCase().includes(props.searchTerm.toLowerCase()); 24 | }); 25 | 26 | //Sort the filterProductListings by switching on several different options and 27 | //this will get rendered in the return statement 28 | switch(props.sortByOption) { 29 | case "Relevance": 30 | console.log("hello relevance") 31 | p = filterProductListings 32 | break; 33 | case "Recent": 34 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 35 | return new Date(productListingObj2["created_at"]) - new Date(productListingObj1["created_at"]); 36 | }); 37 | console.log("p is", p) 38 | break; 39 | case "Price: Low to High": 40 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 41 | return Number(productListingObj1.value) - Number(productListingObj2.value); 42 | }); 43 | break; 44 | case "Price: High to Low": 45 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 46 | return Number(productListingObj2.value) - Number(productListingObj1.value); 47 | }); 48 | break; 49 | case "Featured": 50 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 51 | return Number(productListingObj2.rating) - Number(productListingObj1.rating); 52 | }); 53 | default: 54 | p = filterProductListings; 55 | break; 56 | } 57 | } else { 58 | //p = props.productListings 59 | 60 | //Filter product listings based on the searchTerm so that we can prepare it 61 | //for sorting 62 | const filterProductListings = props.productListings.filter((productListingObj) => { 63 | return productListingObj.name.toLowerCase().includes(props.searchTerm.toLowerCase()); 64 | }); 65 | 66 | //Sort the filterProductListings by switching on several different options and 67 | //this will get rendered in the return statement 68 | switch(props.sortByOption) { 69 | case "Relevance": 70 | console.log("hello relevance") 71 | p = filterProductListings 72 | break; 73 | case "Recent": 74 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 75 | return new Date(productListingObj2["created_at"]) - new Date(productListingObj1["created_at"]); 76 | }); 77 | console.log("p is", p) 78 | break; 79 | case "Price: Low to High": 80 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 81 | return Number(productListingObj1.value) - Number(productListingObj2.value); 82 | }); 83 | break; 84 | case "Price: High to Low": 85 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 86 | return Number(productListingObj2.value) - Number(productListingObj1.value); 87 | }); 88 | break; 89 | case "Featured": 90 | p = filterProductListings.sort((productListingObj1, productListingObj2) => { 91 | return Number(productListingObj2.rating) - Number(productListingObj1.rating); 92 | }); 93 | default: 94 | p = filterProductListings; 95 | break; 96 | } 97 | } 98 | 99 | //Conditional rendering to display either a collection of product listings or 100 | //a specific product listing. If there exists a currentProductListing, then 101 | //we must show its details. Otherwise, show the entire collection of 102 | //product listings. 103 | const productListingsCards = p.map((productListingObj) => { 104 | return 105 | }); 106 | 107 | return ( 108 |
109 | { 110 | props.currentProductListing === null ? 111 |
112 | {productListingsCards} 113 |
114 | : 115 | 116 | } 118 | /> 119 | 120 | } 121 |
122 | ); 123 | } 124 | 125 | function mapStateToProps(state) { 126 | return { 127 | //Filtering product that are not sold 128 | productListings: state.productListings.filter((productListingObj) => { 129 | return !productListingObj.isSold 130 | }), 131 | searchTerm: state.searchTerm, 132 | sortByOption: state.sortByOption, 133 | categorySelected: state.categorySelected, 134 | currentProductListing: state.currentProductListing 135 | } 136 | } 137 | 138 | export default connect(mapStateToProps)(ProductListingsCollection); 139 | -------------------------------------------------------------------------------- /src/Matches/MatchingProductListings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import adapter from "../adapter.js"; 3 | import { connect } from "react-redux"; 4 | import ProductListingDetails from "../ProductListings/ProductListingDetails.js"; 5 | import { updateProductListings, removeCurrentProductListing, selectProductListing } from "../actions/index.js"; 6 | import { Table, Icon } from "semantic-ui-react"; 7 | import { BrowserRouter, Route } from "react-router-dom"; 8 | 9 | class MatchingProductListings extends Component { 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | //This is very important because if the user decided to switch to this page 15 | //after viewing the product details for a particular product and then switch it 16 | //back to the all product listings page, then we want to show all the 17 | //products, not the previous product details. We need to this on all pages, except 18 | //all product listings page 19 | 20 | componentDidMount() { 21 | this.props.removeCurrentProductListing(); 22 | 23 | adapter.get("product_listings") 24 | .then(response => response.json()) 25 | .then(data => this.props.updateProductListings(data)); 26 | } 27 | 28 | componentWillUnmount() { 29 | this.props.updateProductListings([]); 30 | } 31 | 32 | itemsUserisLookingFor = () => { 33 | //Filter products listings belonging to a user and get the items they are looking for 34 | const p = this.props.productListings.filter((productListingObj) => { 35 | return productListingObj.user_id === Number(adapter.getUserId()) && !productListingObj.isSold 36 | }).map((productListingObj) => { 37 | return productListingObj.exchange_item; 38 | }).filter((exchangeItem) => { 39 | return exchangeItem !== null; 40 | }) 41 | 42 | //Remove duplicates 43 | const itemsUserisLookingFor = []; 44 | 45 | p.forEach((exchangeItem) => { 46 | if(itemsUserisLookingFor.indexOf(exchangeItem) === -1) { 47 | itemsUserisLookingFor.push(exchangeItem); 48 | } 49 | }); 50 | 51 | return itemsUserisLookingFor; 52 | } 53 | 54 | matchingProductListings = (items) => { 55 | //Filter all product listings that does not belong to the user and not sold out 56 | //since we are trying to match the products the user is looking to buy or exchange 57 | 58 | const productListings = this.props.productListings.filter((productListingObj) => { 59 | return productListingObj.user_id !== Number(adapter.getUserId()) && !productListingObj.isSold; 60 | }); 61 | 62 | const newProductListings = []; 63 | 64 | for(let i = 0; i < productListings.length; i++) { 65 | const productListing = productListings[i]; 66 | for(let j = 0; j < items.length; j++) { 67 | //productListing.name.toLowerCase().includes(items[j].toLowerCase()) 68 | if(this.call(productListing.name, items[j])) { 69 | newProductListings.push(productListing); 70 | } 71 | } 72 | } 73 | 74 | return newProductListings; 75 | } 76 | 77 | call = (item1, item2) => { 78 | const item1Words = item1.split(" "); 79 | const item2Words = item2.split(" "); 80 | 81 | for(let i = 0; i < item1Words.length; i++) { 82 | const word1 = item1Words[i]; 83 | for(let j = 0; j < item2Words.length; j++) { 84 | if(word1.toLowerCase().includes(item2Words[j].toLowerCase())) { 85 | return true; 86 | } 87 | } 88 | } 89 | 90 | return false 91 | } 92 | 93 | productListingsRow = () => { 94 | const productListings = this.matchingProductListings(this.itemsUserisLookingFor()); 95 | //debugger 96 | const rows = productListings.map((productListingObj) => { 97 | return ( 98 | 99 | 100 | 101 |

this.props.selectProductListing(productListingObj)}>{productListingObj.name}

102 |

{productListingObj.description}

103 |
104 | ${productListingObj.value} 105 | 106 | { 107 | productListingObj.exchange_item === null ? 108 | "Cash" 109 | : 110 | productListingObj.exchange_item 111 | } 112 | 113 | 114 | {productListingObj.created_at} 115 | 116 |
117 | ); 118 | }); 119 | 120 | return rows; 121 | } 122 | 123 | render() { 124 | return ( 125 |
126 | { 127 | this.props.currentProductListing === null ? 128 | 129 | 130 | 131 | Content 132 | Value 133 | Exchange Item 134 | Date Posted 135 | 136 | 137 | 138 | 139 | {this.productListingsRow()} 140 | 141 |
142 | : 143 | 144 | } 146 | /> 147 | 148 | } 149 |
150 | ); 151 | } 152 | } 153 | 154 | const mapStateToProps = (state) => { 155 | return { 156 | productListings: state.productListings, 157 | currentProductListing: state.currentProductListing 158 | }; 159 | } 160 | 161 | const mapDispatchToProps = (dispatch) => { 162 | return { 163 | updateProductListings: (newProductListings) => { 164 | dispatch(updateProductListings(newProductListings)); 165 | }, 166 | selectProductListing: (productListing) => { 167 | dispatch(selectProductListing(productListing)); 168 | }, 169 | removeCurrentProductListing: () => { 170 | dispatch(removeCurrentProductListing()) 171 | } 172 | }; 173 | } 174 | 175 | 176 | export default connect(mapStateToProps, mapDispatchToProps)(MatchingProductListings); 177 | -------------------------------------------------------------------------------- /src/PrivateListings/PrivateProductListings.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Table, Icon } from "semantic-ui-react"; 3 | import adapter from "../adapter.js"; 4 | import { updateProductListings, removeCurrentProductListing, selectProductListing } from "../actions/index.js"; 5 | import { connect } from "react-redux"; 6 | import Moment from 'react-moment'; 7 | import ProductListingDetails from "../ProductListings/ProductListingDetails.js"; 8 | import { BrowserRouter, Route } from "react-router-dom"; 9 | 10 | //This component was once a presentation component for the purpose of listing 11 | //an user's own product listings. Now, it is a class component since I need to 12 | //use the componentDidMount method to reset the currentProductListing to null 13 | //so that when the user switches from this page to the all productListings page, 14 | //they WILL see all product listings rather than once single product listing details. 15 | class PrivateProductListings extends Component { 16 | constructor(props) { 17 | super(props); 18 | } 19 | 20 | //This is very important because if the user decided to switch to this page 21 | //after viewing the product details for a particular product and then switch it 22 | //back to the all product listings age, then we want to show all the 23 | //products, not the previous product details. We need to this on all pages, except 24 | //all product listings page 25 | componentDidMount() { 26 | this.props.removeCurrentProductListing(); 27 | //VERY IMPORTANT CHANGE HERE SINCE ADDING OAUTH! 28 | //Currently, once the user logs in, the user id is stored in the local storage 29 | //and we need to fetch all private listings belonging to that user. 30 | adapter.get(`users/${adapter.getUserId()}/product_listings`) 31 | .then(response => response.json()) 32 | .then(data => this.props.updateProductListings(data)); 33 | } 34 | 35 | productListingsRows = () => { 36 | console.log("INSIDE PRIVATE ProductListings", this.props) 37 | // const rows = this.props.productListings.filter((productListingObj) => { 38 | // return productListingObj.user_id === this.props.userId 39 | // }) 40 | const rows = this.props.productListings.map((productListingObj) => { 41 | return ( 42 | 43 | 44 | 45 | this.props.selectProductListing(productListingObj)}>

{productListingObj.name}

46 |

{productListingObj.description}

47 |
48 | ${productListingObj.value} 49 | 50 | { 51 | productListingObj.exchange_item === null ? 52 | "Cash" 53 | : 54 | productListingObj.exchange_item 55 | } 56 | 57 | {productListingObj.created_at} 58 | 59 | { 60 | productListingObj.isSold ? 61 | product image 62 | : 63 | null 64 | } 65 | 66 | 67 | { 71 | //Upon clicking the delete icon, we will make a DELETE request 72 | //to delete a particular product listing from the backend and then 73 | //update the product listings in the global state, thus causing 74 | //a rerender of PrivateProductListings 75 | adapter.delete(`product_listings/${productListingObj.id}`) 76 | // .then(response => response.json()) 77 | // .then(data => console.log(data)); 78 | .then(() => { 79 | const newProductListings = this.props.productListings.filter((plObj) => { 80 | return plObj.id !== productListingObj.id && plObj.user_id === Number(adapter.getUserId()); 81 | }); 82 | 83 | this.props.updateProductListings(newProductListings); 84 | }); 85 | }) 86 | } 87 | /> 88 | 89 |
90 | ); 91 | }); 92 | 93 | return rows; 94 | } 95 | //Filter the product listings for a particular user. 96 | 97 | render() { 98 | return ( 99 |
100 | { 101 | this.props.currentProductListing === null ? 102 | 103 | 104 | 105 | Content 106 | Value 107 | Exchange Item 108 | Date Posted 109 | 110 | Del 111 | 112 | 113 | 114 | 115 | {this.productListingsRows()} 116 | 117 |
118 | : 119 | 120 | } 122 | /> 123 | 124 | } 125 |
126 | ); 127 | } 128 | } 129 | 130 | const mapStateToProps = (state) => { 131 | return { 132 | productListings: state.productListings, 133 | currentProductListing: state.currentProductListing 134 | }; 135 | } 136 | 137 | const mapDispatchToProps = (dispatch) => { 138 | return { 139 | updateProductListings: (newProductListings) => { 140 | dispatch(updateProductListings(newProductListings)); 141 | }, 142 | removeCurrentProductListing: () => { 143 | dispatch(removeCurrentProductListing()); 144 | }, 145 | selectProductListing: (productListing) => { 146 | dispatch(selectProductListing(productListing)) 147 | } 148 | }; 149 | } 150 | 151 | export default connect(mapStateToProps, mapDispatchToProps)(PrivateProductListings); 152 | -------------------------------------------------------------------------------- /src/Register/UserRegisterForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import adapter from "../adapter.js"; 3 | import { setUserId } from "../actions/index.js"; 4 | import { connect } from "react-redux"; 5 | import { Form, Input, Button } from "semantic-ui-react"; 6 | import { Message } from "semantic-ui-react"; 7 | import Dropzone from "react-dropzone"; 8 | import request from "superagent"; 9 | import { CLOUDINARY_UPLOAD_PRESET, CLOUDINARY_UPLOAD_URL } from "../keys.js"; 10 | 11 | class UserRegisterForm extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = { 16 | firstName: "", 17 | lastName: "", 18 | location: null, 19 | image: "", 20 | username: "", 21 | password: "", 22 | passwordConfirmation: "", 23 | isError: false, 24 | errorMessages: [] 25 | }; 26 | } 27 | 28 | handleSubmit = (event) => { 29 | event.preventDefault(); 30 | 31 | const bodyForUser = { 32 | first_name: this.state.firstName, 33 | last_name: this.state.lastName, 34 | location: this.state.location, 35 | image: this.state.image, 36 | username: this.state.username, 37 | password: this.state.password, 38 | password_confirmation: this.state.passwordConfirmation 39 | } 40 | 41 | adapter.post("users", bodyForUser) 42 | .then(response => response.json()) 43 | .then(data => { 44 | //We store the token in the local storage rather than in the global state 45 | //since page refresh will cause the state to reset. But the local storage 46 | //will persist the token until it has been removed. 47 | //Check whether the json that is sent to the client has errors, 48 | //if it does, then we know the backend validations failed. Display the 49 | //error messages. Otherwise, redirect to all prouct listings page. 50 | if(!!data.errors) { 51 | this.setState({ 52 | isError: true, 53 | errorMessages: data.errors 54 | }); 55 | } else { 56 | adapter.setToken(data.token); 57 | adapter.setUserId(data.userId); 58 | this.props.history.push("/product-listings"); 59 | window.location.reload() 60 | } 61 | }); 62 | } 63 | 64 | handleChange = (event, {name, value}) => { 65 | if (event.target.value === undefined) { 66 | this.setState({ 67 | [name]: value 68 | }, () => console.log(this.state)); 69 | } else { 70 | this.setState({ 71 | [event.target.name]: event.target.value 72 | }, () => console.log(this.state)); 73 | } 74 | } 75 | 76 | locationOptions = () => { 77 | const locations = ['Alabama','Alaska','American Samoa','Arizona','Arkansas','California','Colorado','Connecticut','Delaware','District of Columbia','Federated States of Micronesia','Florida','Georgia','Guam','Hawaii','Idaho','Illinois','Indiana','Iowa','Kansas','Kentucky','Louisiana','Maine', 78 | 'Marshall Islands','Maryland','Massachusetts','Michigan','Minnesota','Mississippi','Missouri','Montana','Nebraska','Nevada','New Hampshire','New Jersey','New Mexico','New York','North Carolina','North Dakota','Northern Mariana Islands','Ohio','Oklahoma','Oregon','Palau','Pennsylvania','Puerto Rico','Rhode Island','South Carolina', 79 | 'South Dakota','Tennessee','Texas','Utah','Vermont','Virgin Island','Virginia','Washington','West Virginia','Wisconsin','Wyoming']; 80 | 81 | return locations.map((location) => { 82 | return {key: location, value: location, text: location}; 83 | }); 84 | } 85 | 86 | loadErrors = () => { 87 | return ( 88 | 94 | ); 95 | } 96 | 97 | onImageDrop = (files) => { 98 | this.setState({ 99 | uploadedFile: files[0] 100 | }, () => this.handleImageUpload(files[0])); 101 | } 102 | 103 | //Uploading to the image to the API. 104 | handleImageUpload = (file) => { 105 | let upload = request.post(CLOUDINARY_UPLOAD_URL) 106 | .field('upload_preset', CLOUDINARY_UPLOAD_PRESET) 107 | .field('file', file); 108 | 109 | upload.end((err, response) => { 110 | if(err) { 111 | console.error(err); 112 | } 113 | 114 | if(response.body.secure_url !== '') { 115 | this.setState({ 116 | image: response.body.secure_url 117 | }, () => console.log("INSIDE UPLOAD PIC", this.state)); 118 | } 119 | }); 120 | } 121 | 122 | loadForm = () => { 123 | return ( 124 |
125 |

Register

126 |
127 | 138 | 149 | this.handleChange(event, { name, value })} 158 | /> 159 | 165 |

Drop an image or click to select a file to upload.

166 |
167 | { 168 | this.state.image.length !== 0 ? 169 | 170 | : 171 | null 172 | } 173 | 183 | 194 | 205 | 206 | 207 |
208 | ); 209 | } 210 | 211 | render() { 212 | console.log("inside registration form", this.props) 213 | return ( 214 |
215 | { 216 | this.state.isError ? 217 | 218 | {this.loadErrors()} 219 | {this.loadForm()} 220 | 221 | : 222 | 223 | {this.loadForm()} 224 | 225 | } 226 |
227 | ); 228 | } 229 | } 230 | const mapDispatchToProps = (dispatch) => { 231 | return { 232 | setUserId: (userId) => { 233 | dispatch(setUserId(userId)); 234 | } 235 | }; 236 | } 237 | 238 | export default connect(null, mapDispatchToProps)(UserRegisterForm); 239 | -------------------------------------------------------------------------------- /src/ProductListings/ProductListingDetails.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import adapter from "../adapter.js"; 3 | import { Form, Input, Button } from "semantic-ui-react"; 4 | import { Rating } from "semantic-ui-react"; 5 | import { connect } from "react-redux"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | import { removeCurrentProductListing, updateProductListings } from "../actions/index.js"; 8 | 9 | class ProductListingDetails extends Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | purchaseOption: null 15 | } 16 | } 17 | 18 | componentDidMount() { 19 | //This will mimic the RAILS show page where rails route for show controller action 20 | //will look something like this: products/4. React Router achieves the same thing. 21 | this.props.history.push(`product-listings/${this.props.currentProductListing.id}`); 22 | } 23 | 24 | //a fetch GET request happens in this method because once the user purchased 25 | //an item from the product listing view details page and goes back to the 26 | //all product listings page, then we need to update the product listings with 27 | //new new information since an item is purchased by the user. 28 | removeCurrentProductListing = () => { 29 | this.props.removeCurrentProductListing(); 30 | //The goBack method will push the previous url to the browser. 31 | this.props.history.goBack(); 32 | } 33 | 34 | handleRedirect = () => { 35 | console.log("inside productListingdetails", this.props) 36 | this.props.history.push("/edit-product-listing") 37 | } 38 | 39 | handleChange = (event, { name, value }) => { 40 | if(event.target.value === undefined) { 41 | this.setState({ 42 | [name]: value 43 | }, () => console.log(this.state.purchaseOption)); 44 | } 45 | } 46 | 47 | handlePurchase = () => { 48 | //sending a POST request to the purchase 49 | //Patch the product listings to mark it as sold so that we can filter 50 | //the product listings that are not sold in the homepage 51 | const bodyForPurchase = { 52 | name: this.props.currentProductListing.name, 53 | description: this.props.currentProductListing.description, 54 | image: this.props.currentProductListing.image, 55 | value: this.props.currentProductListing.value, 56 | condition: this.props.currentProductListing.condition, 57 | location: this.props.currentProductListing.location, 58 | delivery_method: this.props.currentProductListing.delivery_method, 59 | mode_of_purchase: this.state.purchaseOption, 60 | rating: this.props.currentProductListing.rating, 61 | category_id: this.props.currentProductListing.category_id, 62 | user_id: Number(adapter.getUserId()), 63 | seller_id: this.props.currentProductListing.user_id 64 | }; 65 | 66 | const bodyForProductListing = { 67 | isSold: true 68 | }; 69 | 70 | Promise.all([ 71 | adapter.patch(`product_listings/${this.props.currentProductListing.id}`, bodyForProductListing), 72 | adapter.post("purchases", bodyForPurchase) 73 | ]).then(() => {this.getProducts()}); 74 | } 75 | 76 | //Promise.all somehow prioritize fetch GET request since I need to get new product 77 | //listings after the product has been pruchased, we need that new list. 78 | getProducts = () => { 79 | adapter.get("product_listings") 80 | .then(response => response.json()) 81 | .then(data => this.props.updateProductListings(data)) 82 | .then(() => this.removeCurrentProductListing()); 83 | } 84 | 85 | purchaseOptions = () => { 86 | //Checking whether the user is looking for cash or an exchange item 87 | //If they wanted cash, 88 | //then select option will only include cash 89 | //Otherwise, all options are included: cash, exchange_item and offer 90 | if(this.props.currentProductListing.exchange_item === null) { 91 | return [{key: "cash", value: "Cash", text: "Cash"}]; 92 | } else { 93 | return [ 94 | {key: "cash", value: "Cash", text: "Cash"}, 95 | {key: "exchangeItem", value: "Exchange Item", text: "I have the exchange item"} 96 | //{key: "offer", value: "Offer", text: "I neither want to pay with cash or exchange any item. Make an offer instead"} 97 | ]; 98 | } 99 | } 100 | 101 | getUserName = () => { 102 | const username = this.props.users.find((userObj) => { 103 | return userObj.id === this.props.currentProductListing.user_id; 104 | }).username; 105 | 106 | return username; 107 | } 108 | 109 | //Here, we need to conditional rendering since we should not allow a user to 110 | //purchase their own product. 111 | //If there exists a token and user id and the user id does not match the product 112 | //listing user_id, 113 | //Then show the select option 114 | //Otherwise do not show it. 115 | //Also, we need to conditional rendering based on the selected option 116 | //If the user selects the cash option or exchange option 117 | //then we show the checkout button 118 | //If the user selects the offer option 119 | //then we show the offer button 120 | render() { 121 | return ( 122 |
123 |
124 | 125 | {/*{ 126 | this.props.currentProductListing.user_id === Number(adapter.getUserId()) && !this.props.currentProductListing.isSold ? 127 | 128 | : 129 | null 130 | } 131 | */} 132 | 133 |

{this.props.currentProductListing.name}

134 |

Description

135 |

{this.props.currentProductListing.description}

136 |

Location: {this.props.currentProductListing.location}

137 |

Delivery: {this.props.currentProductListing.delivery_method}

138 |

Condition: {this.props.currentProductListing.condition}

139 |

Value: ${this.props.currentProductListing.value}

140 |

Sold by: {this.getUserName()}

141 |
142 |
143 |

Wanted

144 | { 145 | this.props.currentProductListing.exchange_item === null ? 146 | "Cash" 147 | : 148 | this.props.currentProductListing.exchange_item 149 | } 150 | { 151 | !adapter.getToken() ? 152 |

***Log in to buy item***

153 | : 154 | null 155 | } 156 | { 157 | !!adapter.getToken() && !!adapter.getUserId() && Number(adapter.getUserId()) !== this.props.currentProductListing.user_id ? 158 | this.handleChange(event, { name, value })} 166 | /> 167 | : 168 | null 169 | } 170 | { 171 | this.state.purchaseOption === "Cash" || this.state.purchaseOption === "Exchange Item" ? 172 | 173 | : 174 | null 175 | } 176 | { 177 | this.state.purchaseOption === "Offer" ? 178 |
179 | 180 | 181 |
182 | : 183 | null 184 | } 185 |
186 |
187 | ); 188 | } 189 | } 190 | 191 | const mapStateToProps = (state) => { 192 | return { 193 | currentProductListing: state.currentProductListing, 194 | users: state.users 195 | }; 196 | } 197 | 198 | //Once the "back to productListing" button is clicked, make the currentProductListing null so that 199 | //when we go to the match product listings, we will see a table, instead of the 200 | //the product listing details 201 | const mapDispatchToProps = (dispatch) => { 202 | return { 203 | removeCurrentProductListing: () => { 204 | dispatch(removeCurrentProductListing()) 205 | }, 206 | updateProductListings: (productListings) => { 207 | dispatch(updateProductListings(productListings)) 208 | } 209 | } 210 | } 211 | 212 | export default connect(mapStateToProps, mapDispatchToProps)(ProductListingDetails); 213 | -------------------------------------------------------------------------------- /src/NewListing/ProductListingForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { removeCurrentProductListing } from "../actions/index.js"; 3 | import { Form, Input, Button, Select, TextArea } from "semantic-ui-react"; 4 | import { Message } from "semantic-ui-react"; 5 | import adapter from "../adapter.js"; 6 | import { connect } from "react-redux"; 7 | import Dropzone from "react-dropzone"; 8 | import request from "superagent"; 9 | import { CLOUDINARY_UPLOAD_PRESET, CLOUDINARY_UPLOAD_URL } from "../keys.js"; 10 | 11 | 12 | class ProductListingForm extends Component { 13 | constructor() { 14 | super(); 15 | 16 | this.state = { 17 | name: "", 18 | description: "", 19 | image: "", 20 | value: "", 21 | condition: null, 22 | location: null, 23 | deliveryMethod: null, 24 | category: null, 25 | option: "cash", 26 | exchangeItem: "", 27 | isError: false, 28 | errorMessages: [] 29 | }; 30 | } 31 | 32 | //This is very important because if the user decided to switch to this page 33 | //after viewing the product details for a particular product and then switch it 34 | //back to the all product listings page, then we want to show all the 35 | //products, not the previous product details. We need to this on all pages, except 36 | //all product listings page 37 | 38 | componentDidMount() { 39 | this.props.removeCurrentProductListing(); 40 | } 41 | 42 | handleSubmit = (event) => { 43 | event.preventDefault(); 44 | 45 | //get the id of the category so that we can send a POST request 46 | //to store the product listing. REMEMBER - product listing belongs 47 | //to a category 48 | 49 | let categoryId = null; 50 | //Check to see whether user has selected the category. Otherwise, assign 51 | //the product listing to the cateogry "others". Unfortunately, 52 | //adding "required" props to Semantic UI react component, does not force the 53 | //user to select a category. 54 | if(!this.state.category) { 55 | categoryId = this.props.categories.find((categoryObj) => { 56 | return categoryObj.name === "Others"; 57 | }).id; 58 | } else { 59 | categoryId = this.props.categories.find((categoryObj) => { 60 | return categoryObj.name === this.state.category; 61 | }).id; 62 | } 63 | 64 | //Decide if the the seller is looking for any exchange item, 65 | //if the option is cash, 66 | //then exchange item should be set to null 67 | //Otherwise, it should be set to the value of the exchange item 68 | let exchange_item = null; 69 | if(this.state.option === "cash") { 70 | exchange_item = null; 71 | } else { 72 | exchange_item = this.state.exchangeItem 73 | } 74 | 75 | const body = { 76 | name: this.state.name, 77 | description: this.state.description, 78 | image: this.state.image, 79 | value: this.state.value, 80 | condition: this.state.condition, 81 | location: this.state.location, 82 | delivery_method: this.state.deliveryMethod, 83 | exchange_item: exchange_item, 84 | rating: 0, 85 | category_id: categoryId, 86 | user_id: Number(adapter.getUserId()), 87 | isSold: false 88 | }; 89 | 90 | adapter.post("product_listings", body) 91 | .then(response => response.json()) 92 | .then(data => { 93 | if(!!data.errors) { 94 | this.setState({ 95 | isError: true, 96 | errorMessages: data.errors 97 | }); 98 | } else { 99 | this.setState({ 100 | isError: false 101 | }, () => this.resetForm()); 102 | } 103 | }); 104 | } 105 | 106 | resetForm = () => { 107 | this.setState({ 108 | name: "", 109 | description: "", 110 | image: "", 111 | value: "", 112 | condition: null, 113 | location: null, 114 | deliveryMethod: null, 115 | category: null, 116 | option: "cash", 117 | exchangeItem: "" 118 | }); 119 | } 120 | 121 | handleChange = (event, {name, value}) => { 122 | //get the id of the category so that we can send a POST request 123 | //to store the product listing. REMEMBER - product listing belongs 124 | //to a category 125 | if(event.target.value === undefined) { 126 | this.setState({ 127 | [name]: value 128 | }, () => console.log(this.state)); 129 | } else { 130 | this.setState({ 131 | [event.target.name]: event.target.value 132 | }, () => console.log(this.state)); 133 | } 134 | } 135 | 136 | //These are options for the Form.Select which is a Semantic UI react component. 137 | //It requires to have an array of objects. 138 | locationOptions = () => { 139 | const locations = ['Alabama','Alaska','American Samoa','Arizona','Arkansas','California','Colorado','Connecticut','Delaware','District of Columbia','Federated States of Micronesia','Florida','Georgia','Guam','Hawaii','Idaho','Illinois','Indiana','Iowa','Kansas','Kentucky','Louisiana','Maine', 140 | 'Marshall Islands','Maryland','Massachusetts','Michigan','Minnesota','Mississippi','Missouri','Montana','Nebraska','Nevada','New Hampshire','New Jersey','New Mexico','New York','North Carolina','North Dakota','Northern Mariana Islands','Ohio','Oklahoma','Oregon','Palau','Pennsylvania','Puerto Rico','Rhode Island','South Carolina', 141 | 'South Dakota','Tennessee','Texas','Utah','Vermont','Virgin Island','Virginia','Washington','West Virginia','Wisconsin','Wyoming']; 142 | 143 | return locations.map((location) => { 144 | return {key: location, value: location, text: location}; 145 | }); 146 | } 147 | 148 | conditionOptions = () => { 149 | return [ 150 | {key: "brand new", value: "Brand New", text: "Brand New"}, 151 | {key: "like new", value: "Like New", text: "Like New"}, 152 | {key: "good", value: "Good", text: "Good"}, 153 | {key: "old", value: "Old", text: "Old"} 154 | ]; 155 | } 156 | 157 | deliveryOptions = () => { 158 | return [ 159 | {key: "shipping", value: "Shipping", text: "Shipping"}, 160 | {key: "local pick-up", value: "Local Pick-up", text: "Local Pick-up"} 161 | ]; 162 | } 163 | 164 | categoryOptions = () => { 165 | return this.props.categories.map((categoryObj) => { 166 | return {key: categoryObj.name, value: categoryObj.name, text: categoryObj.name}; 167 | }); 168 | } 169 | 170 | onImageDrop = (files) => { 171 | this.setState({ 172 | uploadedFile: files[0] 173 | }, () => this.handleImageUpload(files[0])); 174 | } 175 | 176 | //Uploading to the image to the API. 177 | handleImageUpload = (file) => { 178 | let upload = request.post(CLOUDINARY_UPLOAD_URL) 179 | .field('upload_preset', CLOUDINARY_UPLOAD_PRESET) 180 | .field('file', file); 181 | 182 | upload.end((err, response) => { 183 | if(err) { 184 | console.error(err); 185 | } 186 | 187 | if(response.body.secure_url !== '') { 188 | this.setState({ 189 | image: response.body.secure_url 190 | }, () => console.log("INSIDE UPLOAD PIC", this.state)); 191 | } 192 | }); 193 | } 194 | 195 | loadErrors = () => { 196 | return ( 197 | 203 | ); 204 | } 205 | 206 | loadForm = () => { 207 | return ( 208 |
209 |

Product Listing

210 |
211 | 222 | 233 | 238 |

Drop an image or click to select a file to upload.

239 |
240 | { 241 | this.state.image.length !== 0 ? 242 | 243 | : 244 | null 245 | } 246 | 260 | this.handleChange(event, { name, value })} 269 | /> 270 | this.handleChange(event, { name, value })} 279 | /> 280 | this.handleChange(event, { name, value })} 289 | /> 290 | { 299 | this.handleChange(event, { name, value }); 300 | }} 301 | /> 302 | 305 | this.handleChange(event, { name, value })} 312 | /> 313 | this.handleChange(event, { name, value })} 320 | /> 321 | { 322 | this.state.option === "exchange item" ? 323 | 332 | : 333 | null 334 | } 335 | 336 | 337 |
338 | ); 339 | } 340 | 341 | render() { 342 | console.log('rending product listing form'); 343 | return ( 344 |
345 | {this.state.isError ? 346 | 347 | {this.loadErrors()} 348 | {this.loadForm()} 349 | 350 | : 351 | 352 | {this.loadForm()} 353 | 354 | } 355 |
356 | 357 | ); 358 | } 359 | } 360 | 361 | function mapStateToProps(state) { 362 | return { 363 | categories: state.categories 364 | }; 365 | } 366 | 367 | function mapDispatchToProps(dispatch) { 368 | return { 369 | removeCurrentProductListing: () => { 370 | dispatch(removeCurrentProductListing()) 371 | } 372 | } 373 | } 374 | 375 | export default connect(mapStateToProps, mapDispatchToProps)(ProductListingForm); 376 | --------------------------------------------------------------------------------