├── 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 |
16 | You need to enable JavaScript to run this app.
17 |
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 | props.selectProductListing(props.productListing)}>
19 | View Listing
20 |
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 | {
14 | props.updateSortByOption(event.target.value);
15 | }}
16 | >
17 | Relevance
18 | Recent
19 | Price: Low to High
20 | Price: High to Low
21 |
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 | Log out
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 |
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 |
80 |
91 | Login
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 | 
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 | [](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 |
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 |
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 |
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 | Sign Up
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 |
this.removeCurrentProductListing()}>Back to Product Listings
125 | {/*{
126 | this.props.currentProductListing.user_id === Number(adapter.getUserId()) && !this.props.currentProductListing.isSold ?
127 |
Edit Product Listing
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 | Purchase
173 | :
174 | null
175 | }
176 | {
177 | this.state.purchaseOption === "Offer" ?
178 |
179 |
180 | Make an offer
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 |
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 | Create Listing
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 |
--------------------------------------------------------------------------------