diff --git a/README.md b/README.md
index dfa05e177..f1e35df39 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,6 @@
-# Project Auth API
-Replace this readme with your own information about your project.
-
-Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
-
-## The problem
-
-Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?
## View it live
+Backend: https://hang-authentication-project.onrender.com
-Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.
+Frontend: https://hang-authentication-project.netlify.app
diff --git a/backend/config/db.js b/backend/config/db.js
new file mode 100644
index 000000000..0b5e5cb15
--- /dev/null
+++ 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/controllers/advertControllers.js b/backend/controllers/advertControllers.js
new file mode 100644
index 000000000..27b7f08bf
--- /dev/null
+++ b/backend/controllers/advertControllers.js
@@ -0,0 +1,50 @@
+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});
+
+ 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);
+ } 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
new file mode 100644
index 000000000..a77d26ef0
--- /dev/null
+++ b/backend/controllers/advertiserControllers.js
@@ -0,0 +1,86 @@
+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: "1h"
+ });
+};
+
+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)
+ accessToken: user.accessToken
+ }});
+
+ } catch (err) {
+ res.status(500).json({ success: false, response: err.message});
+ };
+})
\ No newline at end of file
diff --git a/backend/middlewares/authenticateUser.js b/backend/middlewares/authenticateUser.js
new file mode 100644
index 000000000..ac4c19cb0
--- /dev/null
+++ b/backend/middlewares/authenticateUser.js
@@ -0,0 +1,26 @@
+import { AdvertiserModel } from "../models/advertiser";
+
+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 });
+
+ // 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/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
new file mode 100644
index 000000000..4d05e921c
--- /dev/null
+++ b/backend/models/advertiser.js
@@ -0,0 +1,34 @@
+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,
+ 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/advertRoutes.js b/backend/routes/advertRoutes.js
new file mode 100644
index 000000000..af7916acb
--- /dev/null
+++ b/backend/routes/advertRoutes.js
@@ -0,0 +1,16 @@
+import express from "express";
+import { authenticateUser } from "../middlewares/authenticateUser";
+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", authenticateUser, getOwnAdvertsController);
+
+// An authenticated endpoint for the user to post an advert
+router.post("/add", authenticateUser, addNewAdvertController);
+
+export default router;
\ No newline at end of file
diff --git a/backend/routes/advertiserRoutes.js b/backend/routes/advertiserRoutes.js
new file mode 100644
index 000000000..8b0dc22ce
--- /dev/null
+++ b/backend/routes/advertiserRoutes.js
@@ -0,0 +1,28 @@
+import express from "express";
+import {
+ registerUserController,
+ showAllUsersController,
+ signinUserController
+} from "../controllers/advertiserControllers";
+
+const router = express.Router();
+
+// Endpoint to show all users
+router.get(
+ "/users",
+ showAllUsersController
+);
+
+// Registration endpoint, to create a new user
+router.post(
+ "/register",
+ registerUserController
+);
+
+// Sign-in endpoint, to authenticate a returning user
+router.post(
+ "/signin",
+ signinUserController
+);
+
+export default router;
\ No newline at end of file
diff --git a/backend/server.js b/backend/server.js
index 2d7ae8aa1..b44a0beac 100644
--- a/backend/server.js
+++ b/backend/server.js
@@ -1,9 +1,12 @@
import express from "express";
-import cors from "cors";
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";
-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
@@ -17,9 +20,19 @@ app.use(cors());
app.use(express.json());
// Start defining your routes here
-app.get("/", (req, res) => {
- res.send("Hello Technigo!");
-});
+// Endpoint to show documentation of all endpoints
+app.get(
+ "/",
+ asyncHandler(async (req, res) => {
+ const endpoints = listEndpoints(app);
+ res.json(endpoints);
+ })
+);
+app.use(advertiserRoutes);
+app.use(advertRoutes);
+
+// Connect to the database
+connectDB();
// Start the server
app.listen(port, () => {
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/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
diff --git a/frontend/package.json b/frontend/package.json
index e9c95b79f..5dba88de0 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,8 +10,12 @@
"preview": "vite preview"
},
"dependencies": {
+ "lottie-react": "^2.4.0",
"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..5f0bd4f2e 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -1,3 +1,20 @@
+import { BrowserRouter } from "react-router-dom";
+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 = () => {
- 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/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
new file mode 100644
index 000000000..029f1e291
--- /dev/null
+++ b/frontend/src/components/AdvertCard.jsx
@@ -0,0 +1,15 @@
+
+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/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/components/Form.jsx b/frontend/src/components/Form.jsx
new file mode 100644
index 000000000..a32d9d077
--- /dev/null
+++ b/frontend/src/components/Form.jsx
@@ -0,0 +1,108 @@
+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);
+
+ // Only successfully signed-up user can see the authenticated content
+ const isLoggedIn = userStore.getState().isLoggedIn;
+ if (isLoggedIn) {
+ navigate("/home");
+ }
+ };
+
+ const onLogInClick = async (e) => {
+ e.preventDefault();
+
+ await storeHandleLogin(username, password);
+
+ // Only successfully signed-up user can see the authenticated content
+ const isLoggedIn = userStore.getState().isLoggedIn;
+ if (isLoggedIn) {
+ navigate("/home");
+ }
+ };
+
+ return (
+
+ );
+};
+
+export default Form;
\ No newline at end of file
diff --git a/frontend/src/components/SignOutButton.jsx b/frontend/src/components/SignOutButton.jsx
new file mode 100644
index 000000000..afb6b9d9b
--- /dev/null
+++ b/frontend/src/components/SignOutButton.jsx
@@ -0,0 +1,19 @@
+import { Link } from "react-router-dom";
+import { userStore } from "../stores/userStore";
+
+const SignOutButton = () => {
+ const storeHandleSignout = userStore((state) => state.handleSignOut);
+
+ const onSignOutClick = () => {
+ storeHandleSignout();
+ alert("Sign out successful");
+ };
+
+ return (
+
+ Sign Out
+
+ );
+};
+
+export default SignOutButton;
\ No newline at end of file
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 3e560a674..e9feeb131 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1,13 +1,158 @@
-:root {
+* {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
- "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
- sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
+ padding: 0;
+ box-sizing: border-box;
+ font-family: sans-serif;
}
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
- monospace;
+body {
+ text-align: center;
+}
+
+/* Form for sign up or log in */
+.form-container {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+}
+
+form {
+ padding: 1rem;
+ width: 100%;
+ border: 2px solid black;
+ border-radius: 10px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+h1 {
+ font-size: 2rem;
+ margin-bottom: 1rem;
+}
+
+.options {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ width: 80%;
+ gap: 2.5rem;
+ margin-bottom: 1rem;
+}
+
+.option {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+ font-size: 1.5rem;
+}
+
+.user-info-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ margin-bottom: 1rem;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 10px;
+}
+
+.user-info label {
+ font-size: 1rem;
+}
+
+.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;
+ padding: 0.5rem 1rem;
+ font-size: 1.5rem;
+ color: white;
+ border-radius: 0.25rem;
+ cursor: pointer;
+}
+
+.button-wrapper {
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+}
+
+.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%;
+ }
+
+ h1 {
+ font-size: 2.5rem;
+ }
+
+ .user-info label {
+ font-size: 1.5rem;
+ }
+
+ .info-field, .advert-info {
+ border: 2px solid #ccc;
+ border-radius: 0.25rem;
+ padding: 0.5rem;
+ font-size: 1.5rem;
+ max-width: 400px;
+ min-width: 300px;
+ }
}
\ No newline at end of file
diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx
new file mode 100644
index 000000000..636938852
--- /dev/null
+++ b/frontend/src/pages/Home.jsx
@@ -0,0 +1,34 @@
+import SignOutButton from "../components/SignOutButton";
+import Lottie from "lottie-react";
+import animation from "../assets/animation/Animation.json";
+import Adverts from "../sections/Adverts";
+
+const Home = () => {
+ // Create settings for animation
+ const options = {
+ animationData: animation,
+ style: {
+ height: 250
+ },
+ loop: true,
+ autoplay: true
+ }
+
+ return (
+
+
+
+
+
Welcome to your own world in GreenBuddy!
+
+
+
+ );
+};
+
+export default Home;
\ No newline at end of file
diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx
new file mode 100644
index 000000000..beda68088
--- /dev/null
+++ 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
new file mode 100644
index 000000000..7958dda4c
--- /dev/null
+++ b/frontend/src/routes/AppRoutes.jsx
@@ -0,0 +1 @@
+// Cannot deploy on Netlify as it doesn't find this file. Therefore, move everything to App.jsx
\ No newline at end of file
diff --git a/frontend/src/sections/Adverts.jsx b/frontend/src/sections/Adverts.jsx
new file mode 100644
index 000000000..13f4a96f7
--- /dev/null
+++ b/frontend/src/sections/Adverts.jsx
@@ -0,0 +1,38 @@
+import { useEffect } from "react";
+import { userStore } from "../stores/userStore";
+import { advertStore } from "../stores/advertStore";
+import AdvertCard from "../components/AdvertCard";
+import { CreateAdvert } from "../components/CreateAdvert";
+
+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..7d2674f1f
--- /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
new file mode 100644
index 000000000..f21d0557d
--- /dev/null
+++ b/frontend/src/stores/userStore.jsx
@@ -0,0 +1,109 @@
+import { create } from "zustand";
+
+// 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) => ({
+ // 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("https://hang-authentication-project.onrender.com/register", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ username, password, email })
+ });
+
+ const data = await response.json();
+
+ console.log(data);
+ if (data.success) {
+ set({
+ username,
+ password,
+ email,
+ isLoggedIn: true
+ });
+ 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) => {
+
+ if (!username || !password) {
+ alert("Please fill in all the fields");
+ return;
+ }
+
+ try {
+ const response = await fetch("https://hang-authentication-project.onrender.com/signin", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ username, password })
+ });
+
+ const data = await response.json();
+ console.log(data);
+
+ 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");
+ }
+}));
\ No newline at end of file
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({
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"
diff --git a/package.json b/package.json
index d774b8cc3..4978ac798 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
- "name": "project-auth-parent",
- "version": "1.0.0",
- "scripts": {
- "postinstall": "npm install --prefix backend"
- }
-}
+ "name": "authentication",
+ "version": "1.0.0",
+ "scripts": {
+ "postinstall": "npm install --prefix backend"
+ }
+ }
\ No newline at end of file