Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
36b45db
project set up
jordanccox Dec 3, 2023
cbfc0f4
further setup
jordanccox Dec 3, 2023
0b430b9
pagination working for products pages
jordanccox Dec 3, 2023
1a611cc
controllers and routes added
jordanccox Dec 3, 2023
c06a8fc
swagger done
jordanccox Dec 4, 2023
a5233ca
add review schema
jordanccox Dec 4, 2023
b4b44c4
fake reviews added
jordanccox Dec 4, 2023
69d3524
figure out how to populate reviews in products for future
jordanccox Dec 4, 2023
d981f19
product schema validation added
jordanccox Dec 4, 2023
2ae7945
updated error response
jordanccox Dec 4, 2023
ce42f53
refactor validation product schema to its own method
jordanccox Dec 4, 2023
95a4d62
todos added
jordanccox Dec 4, 2023
99f3236
post products complete and get products/:productId started
jordanccox Dec 5, 2023
637967e
remove __v from getProductById result
jordanccox Dec 5, 2023
2a1b7fb
deleteProductById route works
jordanccox Dec 5, 2023
078b49a
reviews search working
jordanccox Dec 5, 2023
5ad5ce7
post new review functionality working
jordanccox Dec 5, 2023
d836b2f
all routes working
jordanccox Dec 5, 2023
68b22ba
product filters and sorting works
jordanccox Dec 6, 2023
4a8c1b7
refactor products function
jordanccox Dec 6, 2023
fd23b6e
readme
jordanccox Dec 6, 2023
c5eea6b
clean up code
jordanccox Dec 6, 2023
e94771b
clean up code
jordanccox Dec 6, 2023
77d2b9f
commenting for clarity
jordanccox Dec 6, 2023
d124c48
update commenting
jordanccox Dec 7, 2023
22ba282
updated validations and error handling
jordanccox Dec 7, 2023
a98fa46
cors support added
jordanccox Dec 7, 2023
d65e463
unused code removed
jordanccox Dec 7, 2023
f4aa4e9
update localhost
jordanccox Dec 7, 2023
273e5c8
Revert "update localhost"
jordanccox Dec 7, 2023
70721fb
move to backend folder
jordanccox Dec 7, 2023
82a7280
create frontend folder
jordanccox Dec 7, 2023
dde6087
readme
jordanccox Dec 7, 2023
6dabc44
readme
jordanccox Dec 7, 2023
563842a
updater readme
jordanccox Dec 7, 2023
0e394dd
setup frontend dev environment
jordanccox Dec 7, 2023
e755459
css styling
jordanccox Dec 7, 2023
59dbe60
more css
jordanccox Dec 7, 2023
40ed6f7
css
jordanccox Dec 7, 2023
521b164
css
jordanccox Dec 7, 2023
deb804f
responsive navbar done
jordanccox Dec 8, 2023
edd3b55
dropdown functionality working
jordanccox Dec 8, 2023
323b87d
work on footer
jordanccox Dec 8, 2023
957576d
fix footer breaking nav issue
jordanccox Dec 8, 2023
f91789b
update search bar style
jordanccox Dec 8, 2023
b0eee89
progress on cards
jordanccox Dec 8, 2023
6aeb9b4
product layout complete
jordanccox Dec 9, 2023
006747a
product styled
jordanccox Dec 9, 2023
8c6ce3c
footer design complete
jordanccox Dec 9, 2023
a3f179a
init react-router
jordanccox Dec 9, 2023
f4896b2
basic product fetch works
jordanccox Dec 9, 2023
0099f25
footer positioned correctly
jordanccox Dec 9, 2023
a2d9e60
work on fetch
jordanccox Dec 9, 2023
4611116
add count results functionality
jordanccox Dec 9, 2023
6244dd2
solved results count
jordanccox Dec 9, 2023
07f1ff1
footer links added
jordanccox Dec 9, 2023
ae170a5
pagination working
jordanccox Dec 9, 2023
ad16bcd
styling fixes
jordanccox Dec 9, 2023
ecb6412
update readme
jordanccox Dec 9, 2023
9e9b052
page selection working
jordanccox Dec 10, 2023
2c60eb5
pagination working
jordanccox Dec 10, 2023
1810bc5
categories route
jordanccox Dec 10, 2023
cee3ba7
category search working
jordanccox Dec 10, 2023
74ac109
fix pagination
jordanccox Dec 10, 2023
5b1bd02
application features working
jordanccox Dec 10, 2023
fdcad50
readme update
jordanccox Dec 10, 2023
cd44bfd
update readme
jordanccox Dec 10, 2023
c036918
improve ui
jordanccox Dec 10, 2023
7edd097
clear search added
jordanccox Dec 10, 2023
d89f301
styling categories
jordanccox Dec 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,37 @@
## Product List

This is full stack web app built for an evaluation for [Parsity Coding School](https://parsity.io/).

This project has been created by a student at Parsity, an online software engineering course. The work in this repository is wholly of the student based on a sample starter project that can be accessed by looking at the repository that this project forks.
### About the Backend

If you have any questions about this project or the program in general, visit [parsity.io](https://parsity.io/) or email hello@parsity.io.
The API is built for a mock e-commerce store that sells various "products" -- each product is randomly generated using [Faker](https://fakerjs.dev/).

This project was built with TypeScript, Node.js, Express, and MongoDB.

### About the Frontend

The frontend for this project was built using React. It displays an e-commerce storefront that allows the user to view all the products upon first load and then filter results by category, price, and a custom search query.

### Run API Locally

1. To run locally, you will need to set up a MongoDB database on your computer. This project was created with MongoDB 5.0, but it should be compatible with later versions as well. Follow [this link](https://www.mongodb.com/docs/manual/installation/) for installation instructions on your operating system of choice. Ensure that as a part of your installation, you specify a data directory. Test that your database is working using the [MongoDB Shell](https://www.mongodb.com/docs/mongodb-shell/). Once you know the database is set up correctly, move on to the next step.

2. Fork and clone this repo.

3. `cd` into the repo on your local computer, then `cd` into `backend` and `npm install` to install dependencies.

4. `npm start` to launch the development server on `localhost:8000`.

5. Copy and paste the contents of `swagger/swagger.yaml` into the [Swagger Editor](https://editor.swagger.io/). This is the documentation for the API routes.

IMPORTANT: Before you are able to use any of the routes, you will need to generate fake data. You can do so by navigating first to `localhost:8000/generate-fake-data` then to `localhost:8000/generate-fake-reviews`.

For testing `GET` routes, you can simply type the URL into your web browser -- for example, `localhost:8000/products`. For `POST` and `DELETE` routes, you will need to use a CLI tool like [curl](https://en.wikipedia.org/wiki/CURL) or [download the desktop agent for Postman](https://www.postman.com/downloads/).

### Run the Frontend Locally

1. To run the frontend, first ensure that you have followed the previous steps to get the API working. Launch the API by `cd`ing into the `backend` folder and running `npm start`.

2. Once the server is running, `cd` into the `frontend` folder and launch the frontend server by running `npm run dev`.

3. Navigate to `localhost:5173` in your browser. If everything was set up correctly, you should be able to interact with your product data with the UI.
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
73 changes: 73 additions & 0 deletions backend/controllers/faker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { faker } from "@faker-js/faker";
import Product from "../models/product.js";
import Review from "../models/review.js";
import { Request, Response } from "express";

// Create fake data for API testing

/**
* Creates 90 fake products
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice work on function documentation

* @param req Client request to server
* @param resp Server response to client
* @returns Promise<void>
*/
const createProductData = async (
req: Request,
res: Response
): Promise<void> => {
for (let i = 0; i < 90; i++) {
const product = new Product();

product.category = faker.commerce.department();
product.name = faker.commerce.productName();
product.price = Number(faker.commerce.price());
product.image = "https://via.placeholder.com/250?text=Product+Image";
product.reviews = [];

try {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great job using try catch

await product.save();
} catch (err) {
throw err;
}
}

res.end();
};

/**
* Adds 0 to 5 fake reviews per product in database. Replaces current product.reviews array so that review list will never start out greater than 5 reviews.
* @param req Client request to server
* @param resp Server response to client
* @returns Promise<void>
*/
const createReviews = async (req: Request, res: Response): Promise<void> => {
const productsList = await Product.find();

productsList.forEach(async (product) => {
const randomNumber = Math.floor(Math.random() * 6);

const reviewsList = [];

for (let i = 0; i < randomNumber; i++) {
const review = new Review();

review.userName = faker.internet.userName();
review.text = faker.lorem.paragraph({ min: 1, max: 3 });
review.product = product._id;

const newReview = await review.save();
reviewsList.push(newReview);
}

try {
product.reviews = reviewsList.map((review) => review._id);
await product.save();
} catch (err) {
throw err;
}
});

res.end();
};

export { createProductData, createReviews };
201 changes: 201 additions & 0 deletions backend/controllers/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import Product from "../models/product.js";
import { Request, Response } from "express";
import mongoose from "mongoose";
import {
validateProductSchema,
validateId,
validateQuery,
} from "../models/model_validations.js";
import { AggregationMatch, AggregationSort } from "../types/types.js";

/**
* Retrieves list of products in groups of 9. Optional query, category, and price queries can be used to filter and sort results.
* @param req Client request to server
* @param res Server response to client
* @returns void || Promise<Response>
*/
const getProducts = async (req: Request, res: Response) => {
let page = 0;

// Set page number if included in query params
if (!isNaN(Number(req.query.page))) {
page = Number(req.query.page);
}

const numToSkip = page * 9;

const aggMatch: AggregationMatch = { $match: {} };
const aggSort: AggregationSort = { $sort: {} };

// Filter by category
const category = req.query.category;
if (typeof category === "string") {
const isValidCategory = validateQuery(category);
if (isValidCategory.error) {
return res.status(400).send({
responseStatus: 400,
responseMessage: isValidCategory.error.details[0].message,
});
}
aggMatch.$match.category = new RegExp(category, "gi");
}

// Search term
const search = req.query.query;
if (typeof search === "string") {
const isValidSearch = validateQuery(search);
if (isValidSearch.error) {
return res.status(400).send({
responseStatus: 400,
responseMessage: isValidSearch.error.details[0].message,
});
}
aggMatch.$match.name = new RegExp(search, "gi");
}

// Sort by price
const price = req.query.price;
if (typeof price === "string") {
switch (price) {
case "lowest":
aggSort.$sort.price = 1;
break;
case "highest":
aggSort.$sort.price = -1;
break;
default:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice to see you included a default case

return res.status(400).send({
responseStatus: res.statusCode,
responseMessage:
"The price field must be either 'lowest' or 'highest'",
});
}

const sortedProducts = await Product.aggregate([
aggMatch,
aggSort as any,
{ $unset: "__v" },
])
.skip(numToSkip)
.limit(9);
const count = await Product.countDocuments(aggMatch.$match);

return res.status(200).send({
responseStatus: res.statusCode,
responseMessage: sortedProducts,
resultsFound: count,
});
}

// Send products unsorted by default
const filteredProducts = await Product.aggregate([
aggMatch,
{ $unset: "__v" },
])
.skip(numToSkip)
.limit(9);
const count = await Product.countDocuments(aggMatch.$match);
res.status(200).send({
responseStatus: res.statusCode,
responseMessage: filteredProducts,
resultsFound: count,
});
};

/**
* Creates a new product and adds it to the database
* @param req Client request to server
* @param res Server response to client
* @returns Promise<Response> || void
*/
const createNewProduct = async (req: Request, res: Response) => {
const validationResult = validateProductSchema(req);

if (validationResult.error) {
return res.status(400).send({
responseStatus: res.statusCode,
responseMessage: validationResult.error.details[0].message,
});
}

const product = new Product(req.body);

const newProduct = (await product.save()) as mongoose.Document;

// Clone product to remove version key __v from response
const { __v, ...otherProps } = newProduct.toObject();
const productClone = { ...otherProps };

res.status(200).send({
responseStatus: res.statusCode,
responseMessage: productClone,
});
};

/**
* Retrieves a single product by its id
* @param req Client request to server
* @param res Server response to client
* @returns Promise<Response> || void
*/
const getProductById = async (req: Request, res: Response) => {
const id = req.params.productId;

const validationResult = validateId(id);

if (validationResult.error) {
return res.status(400).send({
responseStatus: res.statusCode,
responseMessage: validationResult.error.details[0].message,
});
}

const product = await Product.findById(id, { __v: 0 });

if (!product) {
return res.status(404).send({
responseStatus: res.statusCode,
responseMessage: "No product found matching id",
});
}

res.status(200).send({
responseStatus: res.statusCode,
responseMessage: product,
});
};

/**
* Deletes a single product by its id
* @param req Client request to server
* @param res Server response to client
* @returns Promise<Response> || void
*/
const deleteProductById = async (req: Request, res: Response) => {
const id = req.params.productId;

const validationResult = validateId(id);

if (validationResult.error) {
return res.status(400).send({
responseStatus: res.statusCode,
responseMessage: validationResult.error.details[0].message,
});
}

const deletedProduct = await Product.findByIdAndDelete(id);

if (!deletedProduct) {
return res.status(404).send({
responseStatus: res.statusCode,
responseMessage: "No product found matching id",
});
}

res.status(200).send({
responseStatus: res.statusCode,
responseMessage: deletedProduct,
});
};

export { getProducts, createNewProduct, getProductById, deleteProductById };
Loading