A place for shave enthusiasts to come together and create a community. There is currently a market for older and younger gentlemen alike in proper shaving. The use of different razors, lathers, aftershaves, etc... Unfortunately, there currently is no convenient website or application which allows users to keep track of what they use. With ShaveReducer, those needs (and much more) are solved.
Here, users can add various shaving products and seperate them into catgeories. We also add other useful features, such as the shave history page, which allows users to keep a log of products used for a shave and leave comments and ratings for further use. These shaves can be shared with the community of users as well.
[ return to top ]
I used heroku to deploy this application: https://hetokibo-shaving-client.herokuapp.com/
The backend can be found here: https://github.com/thinkful-ei22/shaving-tracker-server
Username: beardy
Password: wicked
[ return to top ]
This app was created using React for the front-end, and Node for the back-end.
Front-end: React was used to build the client side of this application, with Redux used for state-management. React stateful components are used to manage state for each indiviual component. React-router is used to route and connect the various components together. A react-tabs libray is used to create tabs which is used to filter the products viewed on our collection page. The react-modal libaray is used to create a modal when adding products and shaves. PapaParse is another technology used to parse data from a .csv file.
Back-end: Node.js was used to create the server side of this application. Mongo was used to manage the database, with mlab to host the data. The express middleware was used to help modularize the server side. Some testing was done using mocha and chai.
[ return to top ]
{
username: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
email: {
type: String,
validate: [validateEmail,'Validation of `{PATH}` failed with value `{VALUE}`']
}
}
function validateEmail(email) {
const re = new RegExp('[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}', 'i');
if (!email) {
return true;
}
return re.test(email);
}
{
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
comment: String,
nickname: String,
imageUrl: String,
totalUsage: { type: Number, default: 0 },
currentUsage: { type: Number, default: 0 },
}
{
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
razorId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserProduct' },
bladeId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserProduct' },
brushId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserProduct' },
latherId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserProduct' },
aftershaveId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserProduct' },
additionalCareId: { type: mongoose.Schema.Types.ObjectId, ref: 'UserProduct' },
rating: Number,
date: { type: Date, required: true },
imageUrl: String,
share: { type: Boolean, default: false },
comments: String,
}
{
subtype: String,
productType: { type: String, enum: ['razor', 'blade', 'brush', 'lather', 'aftershave', 'additionalcare'] },
brand: String,
model: String,
}
[ return to top ]
Used for creating new accounts.
Purpose: New user registration
Example: POST https://example.com/api/v1/users
Request body:
{
"username": "JoeUser",
"email": "joe@example.com",
"password": "correct-horse-battery-staple"
}
Response header:
{
"status": 201,
"location": "/api/v1/users/<NEW_USER_ID>"
}
Response body:
{
"message": "Registration Success"
}
[ more examples ] [ return to top ]
Used for logging in.
Purpose: Trade user credentials for a JWT
Example: POST https://example.com/api/v1/login
Request body:
{
"username": "JoeUser",
"password": "correct-horse-battery-staple"
}
Response body:
{
"authToken": "VALID_JWT"
}
[ more examples ] [ return to top ]
Purpose: refresh a user's JWT
Example: POST http://example.com/api/v1/refresh
Request header:
"Authorization": "Bearer AUTH_TOKEN"
Request body:
{}
Response body:
{
"authToken": "UPDATED_JWT"
}
[ more examples ] [ return to top ]
Used to manage products in the DB, modifying both the user's personal collection of products and the global product collection.
All /products
endpoints are protected and require a valid JWT that includes the userId
.
Request header:
"Authorization": "Bearer AUTH_TOKEN"
Purpose: Gets the list of products that the user has added to their personal product collection. All products of all types will be objects in the same array.
Example: GET http://example.com/api/v1/products/personal
Request body:
{}
Response body:
[
{
"nickname": "Muhle Tortoise",
"comment": "closed comb",
"imageUrl": "null",
"subtype": "Double Edge",
"productType": "razor",
"brand": "Muhle",
"model": "R108 Tortoise Shell",
"id": "444444444444444444444002",
"productId": "222222222222222222222006"
},
{
"nickname": "Gillette Wilkinson",
"comment": "",
"imageUrl": "https://i.ebayimg.com/images/g/KGsAAOSwikdawiMp/s-l640.jpg",
"subtype": null,
"productType": "blade",
"brand": "Gillette",
"model": "Wilkinson",
"id": "444444444444444444444003",
"productId": "222222222222222222222007"
}
]
[ more examples ] [ return to top ]
Purpose: Adds a new item to the user's personal product collection. Has a dynamic between the user's personal products and the global products.
If the product exists in global, simply make a reference to that item. If the product does not exist in global, create that item in global and then reference it.
If the product already exists in the user's personal products, return 400: "Item already exists"
Example: POST http://example.com/api/v1/products/personal
Request body:
{
"brand": "Gillette",
"model": "Fore 20 Bladed",
"productType": "razor",
"subtype": "cartridge",
"nickname": "The Overkill",
"comments": "Honestly this thing kind of scares me a little"
}
Response body:
{
"nickname": "The Overkill",
"subtype": "cartridge",
"productType": "razor",
"brand": "Gillette",
"model": "Fore 20 Bladed",
"id": "5bbbc3d668b9440dfc77605b",
"productId": "5bbbc3d668b9440dfc77605a"
}
[ more examples ] [ return to top ]
Purpose: Updates personal content of a product in user's product collection
Example: PUT http://example.com/api/v1/products/personal/abcdefghijklmnopqrstuvwx
Request body:
{
"nickname": "The Cyclops",
"comment": "MY EYES"
}
Response body:
{
"status": 200,
"id": "fwwefsk3423",
"nickname": "The Cyclops",
"comment": "MY EYE"
}
[ more examples ] [ return to top ]
Purpose: Removes a product from someone's personal collection
Example: DELETE http://example.com/api/v1/products/personal/abcdefghijklmnopqrstuvwx
Request body:
{}
Response body:
{
"status": 204
}
[ more examples ] [ return to top ]
Used to manage a user's shave events.
All /shaves
endpoints are protected and require a valid JWT that includes the userId
.
Request header:
"Authorization": "Bearer AUTH_TOKEN"
Purpose: Gets a list of the user's shave history, including populated and flattened data detailing each product used for a given shave
Example: GET http://example.com/api/v1/shaves
Request body:
{}
Response body:
[
{
"razor": {
"nickname": "Gillette Tech Travel",
"comment": "1964 vintage",
"imageUrl": "https://farm3.static.flickr.com/2643/4052280439_cd56947ff6.jpg",
"subtype": "Double Edge",
"productType": "razor",
"brand": "Gillette",
"model": "Tech Travel",
"id": "444444444444444444444000",
"productId": "222222222222222222222000"
},
"blade": {
"nickname": "Gillette Wilkinson",
"comment": "",
"imageUrl": "https://i.ebayimg.com/images/g/KGsAAOSwikdawiMp/s-l640.jpg",
"subtype": null,
"productType": "blade",
"brand": "Gillette",
"model": "Wilkinson",
"id": "444444444444444444444003",
"productId": "222222222222222222222007"
},
"brush": {
"nickname": "Surrey 34014 Deluxe",
"comment": "",
"imageUrl": "https://i.ebayimg.com/images/g/50MAAOSwQYZWtt3y/s-l300.jpg",
"subtype": "Boar",
"productType": "brush",
"brand": "Surrey",
"model": "34014 Deluxe",
"id": "444444444444444444444007",
"productId": "222222222222222222222013"
},
"lather": {
"nickname": "Stirling's Barbershop",
"comment": "",
"imageUrl": "https://cdn.shopify.com/s/files/1/2398/1395/products/stirling-soap-company-shave-soap-barbershop_576x.jpg?v=1533596461",
"subtype": "Soap",
"productType": "lather",
"brand": "Stirling Soap Company",
"model": "Barbershop",
"id": "444444444444444444444009",
"productId": "222222222222222222222019"
},
"aftershave": {
"nickname": "Brut Classic",
"comment": "",
"imageUrl": "https://images-na.ssl-images-amazon.com/images/I/51MifkbnbuL._SY550_.jpg",
"subtype": "Splash",
"productType": "aftershave",
"brand": "Brut",
"model": "Classic",
"id": "444444444444444444444012",
"productId": "222222222222222222222029"
},
"additionalCare": {
"nickname": "Proraso Green",
"comment": "",
"imageUrl": "https://sharpologist.com/wp-content/uploads/2012/04/IMG_18641.jpg",
"subtype": null,
"productType": "additionalcare",
"brand": "Proraso",
"model": "Green",
"id": "444444444444444444444014",
"productId": "222222222222222222222035"
},
"id": "555555555555555555555000",
"date": "2018-09-21T00:00:00.000Z",
"share": true,
"rating": 4,
"imageUrl": ""
},
]
[ more examples ] [ return to top ]
Purpose: Allows user to record a new shave event.
Example: POST http://example.com/api/v1/shaves
Request body:
{
"additionalCareId": null,
"aftershaveId": "444444444444444444444012",
"bladeId": "444444444444444444444006",
"brushId": "444444444444444444444008",
"comments": "I shaved my face!",
"date": "2018-10-08",
"imageUrl": null,
"latherId": "444444444444444444444009",
"rating": "3",
"razorId": "444444444444444444444000",
"share": true
}
Response body:
{
"razor": {
"nickname": "Gillette Tech Travel",
"comment": "1964 vintage",
"imageUrl": "https://farm3.static.flickr.com/2643/4052280439_cd56947ff6.jpg",
"subtype": "Double Edge",
"productType": "razor",
"brand": "Gillette",
"model": "Tech Travel",
"id": "444444444444444444444000",
"productId": "222222222222222222222000"
},
"blade": {
"nickname": "Gillette 7 O'Clock",
"comment": "",
"imageUrl": "http://cdn.shopify.com/s/files/1/1690/0287/products/gillette-7-o-clock-super-stainless-double-edge-blades_grande.jpg?v=1498227446",
"subtype": null,
"productType": "blade",
"brand": "Gillette",
"model": "7 O'Clock SharpEdge",
"id": "444444444444444444444006",
"productId": "222222222222222222222011"
},
"brush": {
"nickname": "TSC Admiral Blue 24mm",
"comment": "24mm",
"imageUrl": "https://cdn.shopify.com/s/files/1/1448/2702/products/Turtleshipmay34_grande.jpg?v=1496094806",
"subtype": "Synthetic",
"productType": "brush",
"brand": "Turtleship Shave Co",
"model": "Admiral Blue",
"id": "444444444444444444444008",
"productId": "222222222222222222222016"
},
"lather": {
"nickname": "Stirling's Barbershop",
"comment": "",
"imageUrl": "https://cdn.shopify.com/s/files/1/2398/1395/products/stirling-soap-company-shave-soap-barbershop_576x.jpg?v=1533596461",
"subtype": "Soap",
"productType": "lather",
"brand": "Stirling Soap Company",
"model": "Barbershop",
"id": "444444444444444444444009",
"productId": "222222222222222222222019"
},
"aftershave": {
"nickname": "Brut Classic",
"comment": "",
"imageUrl": "https://images-na.ssl-images-amazon.com/images/I/51MifkbnbuL._SY550_.jpg",
"subtype": "Splash",
"productType": "aftershave",
"brand": "Brut",
"model": "Classic",
"id": "444444444444444444444012",
"productId": "222222222222222222222029"
},
"additionalCare": null,
"id": "5bbbc90a68b9440dfc77605d",
"date": "2018-10-08T00:00:00.000Z",
"share": true,
"rating": 3,
"imageUrl": null,
"comments": "I shaved my face!"
}
[ more examples ] [ return to top ]
Purpose: Updates info of a given shave
Example: PUT http://example.com/api/v1/shaves/abcdefghijklmnopqrstuvwx
Request body:
{
"share": false,
"date": "2018-10-06",
"brushId": "444444444444444444444007"
}
Response body:
{
"razor": {
"nickname": "Gillette Tech Travel",
"comment": "1964 vintage",
"imageUrl": "https://farm3.static.flickr.com/2643/4052280439_cd56947ff6.jpg",
"subtype": "Double Edge",
"productType": "razor",
"brand": "Gillette",
"model": "Tech Travel",
"id": "444444444444444444444000",
"productId": "222222222222222222222000"
},
"blade": {
"nickname": "Gillette 7 O'Clock",
"comment": "",
"imageUrl": "http://cdn.shopify.com/s/files/1/1690/0287/products/gillette-7-o-clock-super-stainless-double-edge-blades_grande.jpg?v=1498227446",
"subtype": null,
"productType": "blade",
"brand": "Gillette",
"model": "7 O'Clock SharpEdge",
"id": "444444444444444444444006",
"productId": "222222222222222222222011"
},
"brush": {
"nickname": "Surrey 34014 Deluxe",
"comment": "",
"imageUrl": "https://i.ebayimg.com/images/g/50MAAOSwQYZWtt3y/s-l300.jpg",
"subtype": "Boar",
"productType": "brush",
"brand": "Surrey",
"model": "34014 Deluxe",
"id": "444444444444444444444007",
"productId": "222222222222222222222013"
},
"lather": {
"nickname": "Stirling's Barbershop",
"comment": "",
"imageUrl": "https://cdn.shopify.com/s/files/1/2398/1395/products/stirling-soap-company-shave-soap-barbershop_576x.jpg?v=1533596461",
"subtype": "Soap",
"productType": "lather",
"brand": "Stirling Soap Company",
"model": "Barbershop",
"id": "444444444444444444444009",
"productId": "222222222222222222222019"
},
"aftershave": {
"nickname": "Brut Classic",
"comment": "",
"imageUrl": "https://images-na.ssl-images-amazon.com/images/I/51MifkbnbuL._SY550_.jpg",
"subtype": "Splash",
"productType": "aftershave",
"brand": "Brut",
"model": "Classic",
"id": "444444444444444444444012",
"productId": "222222222222222222222029"
},
"additionalCare": null,
"id": "5bbbccd168b9440dfc77605e",
"date": "2018-10-06T00:00:00.000Z",
"rating": 3,
"share": false,
"comments": "I shaved my face!"
}
[ more examples ] [ return to top ]
Purpose: Deletes a given shave by id
Example: DELETE http://example.com/api/v1/products/personal/abcdefghijklmnopqrstuvwx
Request body:
{}
Response body:
{
"status": 204
}
[ more examples ] [ return to top ]
Used to fetch publicly-shared information from other users.
All /shaves
endpoints are protected and require a valid JWT that includes the userId
.
Request header:
"Authorization": "Bearer AUTH_TOKEN"
Purpose: Gets publicly-shared shaves from other users between the specified dates (:start and :end)
Example: GET http://example.com/api/v1/community/shaves/2018-09-20/2018-09-25
Request body:
{}
Response body:
[
{
"username": "smooth",
"razor": {
"nickname": "ATT Calypso R1",
"comment": "",
"imageUrl": "https://i.imgur.com/UWfTc2q.jpg",
"subtype": "Double Edge",
"productType": "razor",
"brand": "Above the Tie",
"model": "Calypso R1",
"id": "444444444444444444444015",
"productId": "222222222222222222222001"
},
"blade": {
"nickname": "GSB",
"comment": "",
"imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRfZ_w0Yb677ONBvjHw2J2bQukHADOIOApxv1mlUvKJbaJezuga",
"subtype": null,
"productType": "blade",
"brand": "Gillette",
"model": "Silver Blue",
"id": "444444444444444444444018",
"productId": "222222222222222222222008"
},
"brush": {
"nickname": "Brushbutt LE",
"comment": "Limited Edition #78/100",
"imageUrl": "https://i.imgur.com/m05rFOz.jpg",
"subtype": "Boar",
"productType": "brush",
"brand": "Semogue",
"model": "Brushbutt 22mm",
"id": "444444444444444444444022",
"productId": "222222222222222222222014"
},
"lather": {
"nickname": "B&M Lavanille",
"comment": "tre Citta Line",
"imageUrl": "https://cdn.shopify.com/s/files/1/0392/1301/products/bmlavanillesoap_grande.jpg?v=1533956837",
"subtype": "Soap",
"productType": "lather",
"brand": "Barrister and Mann",
"model": "Lavanille",
"id": "444444444444444444444024",
"productId": "222222222222222222222022"
},
"aftershave": {
"nickname": "Lavanille Splash",
"comment": "",
"imageUrl": "https://cdn.shopify.com/s/files/1/0648/7651/products/LAVANILLE_AFTERSHAVE_SQUARE_70c03830-aa45-4a98-b5b7-e6f593bc688d_2048x2048.jpg?v=1515681770",
"subtype": "Splash",
"productType": "aftershave",
"brand": "Barrister and Mann",
"model": "Lavanille",
"id": "444444444444444444444027",
"productId": "222222222222222222222031"
},
"additionalCare": {
"nickname": "Thayers Witch Hazel",
"comment": "",
"imageUrl": "https://images-na.ssl-images-amazon.com/images/I/51ZxG8xVzAL._SX355_.jpg",
"subtype": null,
"productType": "additionalcare",
"brand": "Thayers",
"model": "Witch Hazel",
"id": "444444444444444444444029",
"productId": "222222222222222222222036"
},
"id": "555555555555555555555002",
"date": "2018-09-21T00:00:00.000Z",
"rating": 4
},
]
[ more examples ] [ return to top ]
Used for image uploads and management.
Purpose: Uploads an image to Cloudinary and returns a usable url.
The image must be parsed as FormData on the front end before being sent to this endpoint. Cloudinary account details must be established in the server environment.
Example: POST http://example.com/api/v1/image/upload
Request files (req.files):
{
"file": {
"fieldName": "file",
"originalFilename": "S1wmzSf.jpg",
"path": "C:\\Users\\devin\\AppData\\Local\\Temp\\rJutRWeZ9VVxPC0nuZrZK5-Y.jpg",
"headers": {
"content-disposition": "form-data; name='file'; filename='S1wmzSf.jpg'",
"content-type": "image/jpeg"
},
"size": 134464,
"name": "S1wmzSf.jpg",
"type": "image/jpeg"
}
}
Request body:
{}
Response body:
{
"public_id": "aktfmak772fpviwfharj",
"version": 1539037170,
"signature": "33784ff3a4a38305dfacb91e296ef239614bd6b8",
"width": 787,
"height": 270,
"format": "jpg",
"resource_type": "image",
"created_at": "2018-10-08T22:19:30Z",
"tags": [],
"bytes": 54718,
"type": "upload",
"etag": "e6e593a96a8ba9d5266819d9642bccbb",
"placeholder": false,
"url": "http://res.cloudinary.com/wickedbeardy/image/upload/v1539037170/aktfmak772fpviwfharj.jpg",
"secure_url": "https://res.cloudinary.com/wickedbeardy/image/upload/v1539037170/aktfmak772fpviwfharj.jpg",
"original_filename": "HC-ml86CalWbfYgDLr0_50oz"
}
[ more examples ] [ return to top ]
This application has several components. A few of the main components that are responsible for our features are: CSV-Products, My-collection, Shave-history , and Product-form/Shave-form.
All of the components live in the src/components/ directory.
CSV-Products: The job of this component is to handle the the upload, reading, and parsing of the data from .csv files. This data is used to add a multitude of products onto our Collections page.
My-collection: My-collection is responsible for handling the fetched data from the database and displaying it appropriately. It filters the different kinds of shaving products by the use of tabs.
Shave-history: Our Shave-history component handles the creation and displaying of the shave history of users. A shave history consists of the shaving products a user may use for his or her shave. The user may also leave a rating and comment to look back on in the future. These shaves may be shared with the "community" of users of the page. Users may also filter their shave's by the date of the shave.
Product-form/Shave-form: These two components handle the modal that displays when adding (or editing) products and shaves.
[ return to top ]