From a3af5f17c9b28bfa417470154073505b504f8a06 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Mon, 4 Dec 2023 22:49:44 +0100 Subject: [PATCH 01/33] create model and the first endpoint --- backend/config/db.js | 0 backend/models/advertiser.js | 35 +++++++++++++++ backend/package.json | 7 +++ backend/routes/advertiserRoutes.js | 68 ++++++++++++++++++++++++++++++ backend/server.js | 7 +-- 5 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 backend/config/db.js create mode 100644 backend/models/advertiser.js create mode 100644 backend/routes/advertiserRoutes.js diff --git a/backend/config/db.js b/backend/config/db.js new file mode 100644 index 000000000..e69de29bb diff --git a/backend/models/advertiser.js b/backend/models/advertiser.js new file mode 100644 index 000000000..54dc61e37 --- /dev/null +++ b/backend/models/advertiser.js @@ -0,0 +1,35 @@ +import mongoose from "mongoose"; +import crypto from "crypto"; + +const { Schema } = mongoose; + +export const advertiserSchema = new Schema( + { + username: { + type: String, + unique: true, + required: true, + minlength: 5 + }, + email: { + type: String, + unique: true, + required: true + }, + password: { + type: String, + unique: true, // does password need this? + required: true, + minlength: 6 + }, + accessToken: { + type: String, + default: () => crypto.randomBytes(128).toString("hex") + } + }, + { + timestamps: true // if this is used, mongoose creates both createdAt and updatedAt + } +); + +export const AdvertiserModel = mongoose.model("advertiser", advertiserSchema); \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index 8de5c4ce0..26e4398f2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,8 +12,15 @@ "@babel/core": "^7.17.9", "@babel/node": "^7.16.8", "@babel/preset-env": "^7.16.11", + "bcrypt": "^5.1.1", + "bcrypt-nodejs": "^0.0.3", "cors": "^2.8.5", + "dotenv": "^16.3.1", "express": "^4.17.3", + "express-async-handler": "^1.2.0", + "express-list-endpoints": "^6.0.0", + "jsonwebtoken": "^9.0.2", + "mongodb": "^6.3.0", "mongoose": "^8.0.0", "nodemon": "^3.0.1" } diff --git a/backend/routes/advertiserRoutes.js b/backend/routes/advertiserRoutes.js new file mode 100644 index 000000000..5df0d0d70 --- /dev/null +++ b/backend/routes/advertiserRoutes.js @@ -0,0 +1,68 @@ +import express from "express"; +import { AdvertiserModel } from "../models/advertiser"; +import listEndpoints from "express-list-endpoints"; +import asyncHandler from "express-async-handler"; +import { jwt } from "jsonwebtoken"; + +const router = express.Router(); + +const generateToken = (advertisement) => { + return jwt.sign({ id: advertiser._id}, process.env.JWT_SECRET, { + expires: "24h" + }); +} +// Endpoint to show documentation of all endpoints +router.get( + "/", + asyncHandler(async (req, res) => { + const endpoints = listEndpoints(router); + res.json(endpoints); + }) +); + +// USER REGISTRATION ROUTE +router.post( + "/register", + asyncHandler(async (req, res) => { + const { username, email, password } = req.body; + try { + if (!username || !email || !password) { + res.status(400).json({message: "Please add all fields", error: err.errors}); + } else { + const existingUser = AdvertiserModel.findOne({ + $or: [{ username }, { email }] + }); + + if (existingUser) { + res.status(400).json({message: `User with ${existingUser.username = username ? "username" : "email"} already exists`}) + }; + + const salt = bcrypt.genSaltSync(10); + const hashedPassword = bcrypt.hashSync(password, salt); + + const newAdvertiser = new AdvertiserModel({ + username, + email, + password: hashedPassword + }); + + await newAdvertiser.save(); + + res.status(201).json ({ + success: true, + response: { + username: newAdvertiser.username, + email: newAdvertiser.email, + id: newAdvertiser._id, + accessToken: generateToken(newAdvertiser._id) + } + }); + }; + } catch (err) { + res.status(500).json({ success: false, response: err.message }); + } + }) +) + + +export default router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 2d7ae8aa1..7b9d74270 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,9 @@ import express from "express"; import cors from "cors"; import mongoose from "mongoose"; +import dotenv from "dotenv"; +dotenv.config(); +import advertiserRoutes from "./routes/advertiserRoutes"; const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); @@ -17,9 +20,7 @@ app.use(cors()); app.use(express.json()); // Start defining your routes here -app.get("/", (req, res) => { - res.send("Hello Technigo!"); -}); +app.use("/", advertiserRoutes); // Start the server app.listen(port, () => { From 653760da1bc17582e762f837ca9c70689f8bbd9d Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Tue, 5 Dec 2023 22:19:37 +0100 Subject: [PATCH 02/33] add endpoint to find all users --- backend/routes/advertiserRoutes.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/backend/routes/advertiserRoutes.js b/backend/routes/advertiserRoutes.js index 5df0d0d70..d3010b459 100644 --- a/backend/routes/advertiserRoutes.js +++ b/backend/routes/advertiserRoutes.js @@ -1,16 +1,19 @@ import express from "express"; import { AdvertiserModel } from "../models/advertiser"; +import bcrypt from "bcrypt"; import listEndpoints from "express-list-endpoints"; import asyncHandler from "express-async-handler"; import { jwt } from "jsonwebtoken"; const router = express.Router(); -const generateToken = (advertisement) => { +// Generate a JWT token containing the user's unique ID, with an optional secret key and a 24-hour expiration time +const generateToken = (advertiser) => { return jwt.sign({ id: advertiser._id}, process.env.JWT_SECRET, { expires: "24h" }); -} +}; + // Endpoint to show documentation of all endpoints router.get( "/", @@ -20,16 +23,24 @@ router.get( }) ); +router.get( + "/users", + asyncHandler(async (req, res) => { + const users = await AdvertiserModel.find(); + res.status(200).json(users); + }) +); + // USER REGISTRATION ROUTE router.post( "/register", - asyncHandler(async (req, res) => { + asyncHandler(async (req, res, err) => { const { username, email, password } = req.body; try { if (!username || !email || !password) { res.status(400).json({message: "Please add all fields", error: err.errors}); } else { - const existingUser = AdvertiserModel.findOne({ + const existingUser = await AdvertiserModel.findOne({ $or: [{ username }, { email }] }); From ea51a8d5637574565046dd518de822c3792641f4 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Tue, 5 Dec 2023 23:06:06 +0100 Subject: [PATCH 03/33] add endpoint for sign in --- backend/models/advertiser.js | 1 - backend/routes/advertiserRoutes.js | 37 ++++++++++++++++++++++++++++-- package.json | 7 ------ 3 files changed, 35 insertions(+), 10 deletions(-) delete mode 100644 package.json diff --git a/backend/models/advertiser.js b/backend/models/advertiser.js index 54dc61e37..e76d313c3 100644 --- a/backend/models/advertiser.js +++ b/backend/models/advertiser.js @@ -18,7 +18,6 @@ export const advertiserSchema = new Schema( }, password: { type: String, - unique: true, // does password need this? required: true, minlength: 6 }, diff --git a/backend/routes/advertiserRoutes.js b/backend/routes/advertiserRoutes.js index d3010b459..4487c1143 100644 --- a/backend/routes/advertiserRoutes.js +++ b/backend/routes/advertiserRoutes.js @@ -10,7 +10,7 @@ const router = express.Router(); // Generate a JWT token containing the user's unique ID, with an optional secret key and a 24-hour expiration time const generateToken = (advertiser) => { return jwt.sign({ id: advertiser._id}, process.env.JWT_SECRET, { - expires: "24h" + expiresIn: "24h" }); }; @@ -23,6 +23,7 @@ router.get( }) ); +// Endpoint to show all users router.get( "/users", asyncHandler(async (req, res) => { @@ -73,7 +74,39 @@ router.post( res.status(500).json({ success: false, response: err.message }); } }) -) +); + +// USER LOG-IN ROUTE +router.post( + "/signin", + asyncHandler(async (req, res) => { + // Retrieve username and password from req.body + const { username, password } = req.body; + + // Find a user with the provided username in the database + try { + const user = await AdvertiserModel.findOne({ username }); + if (!user) { + // If no user is found with the provided username, return status 401 Unauthorized and message "User not found" + res.status(401).json({ success: false, response: "User not found"}); + }; + // If a user is found with the provided username, compare the provided password with the hashed password in the database + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + res.status(401).json({ success: false, response: "Incorrect password"}); + }; + + res.status(200).json({ success: true, response: { + username: user.username, + id: user._id, + accessToken: generateToken(user._id) + }}); + + } catch (err) { + res.status(500).json({ success: false, response: err.message}); + }; + }) +); export default router; \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index d774b8cc3..000000000 --- a/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "project-auth-parent", - "version": "1.0.0", - "scripts": { - "postinstall": "npm install --prefix backend" - } -} From 956aba4a4bc45d355aec2eff67367d38a15c1a93 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Wed, 6 Dec 2023 15:49:03 +0100 Subject: [PATCH 04/33] create controllers for user --- backend/controllers/advertiserControllers.js | 85 +++++++++++++++++ backend/models/advert.js | 36 ++++++++ backend/models/advertiser.js | 2 +- backend/routes/advertRoutes.js | 1 + backend/routes/advertiserRoutes.js | 96 +++----------------- 5 files changed, 136 insertions(+), 84 deletions(-) create mode 100644 backend/controllers/advertiserControllers.js create mode 100644 backend/models/advert.js create mode 100644 backend/routes/advertRoutes.js diff --git a/backend/controllers/advertiserControllers.js b/backend/controllers/advertiserControllers.js new file mode 100644 index 000000000..c2d5562e6 --- /dev/null +++ b/backend/controllers/advertiserControllers.js @@ -0,0 +1,85 @@ +import { AdvertiserModel } from "../models/advertiser"; +import asyncHandler from "express-async-handler"; +import bcrypt from "bcrypt"; +import jwt from "jsonwebtoken"; + +// Generate a JWT token containing the user's unique ID, with an optional secret key and a 24-hour expiration time +const generateToken = (advertiser) => { + return jwt.sign({ id: advertiser._id}, process.env.JWT_SECRET, { + expiresIn: "24h" + }); +}; + +export const showAllUsersController = asyncHandler(async (req, res) => { + const users = await AdvertiserModel.find(); + res.status(200).json(users); +}); + +export const registerUserController = asyncHandler(async (req, res) => { + const { username, email, password } = req.body; + try { + if (!username || !email || !password) { + res.status(400).json({message: "Please add all fields", error: err.errors}); + } else { + const existingUser = await AdvertiserModel.findOne({ + $or: [{ username }, { email }] + }); + + if (existingUser) { + res.status(400).json({message: `User with ${existingUser.username = username ? "username" : "email"} already exists`}) + }; + + const salt = bcrypt.genSaltSync(10); + const hashedPassword = bcrypt.hashSync(password, salt); + + const newAdvertiser = new AdvertiserModel({ + username, + email, + password: hashedPassword + }); + + await newAdvertiser.save(); + + res.status(201).json ({ + success: true, + response: { + username: newAdvertiser.username, + email: newAdvertiser.email, + id: newAdvertiser._id, + accessToken: generateToken(newAdvertiser._id) + } + }); + }; + } catch (err) { + res.status(500).json({ success: false, response: err.message }); + } +}); + +export const signinUserController = asyncHandler(async (req, res) => { + // Retrieve username and password from req.body + const { username, password } = req.body; + + // Find a user with the provided username in the database + try { + const user = await AdvertiserModel.findOne({ username }); + if (!user) { + // If no user is found with the provided username, return status 401 Unauthorized and message "User not found" + res.status(401).json({ success: false, response: "User not found"}); + }; + + // If a user is found with the provided username, compare the provided password with the hashed password in the database + const isMatch = await bcrypt.compare(password, user.password); + if (!isMatch) { + res.status(401).json({ success: false, response: "Incorrect password"}); + }; + + res.status(200).json({ success: true, response: { + username: user.username, + id: user._id, + accessToken: generateToken(user._id) + }}); + + } catch (err) { + res.status(500).json({ success: false, response: err.message}); + }; +}) \ No newline at end of file diff --git a/backend/models/advert.js b/backend/models/advert.js new file mode 100644 index 000000000..9d51b7f61 --- /dev/null +++ b/backend/models/advert.js @@ -0,0 +1,36 @@ +import mongoose from "mongoose"; + +const { Schema } = mongoose; + +export const advertSchema = new Schema({ + product: { + type: String, + required: true + }, + amount: { + type: Number, + required: true, + default: 0 + }, + unit: { + type: String, + required: true + }, + address: { + type: String, + required: true + }, + pickupTime: { + type: Date, + default: Date.now + }, + advertiser: { + type: mongoose.Schema.Types.ObjectId, + ref: "Advertiser" + } +}, +{ + timestamps: true, +}); + +export const AdvertModel = mongoose.model("Advert", advertSchema); \ No newline at end of file diff --git a/backend/models/advertiser.js b/backend/models/advertiser.js index e76d313c3..4d05e921c 100644 --- a/backend/models/advertiser.js +++ b/backend/models/advertiser.js @@ -31,4 +31,4 @@ export const advertiserSchema = new Schema( } ); -export const AdvertiserModel = mongoose.model("advertiser", advertiserSchema); \ No newline at end of file +export const AdvertiserModel = mongoose.model("Advertiser", advertiserSchema); \ No newline at end of file diff --git a/backend/routes/advertRoutes.js b/backend/routes/advertRoutes.js new file mode 100644 index 000000000..9e4142ab2 --- /dev/null +++ b/backend/routes/advertRoutes.js @@ -0,0 +1 @@ +// An authenticated endpoint which only returns content if the Authorization header with the user's token was correct \ No newline at end of file diff --git a/backend/routes/advertiserRoutes.js b/backend/routes/advertiserRoutes.js index 4487c1143..23115aff0 100644 --- a/backend/routes/advertiserRoutes.js +++ b/backend/routes/advertiserRoutes.js @@ -1,19 +1,14 @@ import express from "express"; -import { AdvertiserModel } from "../models/advertiser"; -import bcrypt from "bcrypt"; -import listEndpoints from "express-list-endpoints"; import asyncHandler from "express-async-handler"; -import { jwt } from "jsonwebtoken"; +import listEndpoints from "express-list-endpoints"; +import { + registerUserController, + showAllUsersController, + signinUserController +} from "../controllers/advertiserControllers"; const router = express.Router(); -// Generate a JWT token containing the user's unique ID, with an optional secret key and a 24-hour expiration time -const generateToken = (advertiser) => { - return jwt.sign({ id: advertiser._id}, process.env.JWT_SECRET, { - expiresIn: "24h" - }); -}; - // Endpoint to show documentation of all endpoints router.get( "/", @@ -26,87 +21,22 @@ router.get( // Endpoint to show all users router.get( "/users", - asyncHandler(async (req, res) => { - const users = await AdvertiserModel.find(); - res.status(200).json(users); - }) + showAllUsersController ); -// USER REGISTRATION ROUTE +// Registration endpoint, to create a new user router.post( "/register", - asyncHandler(async (req, res, err) => { - const { username, email, password } = req.body; - try { - if (!username || !email || !password) { - res.status(400).json({message: "Please add all fields", error: err.errors}); - } else { - const existingUser = await AdvertiserModel.findOne({ - $or: [{ username }, { email }] - }); - - if (existingUser) { - res.status(400).json({message: `User with ${existingUser.username = username ? "username" : "email"} already exists`}) - }; - - const salt = bcrypt.genSaltSync(10); - const hashedPassword = bcrypt.hashSync(password, salt); - - const newAdvertiser = new AdvertiserModel({ - username, - email, - password: hashedPassword - }); - - await newAdvertiser.save(); - - res.status(201).json ({ - success: true, - response: { - username: newAdvertiser.username, - email: newAdvertiser.email, - id: newAdvertiser._id, - accessToken: generateToken(newAdvertiser._id) - } - }); - }; - } catch (err) { - res.status(500).json({ success: false, response: err.message }); - } - }) + registerUserController ); -// USER LOG-IN ROUTE +// Sign-in endpoint, to authenticate a returning user router.post( "/signin", - asyncHandler(async (req, res) => { - // Retrieve username and password from req.body - const { username, password } = req.body; - - // Find a user with the provided username in the database - try { - const user = await AdvertiserModel.findOne({ username }); - if (!user) { - // If no user is found with the provided username, return status 401 Unauthorized and message "User not found" - res.status(401).json({ success: false, response: "User not found"}); - }; - - // If a user is found with the provided username, compare the provided password with the hashed password in the database - const isMatch = await bcrypt.compare(password, user.password); - if (!isMatch) { - res.status(401).json({ success: false, response: "Incorrect password"}); - }; + signinUserController +); - res.status(200).json({ success: true, response: { - username: user.username, - id: user._id, - accessToken: generateToken(user._id) - }}); +// An authenticated endpoint which only returns content if the Authorization header with the user's token was correct - } catch (err) { - res.status(500).json({ success: false, response: err.message}); - }; - }) -); export default router; \ No newline at end of file From 296bf310e626e96d9cbb3506e616185a19f7f5f9 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Wed, 6 Dec 2023 23:10:48 +0100 Subject: [PATCH 05/33] add user authorization middleware n advert controllers n authorization endpoints --- backend/controllers/advertControllers.js | 49 ++++++++++++++++++++++++ backend/middlewares/authorizeUser.js | 21 ++++++++++ backend/routes/advertRoutes.js | 17 +++++++- backend/routes/advertiserRoutes.js | 3 -- backend/server.js | 4 +- 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 backend/controllers/advertControllers.js create mode 100644 backend/middlewares/authorizeUser.js diff --git a/backend/controllers/advertControllers.js b/backend/controllers/advertControllers.js new file mode 100644 index 000000000..8ed5f5077 --- /dev/null +++ b/backend/controllers/advertControllers.js @@ -0,0 +1,49 @@ +import { AdvertModel } from "../models/advert"; +import { AdvertiserModel } from "../models/advertiser"; +import asyncHandler from "express-async-handler"; + +export const getOwnAdvertsController = asyncHandler(async (req, res) => { + try { + // Extract accessToken from the request header key "Authorization" + const accessToken = req.header("Authorization"); + + // Find the user in the database that has the same accessToken + const userFromStorage = await AdvertiserModel.findOne({ accessToken: accessToken}); + + // Get all the adverts in the database that belong to the user + const userAdverts = await AdvertModel.find({advertiser: userFromStorage}).sort({createdAt: -1}); + res.status(200).json(userAdverts); + } catch (err) { + res.status(400).json({ success: false, message: err.message }); + } +}); + +export const addNewAdvertController = asyncHandler(async (req, res) => { + try { + // Extract the advert from the request body + const { product, amount, unit, address, pickupTime } = req.body; + + // Extract accessToken from the request header key "Authorization" + const accessToken = req.header("Authorization"); + + // Find the user in the database that has the same accessToken + const userFromStorage = await AdvertiserModel.findOne({ accessToken: accessToken}); + + // Add the new advert to the database and attach the user to it + const newAdvert = new AdvertModel({ + product: product, + amount: amount, + unit: unit, + address: address, + pickupTime: pickupTime, + advertiser: userFromStorage + }); + + await newAdvert.save(); + + // Return the new advert in a response + res.status(201).json(newAdvert); + } catch (err) { + res.status(500).json({ success: false, message: err.message }); + } +}); \ No newline at end of file diff --git a/backend/middlewares/authorizeUser.js b/backend/middlewares/authorizeUser.js new file mode 100644 index 000000000..10e0f5b48 --- /dev/null +++ b/backend/middlewares/authorizeUser.js @@ -0,0 +1,21 @@ +import { AdvertiserModel } from "../models/advertiser"; + +export const authorizeUser = async (req, res, next) => { + // Extract the accessToken from the headers key "Authorization" + const accessToken = req.header("Authorization"); + + // Find the user in the database that has the same accessToken + try { + const user = await AdvertiserModel.findOne({accessToken: accessToken}); + + // If that user exists, add the user object to the request object and hand over it to the next middleware or routes. Otherwise, return status 401 Unauthorized and message "Please log in". + if (user) { + req.user = user; + next(); + } else { + res.status(401).json({ success: false, message: "Please log in" }); + }; + } catch (err) { + res.status(500).json({ success: false, message: err.message }); + } +}; \ No newline at end of file diff --git a/backend/routes/advertRoutes.js b/backend/routes/advertRoutes.js index 9e4142ab2..f19993706 100644 --- a/backend/routes/advertRoutes.js +++ b/backend/routes/advertRoutes.js @@ -1 +1,16 @@ -// An authenticated endpoint which only returns content if the Authorization header with the user's token was correct \ No newline at end of file +import express from "express"; +import { authorizeUser } from "../middlewares/authorizeUser"; +import { + getOwnAdvertsController, + addNewAdvertController +} from "../controllers/advertControllers"; + +const router = express.Router(); + +// An authenticated endpoint which returns only the adverts belonging to the user if the Authorization header with the user's token was correct +router.get("/get", authorizeUser, getOwnAdvertsController); + +// An authenticated endpoint for the user to post an advert +router.post("/add", authorizeUser, addNewAdvertController); + +export default router; \ No newline at end of file diff --git a/backend/routes/advertiserRoutes.js b/backend/routes/advertiserRoutes.js index 23115aff0..1926f6e53 100644 --- a/backend/routes/advertiserRoutes.js +++ b/backend/routes/advertiserRoutes.js @@ -36,7 +36,4 @@ router.post( signinUserController ); -// An authenticated endpoint which only returns content if the Authorization header with the user's token was correct - - export default router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index 7b9d74270..6e53e74f1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,6 +4,7 @@ import mongoose from "mongoose"; import dotenv from "dotenv"; dotenv.config(); import advertiserRoutes from "./routes/advertiserRoutes"; +import advertRoutes from "./routes/advertRoutes"; const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); @@ -20,7 +21,8 @@ app.use(cors()); app.use(express.json()); // Start defining your routes here -app.use("/", advertiserRoutes); +app.use(advertiserRoutes); +app.use(advertRoutes); // Start the server app.listen(port, () => { From dd1f89df3bb827b11d6f91b9a86cfa40eeb51a50 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Wed, 6 Dec 2023 23:32:38 +0100 Subject: [PATCH 06/33] add connection to database --- backend/config/db.js | 19 +++++++++++++++++++ backend/server.js | 10 +++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/backend/config/db.js b/backend/config/db.js index e69de29bb..0b5e5cb15 100644 --- a/backend/config/db.js +++ b/backend/config/db.js @@ -0,0 +1,19 @@ +import mongoose from "mongoose"; +import dotenv from "dotenv"; +dotenv.config(); + +export const connectDB = async () => { + try { + const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-authentication"; + const conn = mongoose.connect(mongoUrl); + + // If the connection is successful, log a message indicating that the MongoDB is connected + // console.log(`Mongo DB Connected: ${conn.connection.host}`); + } catch (err) { + // If an error occurs during the connection attempt, log the error message + console.log(err); + + // Exit the Node.js process with an exit code of 1 to indicate an error + process.exit(1); + }; +} diff --git a/backend/server.js b/backend/server.js index 6e53e74f1..6a7d677bc 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,13 +1,10 @@ import express from "express"; -import cors from "cors"; import mongoose from "mongoose"; -import dotenv from "dotenv"; -dotenv.config(); +import cors from "cors"; import advertiserRoutes from "./routes/advertiserRoutes"; import advertRoutes from "./routes/advertRoutes"; +import { connectDB } from "./config/db"; -const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/project-mongo"; -mongoose.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }); mongoose.Promise = Promise; // Defines the port the app will run on. Defaults to 8080, but can be overridden @@ -24,6 +21,9 @@ app.use(express.json()); app.use(advertiserRoutes); app.use(advertRoutes); +// Connect to the database +connectDB(); + // Start the server app.listen(port, () => { console.log(`Server running on http://localhost:${port}`); From be27bd3b163d9dbc04471b43c6ee56a7399ea5fe Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Wed, 6 Dec 2023 23:58:12 +0100 Subject: [PATCH 07/33] add package.json at root --- package.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 000000000..4978ac798 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "name": "authentication", + "version": "1.0.0", + "scripts": { + "postinstall": "npm install --prefix backend" + } + } \ No newline at end of file From b8154dd10e04a4e90b8c438d22e4e50695663a3a Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Thu, 7 Dec 2023 00:02:54 +0100 Subject: [PATCH 08/33] move endpoint for documentation to server.js --- backend/server.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/backend/server.js b/backend/server.js index 6a7d677bc..9946710a4 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,6 +1,8 @@ import express from "express"; import mongoose from "mongoose"; import cors from "cors"; +import asyncHandler from "express-async-handler"; +import listEndpoints from "express-list-endpoints"; import advertiserRoutes from "./routes/advertiserRoutes"; import advertRoutes from "./routes/advertRoutes"; import { connectDB } from "./config/db"; @@ -18,6 +20,14 @@ app.use(cors()); app.use(express.json()); // Start defining your routes here +// Endpoint to show documentation of all endpoints +app.get( + "/", + asyncHandler(async (req, res) => { + const endpoints = listEndpoints(router); + res.json(endpoints); + }) +); app.use(advertiserRoutes); app.use(advertRoutes); From 110dddc87c18e261b22ff503bafb091fdc200395 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Thu, 7 Dec 2023 00:03:26 +0100 Subject: [PATCH 09/33] minor change --- backend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/server.js b/backend/server.js index 9946710a4..b44a0beac 100644 --- a/backend/server.js +++ b/backend/server.js @@ -24,7 +24,7 @@ app.use(express.json()); app.get( "/", asyncHandler(async (req, res) => { - const endpoints = listEndpoints(router); + const endpoints = listEndpoints(app); res.json(endpoints); }) ); From a83fcc161f89ed6dfdde9be3b31f2183c94f518e Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Thu, 7 Dec 2023 22:27:04 +0100 Subject: [PATCH 10/33] set up frontend structure --- frontend/package.json | 5 ++++- frontend/src/App.jsx | 14 +++++++++++++- frontend/src/components/AdvertCard.jsx | 7 +++++++ frontend/src/components/Button.jsx | 0 frontend/src/components/LoginForm.jsx | 0 frontend/src/components/SignUpForm.jsx | 0 frontend/src/pages/Home.jsx | 0 frontend/src/pages/LandingPage.jsx | 0 frontend/src/routes/AppRoutes.jsx | 12 ++++++++++++ 9 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/AdvertCard.jsx create mode 100644 frontend/src/components/Button.jsx create mode 100644 frontend/src/components/LoginForm.jsx create mode 100644 frontend/src/components/SignUpForm.jsx create mode 100644 frontend/src/pages/Home.jsx create mode 100644 frontend/src/pages/LandingPage.jsx create mode 100644 frontend/src/routes/AppRoutes.jsx diff --git a/frontend/package.json b/frontend/package.json index e9c95b79f..e2b86fe58 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,7 +11,10 @@ }, "dependencies": { "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.1", + "styled-components": "^6.1.1", + "zustand": "^4.4.7" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1091d4310..b047ffe16 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,3 +1,15 @@ +import { BrowserRouter } from "react-router-dom"; +import { AppRoutes } from "./routes/AppRoutes"; + +// Set up the routing and display the navigation links export const App = () => { - return
Find me in src/app.jsx!
; + return ( + // Wrapping the entire app with BrowserRouter to enable client-side routing + <> + + {/* Defining the routes for the application */} + + + + ) }; diff --git a/frontend/src/components/AdvertCard.jsx b/frontend/src/components/AdvertCard.jsx new file mode 100644 index 000000000..61b2f573e --- /dev/null +++ b/frontend/src/components/AdvertCard.jsx @@ -0,0 +1,7 @@ + + +export const AdvertCard = () => { + return ( +
AdvertCard
+ ); +}; diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/LoginForm.jsx b/frontend/src/components/LoginForm.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/components/SignUpForm.jsx b/frontend/src/components/SignUpForm.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/routes/AppRoutes.jsx b/frontend/src/routes/AppRoutes.jsx new file mode 100644 index 000000000..830c6a16e --- /dev/null +++ b/frontend/src/routes/AppRoutes.jsx @@ -0,0 +1,12 @@ +import { Routes, Route } from "react-router-dom"; +import { Home } from "../pages/Home"; +import { LandingPage } from "../pages/LandingPage"; + +export const AppRoutes = () => { + return ( + + } /> + } /> + + ) +} \ No newline at end of file From 2c75e954ad92181376b307e98ad0d85078d114a9 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Fri, 8 Dec 2023 00:08:21 +0100 Subject: [PATCH 11/33] start setting up userStore --- frontend/src/App.jsx | 2 +- frontend/src/components/AdvertCard.jsx | 14 ++++-- frontend/src/components/Button.jsx | 9 ++++ frontend/src/components/Form.jsx | 30 ++++++++++++ frontend/src/components/LoginForm.jsx | 0 frontend/src/components/SignUpForm.jsx | 0 frontend/src/components/SingOutButton.jsx | 11 +++++ frontend/src/pages/Home.jsx | 13 +++++ frontend/src/pages/LandingPage.jsx | 11 +++++ frontend/src/routes/AppRoutes.jsx | 4 +- frontend/src/sections/Adverts.jsx | 13 +++++ frontend/src/stores/userStore.jsx | 59 +++++++++++++++++++++++ 12 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/Form.jsx delete mode 100644 frontend/src/components/LoginForm.jsx delete mode 100644 frontend/src/components/SignUpForm.jsx create mode 100644 frontend/src/components/SingOutButton.jsx create mode 100644 frontend/src/sections/Adverts.jsx create mode 100644 frontend/src/stores/userStore.jsx diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index b047ffe16..3f7ea6597 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import { BrowserRouter } from "react-router-dom"; -import { AppRoutes } from "./routes/AppRoutes"; +import { AppRoutes } from "./routes/appRoutes"; // Set up the routing and display the navigation links export const App = () => { diff --git a/frontend/src/components/AdvertCard.jsx b/frontend/src/components/AdvertCard.jsx index 61b2f573e..c9c36e911 100644 --- a/frontend/src/components/AdvertCard.jsx +++ b/frontend/src/components/AdvertCard.jsx @@ -1,7 +1,15 @@ - -export const AdvertCard = () => { +const AdvertCard = () => { return ( -
AdvertCard
+
+

Product:

+

Amount:

+

Unit:

+

Address:

+

Pick-up time:

+

Advertiser:

+
); }; + +export default AdvertCard; diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx index e69de29bb..fcb057585 100644 --- a/frontend/src/components/Button.jsx +++ b/frontend/src/components/Button.jsx @@ -0,0 +1,9 @@ + + +const Button = () => { + return ( +
Button
+ ); +}; + +export default Button; diff --git a/frontend/src/components/Form.jsx b/frontend/src/components/Form.jsx new file mode 100644 index 000000000..b205973a0 --- /dev/null +++ b/frontend/src/components/Form.jsx @@ -0,0 +1,30 @@ + + + +const Form = () => { + return ( +
+
+

Welcome to GreenBuddy

+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ ); +}; + +export default Form; \ No newline at end of file diff --git a/frontend/src/components/LoginForm.jsx b/frontend/src/components/LoginForm.jsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/components/SignUpForm.jsx b/frontend/src/components/SignUpForm.jsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/components/SingOutButton.jsx b/frontend/src/components/SingOutButton.jsx new file mode 100644 index 000000000..594bae426 --- /dev/null +++ b/frontend/src/components/SingOutButton.jsx @@ -0,0 +1,11 @@ +import { Link } from "react-router-dom"; + +const SignOutButton = () => { + return ( + + Sign Out + + ); +}; + +export default SignOutButton; \ No newline at end of file diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index e69de29bb..f35ff7831 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -0,0 +1,13 @@ +import SignOutButton from "../components/SingOutButton"; +import Adverts from "../sections/Adverts"; + +const Home = () => { + return ( +
+ + +
+ ); +}; + +export default Home; \ No newline at end of file diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx index e69de29bb..beda68088 100644 --- a/frontend/src/pages/LandingPage.jsx +++ b/frontend/src/pages/LandingPage.jsx @@ -0,0 +1,11 @@ +import Form from "../components/Form"; + +const LandingPage = () => { + return ( +
+
+
+ ); +}; + +export default LandingPage; \ No newline at end of file diff --git a/frontend/src/routes/AppRoutes.jsx b/frontend/src/routes/AppRoutes.jsx index 830c6a16e..7e3b03c98 100644 --- a/frontend/src/routes/AppRoutes.jsx +++ b/frontend/src/routes/AppRoutes.jsx @@ -1,6 +1,6 @@ import { Routes, Route } from "react-router-dom"; -import { Home } from "../pages/Home"; -import { LandingPage } from "../pages/LandingPage"; +import Home from "../pages/Home"; +import LandingPage from "../pages/LandingPage"; export const AppRoutes = () => { return ( diff --git a/frontend/src/sections/Adverts.jsx b/frontend/src/sections/Adverts.jsx new file mode 100644 index 000000000..c5bb042bb --- /dev/null +++ b/frontend/src/sections/Adverts.jsx @@ -0,0 +1,13 @@ +import AdvertCard from "../components/AdvertCard"; + +const Adverts = () => { + return ( + <> +

Your adverts

+
+ + + ); +}; + +export default Adverts; diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx new file mode 100644 index 000000000..910fd5373 --- /dev/null +++ b/frontend/src/stores/userStore.jsx @@ -0,0 +1,59 @@ +import { create } from "zustand"; +const apiEnv = import.meta.env.VITE_BACKEND_API; + +const userStore = create((set, get) => ({ + // Using same properties as those in AdvertiserModel + username: "", + setUsername: (username) => set({username}), + password: "", + setPassword: (password) => set({password}), + email: "", + setEmail: (email) => set({email}), + accessToken: null, + setAccessToken: (token) => set({accessToken: token}), + isLoggedIn: false, + setIsLoggedIn: (isLoggedIn) => set({isLoggedIn}), + + // Function to handle sign-up: First extracting the username, password, email from the fields; then making a POST request to the signup endpoint in database, where the authentication is carried out + handleSignUp: async (username, password, email) => { + if (!username || !password || !email) { + alert("Please fill in all the fields"); + return; + } + + try { + const response = await fetch(`${apiEnv}/register`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ username, password, email}) + }); + + const data = await response.json(); + + if (data) { + set({username}); + alert("Sign up successful"); + console.log("Signing up with: ", username); + } else { + alert(data.response || "Sign up failed"); + } + } catch (error) { + console.error("Signup error:", error); + alert("An error occurred during signup"); + } + }, + + // Function to handle log-in: First extracting the username, password from the fields; then making a POST request to the signin endpoint in database, where the authentication is carried out + handleLogIn: async (username, password) => { + + }, + + // Function to handle log-out: redirect the user to the log-in page + handleSignOut: () => { + + } +})); + +export default userStore; \ No newline at end of file From 7235a306e94fb8145c61a78fc5873ba67a5e61df Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Fri, 8 Dec 2023 21:22:38 +0100 Subject: [PATCH 12/33] finalize userStore n form n sign out button --- frontend/src/components/Button.jsx | 9 --- frontend/src/components/Form.jsx | 78 ++++++++++++++++++++--- frontend/src/components/SignOutButton.jsx | 18 ++++++ frontend/src/components/SingOutButton.jsx | 11 ---- frontend/src/pages/Home.jsx | 2 +- frontend/src/stores/userStore.jsx | 55 +++++++++++++--- 6 files changed, 134 insertions(+), 39 deletions(-) delete mode 100644 frontend/src/components/Button.jsx create mode 100644 frontend/src/components/SignOutButton.jsx delete mode 100644 frontend/src/components/SingOutButton.jsx diff --git a/frontend/src/components/Button.jsx b/frontend/src/components/Button.jsx deleted file mode 100644 index fcb057585..000000000 --- a/frontend/src/components/Button.jsx +++ /dev/null @@ -1,9 +0,0 @@ - - -const Button = () => { - return ( -
Button
- ); -}; - -export default Button; diff --git a/frontend/src/components/Form.jsx b/frontend/src/components/Form.jsx index b205973a0..ede853a17 100644 --- a/frontend/src/components/Form.jsx +++ b/frontend/src/components/Form.jsx @@ -1,27 +1,89 @@ +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; +import { userStore } from "../stores/userStore"; +const Form = () => { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [email, setEmail] = useState(""); + const [signUpMode, setSignUpMode] = useState(true); + const navigate = useNavigate(); + const storeHandleSignup = userStore((state) => state.handleSignUp); + const storeHandleLogin = userStore((state) => state.handleLogIn); + + // Handle value change in the fields + const handleSignUpChange = (e) => { + setSignUpMode(e.target.value); + }; + + const handleLoginChange = (e) => { + setSignUpMode(!e.target.value); + } + + const handleUserNameChange = (e) => { + setUsername(e.target.value); + }; + + const handlePasswordChange = (e) => { + setPassword(e.target.value); + } + + const handleEmailChange = (e) => { + setEmail(e.target.value); + } + + // Handle form submission via "SIGN UP" or "LOG IN" buttons + const onSignUpClick = async (e) => { + e.preventDefault(); + + await storeHandleSignup(username, password, email); + if (username && password && email) { + navigate("/home"); + } + }; + + const onLogInClick = async (e) => { + e.preventDefault(); + + await storeHandleLogin(username, password); + if (username && password) { + navigate("/home"); + } + }; -const Form = () => { return (

Welcome to GreenBuddy

+ +
+ + + + +
+
- +
- +
-
- - -
+ {signUpMode ? ( +
+ + +
+ ) : null} - +
); diff --git a/frontend/src/components/SignOutButton.jsx b/frontend/src/components/SignOutButton.jsx new file mode 100644 index 000000000..1c05060f9 --- /dev/null +++ b/frontend/src/components/SignOutButton.jsx @@ -0,0 +1,18 @@ +import { Link } from "react-router-dom"; +import { userStore } from "../stores/userStore"; + +const SignOutButton = () => { + const storeHandleSignout = userStore((state) => state.handleSignout); + + const onSignOutClick = () => { + storeHandleSignout(); + }; + + return ( + + Sign Out + + ); +}; + +export default SignOutButton; \ No newline at end of file diff --git a/frontend/src/components/SingOutButton.jsx b/frontend/src/components/SingOutButton.jsx deleted file mode 100644 index 594bae426..000000000 --- a/frontend/src/components/SingOutButton.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Link } from "react-router-dom"; - -const SignOutButton = () => { - return ( - - Sign Out - - ); -}; - -export default SignOutButton; \ No newline at end of file diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index f35ff7831..13a7960db 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,4 +1,4 @@ -import SignOutButton from "../components/SingOutButton"; +import SignOutButton from "../components/SignOutButton"; import Adverts from "../sections/Adverts"; const Home = () => { diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index 910fd5373..ffe88981d 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -1,7 +1,7 @@ import { create } from "zustand"; const apiEnv = import.meta.env.VITE_BACKEND_API; -const userStore = create((set, get) => ({ +export const userStore = create((set, get) => ({ // Using same properties as those in AdvertiserModel username: "", setUsername: (username) => set({username}), @@ -27,33 +27,68 @@ const userStore = create((set, get) => ({ headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password, email}) - }); + body: JSON.stringify({ username, password, email }) + }); const data = await response.json(); - if (data) { - set({username}); + if (data.success) { + set({username, password, email}); alert("Sign up successful"); console.log("Signing up with: ", username); } else { alert(data.response || "Sign up failed"); } } catch (error) { - console.error("Signup error:", error); + console.error("Signup error: ", error); alert("An error occurred during signup"); } }, // Function to handle log-in: First extracting the username, password from the fields; then making a POST request to the signin endpoint in database, where the authentication is carried out handleLogIn: async (username, password) => { + if (!username || !password) { + alert("Please fill in all the fields"); + return; + } + try { + const response = await fetch(`${apiEnv}/signin`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ username, password }) + }); + + const data = await response.json(); + if (data.success) { + set({ + username, + accessToken: data.response.accessToken, + isLoggedIn: true + }); + + localStorage.setItem("accessToken", data.response.accessToken); + alert("Log in successful"); + console.log("Logging in with: ", username); + } else { + alert(data.response || "Login failed"); + } + } catch (error) { + console.error("Login error:", error); + alert("An error occurred during login"); + } }, // Function to handle log-out: redirect the user to the log-in page handleSignOut: () => { - + set({ + username: "", + password: "", + accessToken: null, + isLoggedIn: false + }); + localStorage.removeItem("accessToken"); } -})); - -export default userStore; \ No newline at end of file +})); \ No newline at end of file From 9d4ebf68a1e089a64f37c60751f6af013643c7dd Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Fri, 8 Dec 2023 21:34:40 +0100 Subject: [PATCH 13/33] change env variable name --- frontend/src/stores/userStore.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index ffe88981d..ee59d4455 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -1,5 +1,5 @@ import { create } from "zustand"; -const apiEnv = import.meta.env.VITE_BACKEND_API; +const apiEnv = import.meta.env.BACKEND_API; export const userStore = create((set, get) => ({ // Using same properties as those in AdvertiserModel From a6dadf6fec316b0b8e353295f42d2469dfbe82ad Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Fri, 8 Dec 2023 22:48:27 +0100 Subject: [PATCH 14/33] change publish in netlify.toml --- netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 95443a1f3..a0ad77f26 100644 --- a/netlify.toml +++ b/netlify.toml @@ -2,5 +2,5 @@ # how it should build the JavaScript assets to deploy from. [build] base = "frontend/" - publish = "build/" + publish = "dist/" command = "npm run build" From 8359108592fc8dd74b5e37b14549c07347757930 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Fri, 8 Dec 2023 23:07:16 +0100 Subject: [PATCH 15/33] move routes into App.jsx to test a new deployment --- frontend/src/App.jsx | 9 +++++++-- frontend/src/routes/AppRoutes.jsx | 13 +------------ 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 3f7ea6597..5f0bd4f2e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,7 @@ import { BrowserRouter } from "react-router-dom"; -import { AppRoutes } from "./routes/appRoutes"; +import { Routes, Route } from "react-router-dom"; +import Home from "./pages/Home"; +import LandingPage from "./pages/LandingPage"; // Set up the routing and display the navigation links export const App = () => { @@ -8,7 +10,10 @@ export const App = () => { <> {/* Defining the routes for the application */} - + + } /> + } /> + ) diff --git a/frontend/src/routes/AppRoutes.jsx b/frontend/src/routes/AppRoutes.jsx index 7e3b03c98..7958dda4c 100644 --- a/frontend/src/routes/AppRoutes.jsx +++ b/frontend/src/routes/AppRoutes.jsx @@ -1,12 +1 @@ -import { Routes, Route } from "react-router-dom"; -import Home from "../pages/Home"; -import LandingPage from "../pages/LandingPage"; - -export const AppRoutes = () => { - return ( - - } /> - } /> - - ) -} \ No newline at end of file +// Cannot deploy on Netlify as it doesn't find this file. Therefore, move everything to App.jsx \ No newline at end of file From d9aff32c01dd4fef6781ae49f50566dc4e685a6c Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Fri, 8 Dec 2023 23:18:59 +0100 Subject: [PATCH 16/33] add console log av apiEnv --- frontend/src/stores/userStore.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index ee59d4455..653c8bf89 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -1,5 +1,6 @@ import { create } from "zustand"; const apiEnv = import.meta.env.BACKEND_API; +console.log(apiEnv); export const userStore = create((set, get) => ({ // Using same properties as those in AdvertiserModel From f7a6703167e9fe2dfda6de0d2815aad46f2c92b6 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Fri, 8 Dec 2023 23:51:00 +0100 Subject: [PATCH 17/33] test removing environment variable --- frontend/src/stores/userStore.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index 653c8bf89..533de3321 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -1,6 +1,6 @@ import { create } from "zustand"; -const apiEnv = import.meta.env.BACKEND_API; -console.log(apiEnv); +// const apiEnv = import.meta.env.VITE_BACKEND_API; +// console.log(apiEnv); export const userStore = create((set, get) => ({ // Using same properties as those in AdvertiserModel @@ -23,7 +23,7 @@ export const userStore = create((set, get) => ({ } try { - const response = await fetch(`${apiEnv}/register`, { + const response = await fetch("https://hang-authentication-project.onrender.com/register", { method: "POST", headers: { "Content-Type": "application/json" @@ -54,7 +54,7 @@ export const userStore = create((set, get) => ({ } try { - const response = await fetch(`${apiEnv}/signin`, { + const response = await fetch("https://hang-authentication-project.onrender.com/signin", { method: "POST", headers: { "Content-Type": "application/json" From 51b2dbc5fc43c2a27ff3a1c880e3dfea13bda327 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Sat, 9 Dec 2023 00:15:16 +0100 Subject: [PATCH 18/33] debug sign out button to remove accessToken from localStorage --- frontend/src/components/Form.jsx | 12 ++++-------- frontend/src/components/SignOutButton.jsx | 2 +- frontend/src/stores/userStore.jsx | 8 ++++++++ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/Form.jsx b/frontend/src/components/Form.jsx index ede853a17..7d34fd45f 100644 --- a/frontend/src/components/Form.jsx +++ b/frontend/src/components/Form.jsx @@ -1,4 +1,4 @@ -import { useNavigate } from "react-router-dom"; + import { useState } from "react"; import { userStore } from "../stores/userStore"; @@ -7,7 +7,6 @@ const Form = () => { const [password, setPassword] = useState(""); const [email, setEmail] = useState(""); const [signUpMode, setSignUpMode] = useState(true); - const navigate = useNavigate(); const storeHandleSignup = userStore((state) => state.handleSignUp); const storeHandleLogin = userStore((state) => state.handleLogIn); @@ -38,18 +37,15 @@ const Form = () => { e.preventDefault(); await storeHandleSignup(username, password, email); - if (username && password && email) { - navigate("/home"); - } }; const onLogInClick = async (e) => { e.preventDefault(); await storeHandleLogin(username, password); - if (username && password) { - navigate("/home"); - } + // if (username && password) { + // navigate("/home"); + // } }; return ( diff --git a/frontend/src/components/SignOutButton.jsx b/frontend/src/components/SignOutButton.jsx index 1c05060f9..c9d00a29a 100644 --- a/frontend/src/components/SignOutButton.jsx +++ b/frontend/src/components/SignOutButton.jsx @@ -2,7 +2,7 @@ import { Link } from "react-router-dom"; import { userStore } from "../stores/userStore"; const SignOutButton = () => { - const storeHandleSignout = userStore((state) => state.handleSignout); + const storeHandleSignout = userStore((state) => state.handleSignOut); const onSignOutClick = () => { storeHandleSignout(); diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index 533de3321..c63c82188 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -1,4 +1,5 @@ import { create } from "zustand"; +import { useNavigate } from "react-router-dom"; // const apiEnv = import.meta.env.VITE_BACKEND_API; // console.log(apiEnv); @@ -15,8 +16,10 @@ export const userStore = create((set, get) => ({ isLoggedIn: false, setIsLoggedIn: (isLoggedIn) => set({isLoggedIn}), + // Function to handle sign-up: First extracting the username, password, email from the fields; then making a POST request to the signup endpoint in database, where the authentication is carried out handleSignUp: async (username, password, email) => { + const navigate = useNavigate(); if (!username || !password || !email) { alert("Please fill in all the fields"); return; @@ -37,6 +40,9 @@ export const userStore = create((set, get) => ({ set({username, password, email}); alert("Sign up successful"); console.log("Signing up with: ", username); + // if (username && password && email) { + navigate("/home"); + // } } else { alert(data.response || "Sign up failed"); } @@ -48,6 +54,7 @@ export const userStore = create((set, get) => ({ // Function to handle log-in: First extracting the username, password from the fields; then making a POST request to the signin endpoint in database, where the authentication is carried out handleLogIn: async (username, password) => { + const navigate = useNavigate(); if (!username || !password) { alert("Please fill in all the fields"); return; @@ -73,6 +80,7 @@ export const userStore = create((set, get) => ({ localStorage.setItem("accessToken", data.response.accessToken); alert("Log in successful"); console.log("Logging in with: ", username); + navigate("/home"); } else { alert(data.response || "Login failed"); } From 99c755e583e27aea210bdf80c8cecfab5048ad5b Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Sat, 9 Dec 2023 00:20:08 +0100 Subject: [PATCH 19/33] move useNavigate hook back to form --- frontend/src/components/Form.jsx | 13 +++++++++---- frontend/src/stores/userStore.jsx | 12 +++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Form.jsx b/frontend/src/components/Form.jsx index 7d34fd45f..653534f52 100644 --- a/frontend/src/components/Form.jsx +++ b/frontend/src/components/Form.jsx @@ -1,4 +1,4 @@ - +import { useNavigate } from "react-router-dom"; import { useState } from "react"; import { userStore } from "../stores/userStore"; @@ -8,6 +8,8 @@ const Form = () => { const [email, setEmail] = useState(""); const [signUpMode, setSignUpMode] = useState(true); + const navigate = useNavigate(); + const storeHandleSignup = userStore((state) => state.handleSignUp); const storeHandleLogin = userStore((state) => state.handleLogIn); @@ -37,15 +39,18 @@ const Form = () => { e.preventDefault(); await storeHandleSignup(username, password, email); + if (username && password && email) { + navigate("/home"); + } }; const onLogInClick = async (e) => { e.preventDefault(); await storeHandleLogin(username, password); - // if (username && password) { - // navigate("/home"); - // } + if (username && password) { + navigate("/home"); + } }; return ( diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index c63c82188..2dbf1b479 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -1,5 +1,5 @@ import { create } from "zustand"; -import { useNavigate } from "react-router-dom"; + // const apiEnv = import.meta.env.VITE_BACKEND_API; // console.log(apiEnv); @@ -19,7 +19,7 @@ export const userStore = create((set, get) => ({ // Function to handle sign-up: First extracting the username, password, email from the fields; then making a POST request to the signup endpoint in database, where the authentication is carried out handleSignUp: async (username, password, email) => { - const navigate = useNavigate(); + if (!username || !password || !email) { alert("Please fill in all the fields"); return; @@ -40,9 +40,7 @@ export const userStore = create((set, get) => ({ set({username, password, email}); alert("Sign up successful"); console.log("Signing up with: ", username); - // if (username && password && email) { - navigate("/home"); - // } + } else { alert(data.response || "Sign up failed"); } @@ -54,7 +52,7 @@ export const userStore = create((set, get) => ({ // Function to handle log-in: First extracting the username, password from the fields; then making a POST request to the signin endpoint in database, where the authentication is carried out handleLogIn: async (username, password) => { - const navigate = useNavigate(); + if (!username || !password) { alert("Please fill in all the fields"); return; @@ -80,7 +78,7 @@ export const userStore = create((set, get) => ({ localStorage.setItem("accessToken", data.response.accessToken); alert("Log in successful"); console.log("Logging in with: ", username); - navigate("/home"); + } else { alert(data.response || "Login failed"); } From 6e87f5432d9cccf215bcb77919e49834ea74efe8 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Sat, 9 Dec 2023 00:21:16 +0100 Subject: [PATCH 20/33] change title in html --- frontend/index.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 0c589eccd..90a88ae22 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,8 @@ - - Vite + React + Authentication project - week 16
From 9d72c9da9102065d3b6c2853c40f60637de8348d Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Sat, 9 Dec 2023 00:32:38 +0100 Subject: [PATCH 21/33] debug navigation to authenticated content --- frontend/src/components/Form.jsx | 7 +++++-- frontend/src/stores/userStore.jsx | 7 ++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Form.jsx b/frontend/src/components/Form.jsx index 653534f52..968d17ff3 100644 --- a/frontend/src/components/Form.jsx +++ b/frontend/src/components/Form.jsx @@ -39,7 +39,8 @@ const Form = () => { e.preventDefault(); await storeHandleSignup(username, password, email); - if (username && password && email) { + const isLoggedIn = userStore.getState().isLoggedIn; + if (isLoggedIn) { navigate("/home"); } }; @@ -48,7 +49,9 @@ const Form = () => { e.preventDefault(); await storeHandleLogin(username, password); - if (username && password) { + + const isLoggedIn = userStore.getState().isLoggedIn; + if (isLoggedIn) { navigate("/home"); } }; diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index 2dbf1b479..580cf2902 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -37,7 +37,12 @@ export const userStore = create((set, get) => ({ const data = await response.json(); if (data.success) { - set({username, password, email}); + set({ + username, + password, + email, + isLoggedIn: true + }); alert("Sign up successful"); console.log("Signing up with: ", username); From 4f4b6715c8ecc330fa29edb1a0ea009ae9f75988 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Sat, 9 Dec 2023 00:37:41 +0100 Subject: [PATCH 22/33] add comments --- frontend/src/components/Form.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Form.jsx b/frontend/src/components/Form.jsx index 968d17ff3..386630b65 100644 --- a/frontend/src/components/Form.jsx +++ b/frontend/src/components/Form.jsx @@ -39,6 +39,8 @@ const Form = () => { e.preventDefault(); await storeHandleSignup(username, password, email); + + // Only successfully signed-up user can see the authenticated content const isLoggedIn = userStore.getState().isLoggedIn; if (isLoggedIn) { navigate("/home"); @@ -50,6 +52,7 @@ const Form = () => { await storeHandleLogin(username, password); + // Only successfully signed-up user can see the authenticated content const isLoggedIn = userStore.getState().isLoggedIn; if (isLoggedIn) { navigate("/home"); @@ -85,7 +88,7 @@ const Form = () => { ) : null} - From 43afa6f24be9e2cab98f0c0f078a975e71d92334 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Sun, 10 Dec 2023 15:03:55 +0100 Subject: [PATCH 23/33] add advertStore and fetch adverts --- frontend/.eslintrc.cjs | 1 + frontend/src/components/AdvertCard.jsx | 14 +++---- frontend/src/sections/Adverts.jsx | 31 ++++++++++++++- frontend/src/stores/advertStore.jsx | 53 ++++++++++++++++++++++++++ frontend/src/stores/userStore.jsx | 4 +- 5 files changed, 93 insertions(+), 10 deletions(-) create mode 100644 frontend/src/stores/advertStore.jsx diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 4dcb43901..c300651a5 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -16,5 +16,6 @@ module.exports = { 'warn', { allowConstantExport: true }, ], + 'react/prop-types': 0 }, } diff --git a/frontend/src/components/AdvertCard.jsx b/frontend/src/components/AdvertCard.jsx index c9c36e911..b3882d353 100644 --- a/frontend/src/components/AdvertCard.jsx +++ b/frontend/src/components/AdvertCard.jsx @@ -1,13 +1,13 @@ -const AdvertCard = () => { +const AdvertCard = ({ advert }) => { return (
-

Product:

-

Amount:

-

Unit:

-

Address:

-

Pick-up time:

-

Advertiser:

+

Product: {advert.product}

+

Amount: {advert.amount}

+

Unit: {advert.unit}

+

Address: {advert.address}

+

Pick-up time: {advert.pickUpTime}

+

Advertiser: {advert.advertiser}

); }; diff --git a/frontend/src/sections/Adverts.jsx b/frontend/src/sections/Adverts.jsx index c5bb042bb..5f02c2a48 100644 --- a/frontend/src/sections/Adverts.jsx +++ b/frontend/src/sections/Adverts.jsx @@ -1,10 +1,39 @@ +import { useCallback, useEffect } from "react"; +import { userStore } from "../stores/userStore"; +import { advertStore } from "../stores/advertStore"; import AdvertCard from "../components/AdvertCard"; const Adverts = () => { + const { adverts, fetchAdverts } = advertStore(); + const { accessToken } = userStore(); + + // Problem with useEffect(): "React Hook useEffect has a missing dependency: 'fetchAdverts'. Either include it or remove the dependency array.eslintreact-hooks/exhaustive-deps". + // fetchAdverts is a function provided by the advertStore, and it's unlikely to change between renders. If it is included in the dependency array, it might trigger unnecessary re-renders. To address this, the useCallback hook is used to memorize the fetchAdverts function, making it stable across renders. + + const memorizedFetchAdverts = useCallback(() => { + fetchAdverts(); + }, [fetchAdverts]); + + useEffect(() => { + memorizedFetchAdverts(); + }, [memorizedFetchAdverts, accessToken]); + return ( <>

Your adverts

-
+ {adverts.length === 0 ? ( + <> +

You don't have any advert...

+ + ) : ( + adverts.map((advert, index) => ( +
+ +
+ )) + )} + + ); diff --git a/frontend/src/stores/advertStore.jsx b/frontend/src/stores/advertStore.jsx new file mode 100644 index 000000000..888674a6b --- /dev/null +++ b/frontend/src/stores/advertStore.jsx @@ -0,0 +1,53 @@ +import { create } from "zustand"; +import { userStore } from "./userStore"; + +export const advertStore = create((set) => ({ + adverts: [], + setAdverts: (adverts) => set({ adverts }), + addAdvert: (newAdvert) => set((state) => ({ adverts: [...state.adverts, newAdvert]})), + userId: userStore.userId, + + // Function to fetch adverts belonging to a user based on their accessToken stored in the localStorage + fetchAdverts: async () => { + try { + const response = await fetch("https://hang-authentication-project.onrender.com/get", { + method: "GET", + headers: { + Authorization: localStorage.getItem("accessToken") + } + }) + + if (response.ok) { + const data = await response.json(); + set({ adverts: data }); + } else { + console.error("Failed to fetch adverts"); + } + } catch (error) { + console.error(error); + } + }, + + // Function to add a new advert to the server and then to the store + addAdvertToServer: async (advert) => { + try { + const response = await fetch("https://hang-authentication-project.onrender.com/add", { + method: "POST", + headers: { + Authorization: localStorage.getItem("accessToken"), + "Content-Type": "application/json" + }, + body: JSON.stringify({ advert: advert }) + }); + + if (response.ok) { + const data = await response.json(); + set((state) => ({ adverts: [...state.adverts, data]})); + } else { + console.error("Failed to add advert"); + } + } catch (error) { + console.error(error); + } + } +})); \ No newline at end of file diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index 580cf2902..6b4bf759d 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -1,9 +1,9 @@ import { create } from "zustand"; -// const apiEnv = import.meta.env.VITE_BACKEND_API; +// const apiEnv = import.meta.env.VITE_BACKEND_API; // The import does not work, therefore I had to put the URL directly in the fetch function // console.log(apiEnv); -export const userStore = create((set, get) => ({ +export const userStore = create((set) => ({ // Using same properties as those in AdvertiserModel username: "", setUsername: (username) => set({username}), From 630d64572d3f75b82c775a01e395b990259e6818 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Sun, 10 Dec 2023 23:46:51 +0100 Subject: [PATCH 24/33] remove unnecessary files and add styling --- backend/controllers/advertControllers.js | 49 ------- backend/controllers/advertiserControllers.js | 5 +- .../{authorizeUser.js => authenticateUser.js} | 9 +- backend/models/advert.js | 36 ------ backend/routes/advertRoutes.js | 16 --- backend/routes/advertiserRoutes.js | 11 -- backend/server.js | 2 - frontend/package.json | 1 + frontend/src/assets/animation/Animation.json | 1 + frontend/src/components/AdvertCard.jsx | 15 --- frontend/src/components/Form.jsx | 47 ++++--- frontend/src/components/SignOutButton.jsx | 3 +- frontend/src/index.css | 121 ++++++++++++++++-- frontend/src/pages/Home.jsx | 23 +++- frontend/src/sections/Adverts.jsx | 42 ------ frontend/src/stores/advertStore.jsx | 53 -------- frontend/src/stores/userStore.jsx | 3 + 17 files changed, 178 insertions(+), 259 deletions(-) delete mode 100644 backend/controllers/advertControllers.js rename backend/middlewares/{authorizeUser.js => authenticateUser.js} (71%) delete mode 100644 backend/models/advert.js delete mode 100644 backend/routes/advertRoutes.js create mode 100644 frontend/src/assets/animation/Animation.json delete mode 100644 frontend/src/components/AdvertCard.jsx delete mode 100644 frontend/src/sections/Adverts.jsx delete mode 100644 frontend/src/stores/advertStore.jsx diff --git a/backend/controllers/advertControllers.js b/backend/controllers/advertControllers.js deleted file mode 100644 index 8ed5f5077..000000000 --- a/backend/controllers/advertControllers.js +++ /dev/null @@ -1,49 +0,0 @@ -import { AdvertModel } from "../models/advert"; -import { AdvertiserModel } from "../models/advertiser"; -import asyncHandler from "express-async-handler"; - -export const getOwnAdvertsController = asyncHandler(async (req, res) => { - try { - // Extract accessToken from the request header key "Authorization" - const accessToken = req.header("Authorization"); - - // Find the user in the database that has the same accessToken - const userFromStorage = await AdvertiserModel.findOne({ accessToken: accessToken}); - - // Get all the adverts in the database that belong to the user - const userAdverts = await AdvertModel.find({advertiser: userFromStorage}).sort({createdAt: -1}); - res.status(200).json(userAdverts); - } catch (err) { - res.status(400).json({ success: false, message: err.message }); - } -}); - -export const addNewAdvertController = asyncHandler(async (req, res) => { - try { - // Extract the advert from the request body - const { product, amount, unit, address, pickupTime } = req.body; - - // Extract accessToken from the request header key "Authorization" - const accessToken = req.header("Authorization"); - - // Find the user in the database that has the same accessToken - const userFromStorage = await AdvertiserModel.findOne({ accessToken: accessToken}); - - // Add the new advert to the database and attach the user to it - const newAdvert = new AdvertModel({ - product: product, - amount: amount, - unit: unit, - address: address, - pickupTime: pickupTime, - advertiser: userFromStorage - }); - - await newAdvert.save(); - - // Return the new advert in a response - res.status(201).json(newAdvert); - } catch (err) { - res.status(500).json({ success: false, message: err.message }); - } -}); \ No newline at end of file diff --git a/backend/controllers/advertiserControllers.js b/backend/controllers/advertiserControllers.js index c2d5562e6..a77d26ef0 100644 --- a/backend/controllers/advertiserControllers.js +++ b/backend/controllers/advertiserControllers.js @@ -6,7 +6,7 @@ import jwt from "jsonwebtoken"; // Generate a JWT token containing the user's unique ID, with an optional secret key and a 24-hour expiration time const generateToken = (advertiser) => { return jwt.sign({ id: advertiser._id}, process.env.JWT_SECRET, { - expiresIn: "24h" + expiresIn: "1h" }); }; @@ -76,7 +76,8 @@ export const signinUserController = asyncHandler(async (req, res) => { res.status(200).json({ success: true, response: { username: user.username, id: user._id, - accessToken: generateToken(user._id) + // accessToken: generateToken(user._id) + accessToken: user.accessToken }}); } catch (err) { diff --git a/backend/middlewares/authorizeUser.js b/backend/middlewares/authenticateUser.js similarity index 71% rename from backend/middlewares/authorizeUser.js rename to backend/middlewares/authenticateUser.js index 10e0f5b48..18555aeb2 100644 --- a/backend/middlewares/authorizeUser.js +++ b/backend/middlewares/authenticateUser.js @@ -1,12 +1,17 @@ import { AdvertiserModel } from "../models/advertiser"; -export const authorizeUser = async (req, res, next) => { +export const authenticateUser = async (req, res, next) => { // Extract the accessToken from the headers key "Authorization" const accessToken = req.header("Authorization"); + // Handle missing or invalid tokens + if (!accessToken) { + return res.status(401).json({ success: false, message: "Access token is missing" }); + }; + // Find the user in the database that has the same accessToken try { - const user = await AdvertiserModel.findOne({accessToken: accessToken}); + const user = await AdvertiserModel.findOne({ accessToken }); // If that user exists, add the user object to the request object and hand over it to the next middleware or routes. Otherwise, return status 401 Unauthorized and message "Please log in". if (user) { diff --git a/backend/models/advert.js b/backend/models/advert.js deleted file mode 100644 index 9d51b7f61..000000000 --- a/backend/models/advert.js +++ /dev/null @@ -1,36 +0,0 @@ -import mongoose from "mongoose"; - -const { Schema } = mongoose; - -export const advertSchema = new Schema({ - product: { - type: String, - required: true - }, - amount: { - type: Number, - required: true, - default: 0 - }, - unit: { - type: String, - required: true - }, - address: { - type: String, - required: true - }, - pickupTime: { - type: Date, - default: Date.now - }, - advertiser: { - type: mongoose.Schema.Types.ObjectId, - ref: "Advertiser" - } -}, -{ - timestamps: true, -}); - -export const AdvertModel = mongoose.model("Advert", advertSchema); \ No newline at end of file diff --git a/backend/routes/advertRoutes.js b/backend/routes/advertRoutes.js deleted file mode 100644 index f19993706..000000000 --- a/backend/routes/advertRoutes.js +++ /dev/null @@ -1,16 +0,0 @@ -import express from "express"; -import { authorizeUser } from "../middlewares/authorizeUser"; -import { - getOwnAdvertsController, - addNewAdvertController -} from "../controllers/advertControllers"; - -const router = express.Router(); - -// An authenticated endpoint which returns only the adverts belonging to the user if the Authorization header with the user's token was correct -router.get("/get", authorizeUser, getOwnAdvertsController); - -// An authenticated endpoint for the user to post an advert -router.post("/add", authorizeUser, addNewAdvertController); - -export default router; \ No newline at end of file diff --git a/backend/routes/advertiserRoutes.js b/backend/routes/advertiserRoutes.js index 1926f6e53..8b0dc22ce 100644 --- a/backend/routes/advertiserRoutes.js +++ b/backend/routes/advertiserRoutes.js @@ -1,6 +1,4 @@ import express from "express"; -import asyncHandler from "express-async-handler"; -import listEndpoints from "express-list-endpoints"; import { registerUserController, showAllUsersController, @@ -9,15 +7,6 @@ import { const router = express.Router(); -// Endpoint to show documentation of all endpoints -router.get( - "/", - asyncHandler(async (req, res) => { - const endpoints = listEndpoints(router); - res.json(endpoints); - }) -); - // Endpoint to show all users router.get( "/users", diff --git a/backend/server.js b/backend/server.js index b44a0beac..f0eea610f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -4,7 +4,6 @@ import cors from "cors"; import asyncHandler from "express-async-handler"; import listEndpoints from "express-list-endpoints"; import advertiserRoutes from "./routes/advertiserRoutes"; -import advertRoutes from "./routes/advertRoutes"; import { connectDB } from "./config/db"; mongoose.Promise = Promise; @@ -29,7 +28,6 @@ app.get( }) ); app.use(advertiserRoutes); -app.use(advertRoutes); // Connect to the database connectDB(); diff --git a/frontend/package.json b/frontend/package.json index e2b86fe58..5dba88de0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "lottie-react": "^2.4.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.1", diff --git a/frontend/src/assets/animation/Animation.json b/frontend/src/assets/animation/Animation.json new file mode 100644 index 000000000..1ac83feed --- /dev/null +++ b/frontend/src/assets/animation/Animation.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"","k":"","d":"","tc":""},"fr":30,"ip":0,"op":45,"w":1200,"h":1200,"nm":"Lock rectangle","ddd":1,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Rotation","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[-8]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[-8]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[10]},{"t":30,"s":[0]}],"ix":10},"p":{"a":0,"k":[872,960,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":1798,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Lock","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-272,-186,0],"ix":2},"a":{"a":0,"k":[24,232,0],"ix":1},"s":{"a":0,"k":[75,75,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[180,153],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[10]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.736],"y":[-9.597]},"t":5,"s":[12]},{"i":{"x":[0.476],"y":[25.153]},"o":{"x":[0.333],"y":[0]},"t":10.01,"s":[10]},{"i":{"x":[0.749],"y":[-2.101]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[10]},{"i":{"x":[0.749],"y":[0.996]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[10]},{"t":30.0302734375,"s":[737]}],"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.494117647409,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.027450982481,0.06274510175,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24,261.333],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 2","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[700,480],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":10,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0,0.027450980619,0.06274510175,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.027450980619,0.06274510175,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[24,232],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1798.7987987988,"st":0,"bm":0},{"ddd":1,"ind":3,"ty":4,"nm":"Line","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"rx":{"a":0,"k":0,"ix":8},"ry":{"a":0,"k":0,"ix":9},"rz":{"a":0,"k":0,"ix":10},"or":{"a":0,"k":[0,0,0],"ix":7},"p":{"a":0,"k":[-272,-356.25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[75,75,75],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-238,-20],[-238,-400],[242,-402],[244,-16]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":803,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[88]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":15,"s":[88]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":20,"s":[88]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":25,"s":[88]},{"t":30,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,0.494117647409,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":50,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.146158988803,0.48555800494,0.776471007104,1],"ix":4},"o":{"a":0,"k":0,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1798.7987987988,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":1,"nm":"Lock","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[600,600,0],"ix":2},"a":{"a":0,"k":[600,600,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"sw":1200,"sh":1200,"sc":"#ffffff","ip":0,"op":1798,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/frontend/src/components/AdvertCard.jsx b/frontend/src/components/AdvertCard.jsx deleted file mode 100644 index b3882d353..000000000 --- a/frontend/src/components/AdvertCard.jsx +++ /dev/null @@ -1,15 +0,0 @@ - -const AdvertCard = ({ advert }) => { - return ( -
-

Product: {advert.product}

-

Amount: {advert.amount}

-

Unit: {advert.unit}

-

Address: {advert.address}

-

Pick-up time: {advert.pickUpTime}

-

Advertiser: {advert.advertiser}

-
- ); -}; - -export default AdvertCard; diff --git a/frontend/src/components/Form.jsx b/frontend/src/components/Form.jsx index 386630b65..a32d9d077 100644 --- a/frontend/src/components/Form.jsx +++ b/frontend/src/components/Form.jsx @@ -60,33 +60,42 @@ const Form = () => { }; return ( -
+

Welcome to GreenBuddy

-
- - - - +
+
+ + +
+ +
+ + +
+
-
- - -
+
+
+ + +
-
- - +
+ + +
+ + {signUpMode ? ( +
+ + +
+ ) : null}
- {signUpMode ? ( -
- - -
- ) : null}
); }; diff --git a/frontend/src/sections/Adverts.jsx b/frontend/src/sections/Adverts.jsx new file mode 100644 index 000000000..b0ca5a06c --- /dev/null +++ b/frontend/src/sections/Adverts.jsx @@ -0,0 +1,34 @@ +import { useEffect } from "react"; +import { userStore } from "../stores/userStore"; +import { advertStore } from "../stores/advertStore"; +import AdvertCard from "../components/AdvertCard"; + +const Adverts = () => { + const { adverts, fetchAdverts } = advertStore(); + const { accessToken } = userStore(); + + console.log(accessToken); + + useEffect(() => { + fetchAdverts(); + }, [fetchAdverts, adverts, accessToken]); + + return ( + <> +

Your adverts

+ {adverts.length === 0 ? ( + <> +

You don't have any advert...

+ + ) : ( + adverts.map((advert, index) => ( +
+ +
+ )) + )} + + ); +}; + +export default Adverts; diff --git a/frontend/src/stores/advertStore.jsx b/frontend/src/stores/advertStore.jsx new file mode 100644 index 000000000..8d6365f2d --- /dev/null +++ b/frontend/src/stores/advertStore.jsx @@ -0,0 +1,56 @@ +import { create } from "zustand"; +import { userStore } from "./userStore"; + +export const advertStore = create((set) => ({ + adverts: [], + setAdverts: (adverts) => set({ adverts }), + addAdvert: (newAdvert) => set((state) => ({ adverts: [...state.adverts, newAdvert]})), + userId: userStore.userId, + + // Function to fetch adverts belonging to a user based on their accessToken stored in the localStorage + fetchAdverts: async () => { + try { + const response = await fetch("https://localhost:8000/get", { + method: "GET", + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}` + } + }); + + const data = await response.json(); + + console.log(data); + + if (data.success) { + set({ adverts: data }); + } else { + console.error("Failed to fetch adverts"); + } + } catch (error) { + console.error(error); + } + }, + + // Function to add a new advert to the server and then to the store + addAdvertToServer: async (advert) => { + try { + const response = await fetch("https://localhost:8000/add", { + method: "POST", + headers: { + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ advert: advert }) + }); + + if (response.ok) { + const data = await response.json(); + set((state) => ({ adverts: [...state.adverts, data]})); + } else { + console.error("Failed to add advert"); + } + } catch (error) { + console.error(error); + } + } +})); \ No newline at end of file diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index f21d0557d..dbe51a392 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -26,7 +26,7 @@ export const userStore = create((set) => ({ } try { - const response = await fetch("https://hang-authentication-project.onrender.com/register", { + const response = await fetch("http://localhost:8000/register", { method: "POST", headers: { "Content-Type": "application/json" @@ -65,7 +65,7 @@ export const userStore = create((set) => ({ } try { - const response = await fetch("https://hang-authentication-project.onrender.com/signin", { + const response = await fetch("http://localhost:8000/signin", { method: "POST", headers: { "Content-Type": "application/json" diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 5a33944a9..4a57ae52b 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,5 +1,5 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ From ddf582a1c6e503d4be2c79a033dfa327b729fc64 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Mon, 11 Dec 2023 23:26:28 +0100 Subject: [PATCH 28/33] replace localhost with backend API --- frontend/src/stores/advertStore.jsx | 4 ++-- frontend/src/stores/userStore.jsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/stores/advertStore.jsx b/frontend/src/stores/advertStore.jsx index 8d6365f2d..7324aeb9e 100644 --- a/frontend/src/stores/advertStore.jsx +++ b/frontend/src/stores/advertStore.jsx @@ -10,7 +10,7 @@ export const advertStore = create((set) => ({ // Function to fetch adverts belonging to a user based on their accessToken stored in the localStorage fetchAdverts: async () => { try { - const response = await fetch("https://localhost:8000/get", { + const response = await fetch("https://hang-authentication-project.onrender.com/get", { method: "GET", headers: { Authorization: `Bearer ${localStorage.getItem("accessToken")}` @@ -34,7 +34,7 @@ export const advertStore = create((set) => ({ // Function to add a new advert to the server and then to the store addAdvertToServer: async (advert) => { try { - const response = await fetch("https://localhost:8000/add", { + const response = await fetch("https://hang-authentication-project.onrender.com/add", { method: "POST", headers: { Authorization: `Bearer ${localStorage.getItem("accessToken")}`, diff --git a/frontend/src/stores/userStore.jsx b/frontend/src/stores/userStore.jsx index dbe51a392..f21d0557d 100644 --- a/frontend/src/stores/userStore.jsx +++ b/frontend/src/stores/userStore.jsx @@ -26,7 +26,7 @@ export const userStore = create((set) => ({ } try { - const response = await fetch("http://localhost:8000/register", { + const response = await fetch("https://hang-authentication-project.onrender.com/register", { method: "POST", headers: { "Content-Type": "application/json" @@ -65,7 +65,7 @@ export const userStore = create((set) => ({ } try { - const response = await fetch("http://localhost:8000/signin", { + const response = await fetch("https://hang-authentication-project.onrender.com/signin", { method: "POST", headers: { "Content-Type": "application/json" From 90dacbc9accedf929bc283abb57b1412b4e774bd Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Mon, 11 Dec 2023 23:32:49 +0100 Subject: [PATCH 29/33] debugging middleware --- backend/middlewares/authenticateUser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/middlewares/authenticateUser.js b/backend/middlewares/authenticateUser.js index 18555aeb2..ac4c19cb0 100644 --- a/backend/middlewares/authenticateUser.js +++ b/backend/middlewares/authenticateUser.js @@ -11,7 +11,7 @@ export const authenticateUser = async (req, res, next) => { // Find the user in the database that has the same accessToken try { - const user = await AdvertiserModel.findOne({ accessToken }); + const user = await AdvertiserModel.findOne({ accessToken: accessToken }); // If that user exists, add the user object to the request object and hand over it to the next middleware or routes. Otherwise, return status 401 Unauthorized and message "Please log in". if (user) { From a66bcb8aef2112f799017b398a50668f1f287553 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Mon, 11 Dec 2023 23:48:47 +0100 Subject: [PATCH 30/33] add console log --- backend/controllers/advertControllers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/controllers/advertControllers.js b/backend/controllers/advertControllers.js index 8ed5f5077..27b7f08bf 100644 --- a/backend/controllers/advertControllers.js +++ b/backend/controllers/advertControllers.js @@ -10,6 +10,7 @@ export const getOwnAdvertsController = asyncHandler(async (req, res) => { // Find the user in the database that has the same accessToken const userFromStorage = await AdvertiserModel.findOne({ accessToken: accessToken}); + console.log(userFromStorage); // Get all the adverts in the database that belong to the user const userAdverts = await AdvertModel.find({advertiser: userFromStorage}).sort({createdAt: -1}); res.status(200).json(userAdverts); From 3c4c951d86538db3f1458568be3b786bbd79d382 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Tue, 12 Dec 2023 13:20:29 +0100 Subject: [PATCH 31/33] remove Bearer in authorization --- frontend/src/stores/advertStore.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/stores/advertStore.jsx b/frontend/src/stores/advertStore.jsx index 7324aeb9e..fa059ac42 100644 --- a/frontend/src/stores/advertStore.jsx +++ b/frontend/src/stores/advertStore.jsx @@ -13,7 +13,7 @@ export const advertStore = create((set) => ({ const response = await fetch("https://hang-authentication-project.onrender.com/get", { method: "GET", headers: { - Authorization: `Bearer ${localStorage.getItem("accessToken")}` + Authorization: localStorage.getItem("accessToken") } }); @@ -37,7 +37,7 @@ export const advertStore = create((set) => ({ const response = await fetch("https://hang-authentication-project.onrender.com/add", { method: "POST", headers: { - Authorization: `Bearer ${localStorage.getItem("accessToken")}`, + Authorization: localStorage.getItem("accessToken"), "Content-Type": "application/json" }, body: JSON.stringify({ advert: advert }) From 84a5bf1c4c7c9ed7e56848f642ed9889c2bb0f1e Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Tue, 12 Dec 2023 13:58:56 +0100 Subject: [PATCH 32/33] change fetchAdverts --- frontend/src/stores/advertStore.jsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/src/stores/advertStore.jsx b/frontend/src/stores/advertStore.jsx index fa059ac42..7d2674f1f 100644 --- a/frontend/src/stores/advertStore.jsx +++ b/frontend/src/stores/advertStore.jsx @@ -16,12 +16,9 @@ export const advertStore = create((set) => ({ Authorization: localStorage.getItem("accessToken") } }); - - const data = await response.json(); - - console.log(data); - - if (data.success) { + + if (response.ok) { + const data = await response.json(); set({ adverts: data }); } else { console.error("Failed to fetch adverts"); From f635ce0ee5a0a37321c6b1a8a74adb3a7a03df62 Mon Sep 17 00:00:00 2001 From: "nguyenvuhang@gmail.com" Date: Tue, 12 Dec 2023 16:20:50 +0100 Subject: [PATCH 33/33] add funtionality to create advert which hasn't worked yet --- frontend/src/components/AdvertCard.jsx | 2 +- frontend/src/components/CreateAdvert.jsx | 42 +++++++++++++++++++++ frontend/src/index.css | 48 ++++++++++++++++++++++-- frontend/src/pages/Home.jsx | 2 +- frontend/src/sections/Adverts.jsx | 18 +++++---- 5 files changed, 100 insertions(+), 12 deletions(-) create mode 100644 frontend/src/components/CreateAdvert.jsx diff --git a/frontend/src/components/AdvertCard.jsx b/frontend/src/components/AdvertCard.jsx index b3882d353..029f1e291 100644 --- a/frontend/src/components/AdvertCard.jsx +++ b/frontend/src/components/AdvertCard.jsx @@ -1,7 +1,7 @@ const AdvertCard = ({ advert }) => { return ( -
+

Product: {advert.product}

Amount: {advert.amount}

Unit: {advert.unit}

diff --git a/frontend/src/components/CreateAdvert.jsx b/frontend/src/components/CreateAdvert.jsx new file mode 100644 index 000000000..d76880e36 --- /dev/null +++ b/frontend/src/components/CreateAdvert.jsx @@ -0,0 +1,42 @@ +import { useState } from "react"; +import { advertStore } from "../stores/advertStore"; + +export const CreateAdvert = () => { + // For simplicity, test first with adverts containing only one input field. If it works, add more fields + const [advert, setAdvert] = useState({product: ""}); + const [product, setProduct] = useState(""); + const { addAdvertToServer } = advertStore(); + + // Create a simple advert with only product to see if it works + const handleProductInput = (e) => { + setProduct(e.target.value); + }; + + const addAdvertLocal = async () => { + if (product.trim() !== "") { + setAdvert({product: product}); + await addAdvertToServer(advert); + // alert("Advert published successfully"); + setProduct(""); //Clear the input field after the advert is added + } + }; + + return ( +
+

Create A New Advert

+
+ + +
+ + +
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css index b7737c275..e9feeb131 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -9,6 +9,7 @@ body { text-align: center; } +/* Form for sign up or log in */ .form-container { display: flex; align-items: center; @@ -44,6 +45,7 @@ h1 { display: flex; flex-direction: row; justify-content: center; + align-items: center; gap: 10px; font-size: 1.5rem; } @@ -66,13 +68,14 @@ h1 { font-size: 1rem; } -.info-field { +.info-field, .advert-info { border: 2px solid #ccc; border-radius: 0.25rem; padding: 0.5rem; font-size: 1rem; } +/* Buttons */ .button { background-color: #335383; border: none; @@ -88,10 +91,49 @@ h1 { margin-bottom: 2rem; } -.sign-out { +.sign-out, .create { text-decoration: none; } +/* Adverts in home page */ +.adverts-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; +} + +.adverts-wrapper { + display: flex; + flex-direction: wrap; + gap: 1rem; +} + +.no-advert { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.advert-card { + border: 2px solid black; + border-radius: 10px; + padding: 10px; + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; +} + +/* Create Advert Page*/ +.create-advert { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 2rem; +} + @media (min-width: 768px) { form { width: 50%; @@ -105,7 +147,7 @@ h1 { font-size: 1.5rem; } - .info-field { + .info-field, .advert-info { border: 2px solid #ccc; border-radius: 0.25rem; padding: 0.5rem; diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 46e4b63d0..636938852 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -19,7 +19,7 @@ const Home = () => {
-

Welcome to the world of secrets!

+

Welcome to your own world in GreenBuddy!

{ const { adverts, fetchAdverts } = advertStore(); @@ -14,20 +15,23 @@ const Adverts = () => { }, [fetchAdverts, adverts, accessToken]); return ( - <> -

Your adverts

+
+

Your adverts

+
{adverts.length === 0 ? ( - <> +

You don't have any advert...

- + +
) : ( adverts.map((advert, index) => (
- )) - )} - + )) + )} +
+
); };