phone auth, image uploading, address broken up
This commit is contained in:
6
backend/.gitignore
vendored
Normal file
6
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
uploads/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
@@ -1,35 +1,34 @@
|
|||||||
const jwt = require('jsonwebtoken');
|
const jwt = require("jsonwebtoken");
|
||||||
const { User } = require('../models'); // Import from models/index.js to get models with associations
|
const { User } = require("../models"); // Import from models/index.js to get models with associations
|
||||||
|
|
||||||
const authenticateToken = async (req, res, next) => {
|
const authenticateToken = async (req, res, next) => {
|
||||||
const authHeader = req.headers['authorization'];
|
const authHeader = req.headers["authorization"];
|
||||||
const token = authHeader && authHeader.split(' ')[1];
|
const token = authHeader && authHeader.split(" ")[1];
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return res.status(401).json({ error: 'Access token required' });
|
return res.status(401).json({ error: "Access token required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
// Handle both 'userId' and 'id' for backward compatibility
|
const userId = decoded.id;
|
||||||
const userId = decoded.userId || decoded.id;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return res.status(401).json({ error: 'Invalid token format' });
|
return res.status(401).json({ error: "Invalid token format" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.findByPk(userId);
|
const user = await User.findByPk(userId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({ error: 'User not found' });
|
return res.status(401).json({ error: "User not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Auth middleware error:', error);
|
console.error("Auth middleware error:", error);
|
||||||
return res.status(403).json({ error: 'Invalid or expired token' });
|
return res.status(403).json({ error: "Invalid or expired token" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = { authenticateToken };
|
module.exports = { authenticateToken };
|
||||||
|
|||||||
40
backend/middleware/upload.js
Normal file
40
backend/middleware/upload.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
|
||||||
|
// Configure storage for profile images
|
||||||
|
const profileImageStorage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, path.join(__dirname, '../uploads/profiles'));
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
// Generate unique filename: uuid + original extension
|
||||||
|
const uniqueId = uuidv4();
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
cb(null, `${uniqueId}${ext}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File filter to accept only images
|
||||||
|
const imageFileFilter = (req, file, cb) => {
|
||||||
|
// Accept images only
|
||||||
|
const allowedMimes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
if (allowedMimes.includes(file.mimetype)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Invalid file type. Only JPEG, PNG, GIF and WebP images are allowed.'), false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create multer upload middleware for profile images
|
||||||
|
const uploadProfileImage = multer({
|
||||||
|
storage: profileImageStorage,
|
||||||
|
fileFilter: imageFileFilter,
|
||||||
|
limits: {
|
||||||
|
fileSize: 5 * 1024 * 1024 // 5MB limit
|
||||||
|
}
|
||||||
|
}).single('profileImage');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
uploadProfileImage
|
||||||
|
};
|
||||||
@@ -60,6 +60,24 @@ const Item = sequelize.define('Item', {
|
|||||||
type: DataTypes.STRING,
|
type: DataTypes.STRING,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
|
address1: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
address2: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
zipCode: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
type: DataTypes.STRING
|
||||||
|
},
|
||||||
latitude: {
|
latitude: {
|
||||||
type: DataTypes.DECIMAL(10, 8)
|
type: DataTypes.DECIMAL(10, 8)
|
||||||
},
|
},
|
||||||
|
|||||||
189
backend/package-lock.json
generated
189
backend/package-lock.json
generated
@@ -15,9 +15,11 @@
|
|||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"multer": "^2.0.2",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"sequelize-cli": "^6.6.3"
|
"sequelize-cli": "^6.6.3",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.10"
|
||||||
@@ -134,6 +136,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/append-field": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/at-least-node": {
|
"node_modules/at-least-node": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
|
||||||
@@ -216,6 +224,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-from": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/busboy": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||||
|
"dependencies": {
|
||||||
|
"streamsearch": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -382,6 +407,21 @@
|
|||||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/concat-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||||
|
"engines": [
|
||||||
|
"node >= 6.0"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.0.2",
|
||||||
|
"typedarray": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/config-chain": {
|
"node_modules/config-chain": {
|
||||||
"version": "1.1.13",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
|
||||||
@@ -1209,6 +1249,15 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minipass": {
|
"node_modules/minipass": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
@@ -1217,6 +1266,18 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
|
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "bin/cmd.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/moment": {
|
"node_modules/moment": {
|
||||||
"version": "2.30.1",
|
"version": "2.30.1",
|
||||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||||
@@ -1241,6 +1302,67 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/multer": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"append-field": "^1.0.0",
|
||||||
|
"busboy": "^1.6.0",
|
||||||
|
"concat-stream": "^2.0.0",
|
||||||
|
"mkdirp": "^0.5.6",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"type-is": "^1.6.18",
|
||||||
|
"xtend": "^4.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/multer/node_modules/type-is": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": "0.3.0",
|
||||||
|
"mime-types": "~2.1.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
@@ -1601,6 +1723,20 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"string_decoder": "^1.1.1",
|
||||||
|
"util-deprecate": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@@ -1806,6 +1942,15 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sequelize/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
|
||||||
@@ -1951,6 +2096,23 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamsearch": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
@@ -2109,6 +2271,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typedarray": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/umzug": {
|
"node_modules/umzug": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz",
|
||||||
@@ -2147,12 +2315,23 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/validator": {
|
"node_modules/validator": {
|
||||||
|
|||||||
@@ -22,9 +22,11 @@
|
|||||||
"dotenv": "^17.2.0",
|
"dotenv": "^17.2.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"multer": "^2.0.2",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"sequelize": "^6.37.7",
|
"sequelize": "^6.37.7",
|
||||||
"sequelize-cli": "^6.6.3"
|
"sequelize-cli": "^6.6.3",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.1.10"
|
"nodemon": "^3.1.10"
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
const express = require('express');
|
const express = require("express");
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require("jsonwebtoken");
|
||||||
const { User } = require('../models'); // Import from models/index.js to get models with associations
|
const { User } = require("../models"); // Import from models/index.js to get models with associations
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.post('/register', async (req, res) => {
|
router.post("/register", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { username, email, password, firstName, lastName, phone } = req.body;
|
const { username, email, password, firstName, lastName, phone } = req.body;
|
||||||
|
|
||||||
const existingUser = await User.findOne({
|
const existingUser = await User.findOne({
|
||||||
where: {
|
where: {
|
||||||
[require('sequelize').Op.or]: [{ email }, { username }]
|
[require("sequelize").Op.or]: [{ email }, { username }],
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return res.status(400).json({ error: 'User already exists' });
|
return res.status(400).json({ error: "User already exists" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
@@ -23,11 +23,11 @@ router.post('/register', async (req, res) => {
|
|||||||
password,
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
phone
|
phone,
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
|
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
|
||||||
expiresIn: '7d'
|
expiresIn: "7d",
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
@@ -36,27 +36,27 @@ router.post('/register', async (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName
|
lastName: user.lastName,
|
||||||
},
|
},
|
||||||
token
|
token,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/login', async (req, res) => {
|
router.post("/login", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
|
||||||
const user = await User.findOne({ where: { email } });
|
const user = await User.findOne({ where: { email } });
|
||||||
|
|
||||||
if (!user || !(await user.comparePassword(password))) {
|
if (!user || !(await user.comparePassword(password))) {
|
||||||
return res.status(401).json({ error: 'Invalid credentials' });
|
return res.status(401).json({ error: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
|
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
|
||||||
expiresIn: '7d'
|
expiresIn: "7d",
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -65,13 +65,13 @@ router.post('/login', async (req, res) => {
|
|||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName
|
lastName: user.lastName,
|
||||||
},
|
},
|
||||||
token
|
token,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -59,31 +59,25 @@ router.post("/verify-code", async (req, res) => {
|
|||||||
const storedData = verificationCodes.get(phoneNumber);
|
const storedData = verificationCodes.get(phoneNumber);
|
||||||
|
|
||||||
if (!storedData) {
|
if (!storedData) {
|
||||||
return res
|
return res.status(400).json({
|
||||||
.status(400)
|
message: "No verification code found. Please request a new one.",
|
||||||
.json({
|
});
|
||||||
message: "No verification code found. Please request a new one.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if code expired (10 minutes)
|
// Check if code expired (10 minutes)
|
||||||
if (Date.now() - storedData.createdAt > 10 * 60 * 1000) {
|
if (Date.now() - storedData.createdAt > 10 * 60 * 1000) {
|
||||||
verificationCodes.delete(phoneNumber);
|
verificationCodes.delete(phoneNumber);
|
||||||
return res
|
return res.status(400).json({
|
||||||
.status(400)
|
message: "Verification code expired. Please request a new one.",
|
||||||
.json({
|
});
|
||||||
message: "Verification code expired. Please request a new one.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check attempts
|
// Check attempts
|
||||||
if (storedData.attempts >= 3) {
|
if (storedData.attempts >= 3) {
|
||||||
verificationCodes.delete(phoneNumber);
|
verificationCodes.delete(phoneNumber);
|
||||||
return res
|
return res.status(400).json({
|
||||||
.status(400)
|
message: "Too many failed attempts. Please request a new code.",
|
||||||
.json({
|
});
|
||||||
message: "Too many failed attempts. Please request a new code.",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storedData.code !== code) {
|
if (storedData.code !== code) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { User } = require('../models'); // Import from models/index.js to get models with associations
|
const { User } = require('../models'); // Import from models/index.js to get models with associations
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
|
const { uploadProfileImage } = require('../middleware/upload');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const path = require('path');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/profile', authenticateToken, async (req, res) => {
|
router.get('/profile', authenticateToken, async (req, res) => {
|
||||||
@@ -32,15 +35,38 @@ router.get('/:id', async (req, res) => {
|
|||||||
|
|
||||||
router.put('/profile', authenticateToken, async (req, res) => {
|
router.put('/profile', authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { firstName, lastName, phone, address, profileImage } = req.body;
|
const {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
address1,
|
||||||
|
address2,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
zipCode,
|
||||||
|
country
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
await req.user.update({
|
// Build update object, excluding empty email
|
||||||
|
const updateData = {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
phone,
|
phone,
|
||||||
address,
|
address1,
|
||||||
profileImage
|
address2,
|
||||||
});
|
city,
|
||||||
|
state,
|
||||||
|
zipCode,
|
||||||
|
country
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only include email if it's not empty
|
||||||
|
if (email && email.trim() !== '') {
|
||||||
|
updateData.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
await req.user.update(updateData);
|
||||||
|
|
||||||
const updatedUser = await User.findByPk(req.user.id, {
|
const updatedUser = await User.findByPk(req.user.id, {
|
||||||
attributes: { exclude: ['password'] }
|
attributes: { exclude: ['password'] }
|
||||||
@@ -48,8 +74,53 @@ router.put('/profile', authenticateToken, async (req, res) => {
|
|||||||
|
|
||||||
res.json(updatedUser);
|
res.json(updatedUser);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
console.error('Profile update error:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
error: error.message,
|
||||||
|
details: error.errors ? error.errors.map(e => ({ field: e.path, message: e.message })) : undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Upload profile image endpoint
|
||||||
|
router.post('/profile/image', authenticateToken, (req, res) => {
|
||||||
|
uploadProfileImage(req, res, async (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Upload error:', err);
|
||||||
|
return res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Delete old profile image if exists
|
||||||
|
const user = await User.findByPk(req.user.id);
|
||||||
|
if (user.profileImage) {
|
||||||
|
const oldImagePath = path.join(__dirname, '../uploads/profiles', user.profileImage);
|
||||||
|
try {
|
||||||
|
await fs.unlink(oldImagePath);
|
||||||
|
} catch (unlinkErr) {
|
||||||
|
console.error('Error deleting old image:', unlinkErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user with new image filename
|
||||||
|
await user.update({
|
||||||
|
profileImage: req.file.filename
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
message: 'Profile image uploaded successfully',
|
||||||
|
filename: req.file.filename,
|
||||||
|
imageUrl: `/uploads/profiles/${req.file.filename}`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database update error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to update profile image' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -8,6 +8,7 @@ require('dotenv').config({
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const bodyParser = require('body-parser');
|
const bodyParser = require('body-parser');
|
||||||
|
const path = require('path');
|
||||||
const { sequelize } = require('./models'); // Import from models/index.js to ensure associations are loaded
|
const { sequelize } = require('./models'); // Import from models/index.js to ensure associations are loaded
|
||||||
|
|
||||||
const authRoutes = require('./routes/auth');
|
const authRoutes = require('./routes/auth');
|
||||||
@@ -20,8 +21,11 @@ const messageRoutes = require('./routes/messages');
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json({ limit: '5mb' }));
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
app.use(bodyParser.urlencoded({ extended: true, limit: '5mb' }));
|
||||||
|
|
||||||
|
// Serve static files from uploads directory
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||||
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/auth/phone', phoneAuthRoutes);
|
app.use('/api/auth/phone', phoneAuthRoutes);
|
||||||
|
|||||||
@@ -134,16 +134,16 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
|||||||
console.log("Current mode:", mode);
|
console.log("Current mode:", mode);
|
||||||
console.log("First Name:", firstName);
|
console.log("First Name:", firstName);
|
||||||
console.log("Last Name:", lastName);
|
console.log("Last Name:", lastName);
|
||||||
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
phoneNumber: cleanPhone,
|
phoneNumber: cleanPhone,
|
||||||
code: verificationCode,
|
code: verificationCode,
|
||||||
firstName: mode === "signup" ? firstName : undefined,
|
firstName: mode === "signup" ? firstName : undefined,
|
||||||
lastName: mode === "signup" ? lastName : undefined,
|
lastName: mode === "signup" ? lastName : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Request body:", requestBody);
|
console.log("Request body:", requestBody);
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"http://localhost:5001/api/auth/phone/verify-code",
|
"http://localhost:5001/api/auth/phone/verify-code",
|
||||||
{
|
{
|
||||||
@@ -171,23 +171,23 @@ const AuthModal: React.FC<AuthModalProps> = ({
|
|||||||
// Store token and user data
|
// Store token and user data
|
||||||
console.log("Storing token:", data.token);
|
console.log("Storing token:", data.token);
|
||||||
localStorage.setItem("token", data.token);
|
localStorage.setItem("token", data.token);
|
||||||
|
|
||||||
// Verify token was stored
|
// Verify token was stored
|
||||||
const storedToken = localStorage.getItem("token");
|
const storedToken = localStorage.getItem("token");
|
||||||
console.log("Token stored successfully:", !!storedToken);
|
console.log("Token stored successfully:", !!storedToken);
|
||||||
console.log("User data:", data.user);
|
console.log("User data:", data.user);
|
||||||
|
|
||||||
// Update auth context with the user data
|
// Update auth context with the user data
|
||||||
updateUser(data.user);
|
updateUser(data.user);
|
||||||
|
|
||||||
// Close modal and reset state
|
// Close modal and reset state
|
||||||
onHide();
|
onHide();
|
||||||
resetModal();
|
resetModal();
|
||||||
|
|
||||||
// Force a page reload to ensure auth state is properly initialized
|
// Force a page reload to ensure auth state is properly initialized
|
||||||
// This is needed because AuthContext's useEffect only runs once on mount
|
// This is needed because AuthContext's useEffect only runs once on mount
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '/';
|
window.location.href = "/";
|
||||||
}, 100);
|
}, 100);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Verification error:", err);
|
console.error("Verification error:", err);
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import React, { createContext, useState, useContext, useEffect, ReactNode } from 'react';
|
import React, {
|
||||||
import { User } from '../types';
|
createContext,
|
||||||
import { authAPI, userAPI } from '../services/api';
|
useState,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { User } from "../types";
|
||||||
|
import { authAPI, userAPI } from "../services/api";
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: User | null;
|
user: User | null;
|
||||||
@@ -16,7 +22,7 @@ const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useAuth must be used within an AuthProvider');
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
@@ -30,41 +36,38 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
console.log('AuthContext: Found token, fetching profile...');
|
userAPI
|
||||||
userAPI.getProfile()
|
.getProfile()
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
console.log('AuthContext: Profile loaded', response.data);
|
|
||||||
setUser(response.data);
|
setUser(response.data);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('AuthContext: Failed to load profile', error);
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem('token');
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log('AuthContext: No token found');
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const login = async (email: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
const response = await authAPI.login({ email, password });
|
const response = await authAPI.login({ email, password });
|
||||||
localStorage.setItem('token', response.data.token);
|
localStorage.setItem("token", response.data.token);
|
||||||
setUser(response.data.user);
|
setUser(response.data.user);
|
||||||
};
|
};
|
||||||
|
|
||||||
const register = async (data: any) => {
|
const register = async (data: any) => {
|
||||||
const response = await authAPI.register(data);
|
const response = await authAPI.register(data);
|
||||||
localStorage.setItem('token', response.data.token);
|
localStorage.setItem("token", response.data.token);
|
||||||
setUser(response.data.user);
|
setUser(response.data.user);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem("token");
|
||||||
setUser(null);
|
setUser(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,8 +76,10 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ user, loading, login, register, logout, updateUser }}>
|
<AuthContext.Provider
|
||||||
|
value={{ user, loading, login, register, logout, updateUser }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useAuth } from "../contexts/AuthContext";
|
import { useAuth } from "../contexts/AuthContext";
|
||||||
import api from "../services/api";
|
import api from "../services/api";
|
||||||
import AvailabilityCalendar from "../components/AvailabilityCalendar";
|
import AvailabilityCalendar from "../components/AvailabilityCalendar";
|
||||||
import AddressAutocomplete from "../components/AddressAutocomplete";
|
|
||||||
|
|
||||||
interface ItemFormData {
|
interface ItemFormData {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -18,6 +17,12 @@ interface ItemFormData {
|
|||||||
pricePerDay?: number;
|
pricePerDay?: number;
|
||||||
replacementCost: number;
|
replacementCost: number;
|
||||||
location: string;
|
location: string;
|
||||||
|
address1: string;
|
||||||
|
address2: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
zipCode: string;
|
||||||
|
country: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rules?: string;
|
rules?: string;
|
||||||
@@ -49,6 +54,12 @@ const CreateItem: React.FC = () => {
|
|||||||
pricePerDay: undefined,
|
pricePerDay: undefined,
|
||||||
replacementCost: 0,
|
replacementCost: 0,
|
||||||
location: "",
|
location: "",
|
||||||
|
address1: "",
|
||||||
|
address2: "",
|
||||||
|
city: "",
|
||||||
|
state: "",
|
||||||
|
zipCode: "",
|
||||||
|
country: "",
|
||||||
minimumRentalDays: 1,
|
minimumRentalDays: 1,
|
||||||
needsTraining: false,
|
needsTraining: false,
|
||||||
unavailablePeriods: [],
|
unavailablePeriods: [],
|
||||||
@@ -73,8 +84,21 @@ const CreateItem: React.FC = () => {
|
|||||||
// In production, you'd upload to a service like S3
|
// In production, you'd upload to a service like S3
|
||||||
const imageUrls = imagePreviews;
|
const imageUrls = imagePreviews;
|
||||||
|
|
||||||
|
// Construct location from address components
|
||||||
|
const locationParts = [
|
||||||
|
formData.address1,
|
||||||
|
formData.address2,
|
||||||
|
formData.city,
|
||||||
|
formData.state,
|
||||||
|
formData.zipCode,
|
||||||
|
formData.country
|
||||||
|
].filter(part => part && part.trim());
|
||||||
|
|
||||||
|
const location = locationParts.join(', ');
|
||||||
|
|
||||||
const response = await api.post("/items", {
|
const response = await api.post("/items", {
|
||||||
...formData,
|
...formData,
|
||||||
|
location,
|
||||||
images: imageUrls,
|
images: imageUrls,
|
||||||
});
|
});
|
||||||
navigate(`/items/${response.data.id}`);
|
navigate(`/items/${response.data.id}`);
|
||||||
@@ -271,23 +295,99 @@ const CreateItem: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h6 className="mb-3">Location *</h6>
|
||||||
|
|
||||||
|
<div className="row mb-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="address1" className="form-label">
|
||||||
|
Address Line 1
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="address1"
|
||||||
|
name="address1"
|
||||||
|
value={formData.address1}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="123 Main Street"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="address2" className="form-label">
|
||||||
|
Address Line 2
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="address2"
|
||||||
|
name="address2"
|
||||||
|
value={formData.address2}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Apt, Suite, Unit, etc."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="city" className="form-label">
|
||||||
|
City
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="city"
|
||||||
|
name="city"
|
||||||
|
value={formData.city}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3">
|
||||||
|
<label htmlFor="state" className="form-label">
|
||||||
|
State
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="state"
|
||||||
|
name="state"
|
||||||
|
value={formData.state}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="CA"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3">
|
||||||
|
<label htmlFor="zipCode" className="form-label">
|
||||||
|
ZIP Code
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="zipCode"
|
||||||
|
name="zipCode"
|
||||||
|
value={formData.zipCode}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="12345"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label htmlFor="location" className="form-label">
|
<label htmlFor="country" className="form-label">
|
||||||
Location *
|
Country
|
||||||
</label>
|
</label>
|
||||||
<AddressAutocomplete
|
<input
|
||||||
id="location"
|
type="text"
|
||||||
name="location"
|
className="form-control"
|
||||||
value={formData.location}
|
id="country"
|
||||||
onChange={(value, lat, lon) => {
|
name="country"
|
||||||
setFormData(prev => ({
|
value={formData.country}
|
||||||
...prev,
|
onChange={handleChange}
|
||||||
location: value,
|
placeholder="United States"
|
||||||
latitude: lat,
|
|
||||||
longitude: lon
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
placeholder="Address"
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,7 +405,10 @@ const CreateItem: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
<label className="form-check-label" htmlFor="pickUpAvailable">
|
<label className="form-check-label" htmlFor="pickUpAvailable">
|
||||||
Pick-Up
|
Pick-Up
|
||||||
<div className="small text-muted">They pick-up the item from your location and they return the item to your location</div>
|
<div className="small text-muted">
|
||||||
|
They pick-up the item from your location and they return the
|
||||||
|
item to your location
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
@@ -325,24 +428,27 @@ const CreateItem: React.FC = () => {
|
|||||||
Local Delivery
|
Local Delivery
|
||||||
{formData.localDeliveryAvailable && (
|
{formData.localDeliveryAvailable && (
|
||||||
<span className="ms-2">
|
<span className="ms-2">
|
||||||
(Delivery Radius:
|
(Delivery Radius:
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="form-control form-control-sm d-inline-block mx-1"
|
className="form-control form-control-sm d-inline-block mx-1"
|
||||||
id="localDeliveryRadius"
|
id="localDeliveryRadius"
|
||||||
name="localDeliveryRadius"
|
name="localDeliveryRadius"
|
||||||
value={formData.localDeliveryRadius || ''}
|
value={formData.localDeliveryRadius || ""}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
placeholder="25"
|
placeholder="25"
|
||||||
min="1"
|
min="1"
|
||||||
max="100"
|
max="100"
|
||||||
style={{ width: '60px' }}
|
style={{ width: "60px" }}
|
||||||
/>
|
/>
|
||||||
miles)
|
miles)
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="small text-muted">You deliver and then pick-up the item when the rental period ends</div>
|
<div className="small text-muted">
|
||||||
|
You deliver and then pick-up the item when the rental
|
||||||
|
period ends
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,7 +479,9 @@ const CreateItem: React.FC = () => {
|
|||||||
htmlFor="inPlaceUseAvailable"
|
htmlFor="inPlaceUseAvailable"
|
||||||
>
|
>
|
||||||
In-Place Use
|
In-Place Use
|
||||||
<div className="small text-muted">They use at your location</div>
|
<div className="small text-muted">
|
||||||
|
They use at your location
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,7 +495,9 @@ const CreateItem: React.FC = () => {
|
|||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={priceType}
|
value={priceType}
|
||||||
onChange={(e) => setPriceType(e.target.value as "hour" | "day")}
|
onChange={(e) =>
|
||||||
|
setPriceType(e.target.value as "hour" | "day")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<option value="hour">Hour</option>
|
<option value="hour">Hour</option>
|
||||||
<option value="day">Day</option>
|
<option value="day">Day</option>
|
||||||
@@ -400,8 +510,14 @@ const CreateItem: React.FC = () => {
|
|||||||
type="number"
|
type="number"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
id={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
||||||
name={priceType === "hour" ? "pricePerHour" : "pricePerDay"}
|
name={
|
||||||
value={priceType === "hour" ? (formData.pricePerHour || "") : (formData.pricePerDay || "")}
|
priceType === "hour" ? "pricePerHour" : "pricePerDay"
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
priceType === "hour"
|
||||||
|
? formData.pricePerHour || ""
|
||||||
|
: formData.pricePerDay || ""
|
||||||
|
}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
step="0.01"
|
step="0.01"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -429,11 +545,16 @@ const CreateItem: React.FC = () => {
|
|||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h5>Availability Schedule</h5>
|
<h5>Availability Schedule</h5>
|
||||||
<p className="text-muted">Select dates when the item is NOT available for rent</p>
|
<p className="text-muted">
|
||||||
|
Select dates when the item is NOT available for rent
|
||||||
|
</p>
|
||||||
<AvailabilityCalendar
|
<AvailabilityCalendar
|
||||||
unavailablePeriods={formData.unavailablePeriods || []}
|
unavailablePeriods={formData.unavailablePeriods || []}
|
||||||
onPeriodsChange={(periods) =>
|
onPeriodsChange={(periods) =>
|
||||||
setFormData(prev => ({ ...prev, unavailablePeriods: periods }))
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
unavailablePeriods: periods,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
mode="owner"
|
mode="owner"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { userAPI, itemAPI, rentalAPI } from '../services/api';
|
import { userAPI, itemAPI, rentalAPI } from '../services/api';
|
||||||
import { User, Item, Rental } from '../types';
|
import { User, Item, Rental } from '../types';
|
||||||
import AddressAutocomplete from '../components/AddressAutocomplete';
|
import { getImageUrl } from '../utils/imageUrl';
|
||||||
|
|
||||||
const Profile: React.FC = () => {
|
const Profile: React.FC = () => {
|
||||||
const { user, updateUser, logout } = useAuth();
|
const { user, updateUser, logout } = useAuth();
|
||||||
@@ -16,7 +16,12 @@ const Profile: React.FC = () => {
|
|||||||
lastName: '',
|
lastName: '',
|
||||||
email: '',
|
email: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
address: '',
|
address1: '',
|
||||||
|
address2: '',
|
||||||
|
city: '',
|
||||||
|
state: '',
|
||||||
|
zipCode: '',
|
||||||
|
country: '',
|
||||||
profileImage: ''
|
profileImage: ''
|
||||||
});
|
});
|
||||||
const [imageFile, setImageFile] = useState<File | null>(null);
|
const [imageFile, setImageFile] = useState<File | null>(null);
|
||||||
@@ -41,11 +46,16 @@ const Profile: React.FC = () => {
|
|||||||
lastName: response.data.lastName || '',
|
lastName: response.data.lastName || '',
|
||||||
email: response.data.email || '',
|
email: response.data.email || '',
|
||||||
phone: response.data.phone || '',
|
phone: response.data.phone || '',
|
||||||
address: response.data.address || '',
|
address1: response.data.address1 || '',
|
||||||
|
address2: response.data.address2 || '',
|
||||||
|
city: response.data.city || '',
|
||||||
|
state: response.data.state || '',
|
||||||
|
zipCode: response.data.zipCode || '',
|
||||||
|
country: response.data.country || '',
|
||||||
profileImage: response.data.profileImage || ''
|
profileImage: response.data.profileImage || ''
|
||||||
});
|
});
|
||||||
if (response.data.profileImage) {
|
if (response.data.profileImage) {
|
||||||
setImagePreview(response.data.profileImage);
|
setImagePreview(getImageUrl(response.data.profileImage));
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || 'Failed to fetch profile');
|
setError(err.response?.data?.message || 'Failed to fetch profile');
|
||||||
@@ -82,15 +92,43 @@ const Profile: React.FC = () => {
|
|||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
setFormData(prev => ({ ...prev, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
setImageFile(file);
|
setImageFile(file);
|
||||||
|
|
||||||
|
// Show preview
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onloadend = () => {
|
reader.onloadend = () => {
|
||||||
setImagePreview(reader.result as string);
|
setImagePreview(reader.result as string);
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
// Upload image immediately
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('profileImage', file);
|
||||||
|
|
||||||
|
const response = await userAPI.uploadProfileImage(formData);
|
||||||
|
|
||||||
|
// Update the profileImage in formData with the new filename
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
profileImage: response.data.filename
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update preview to use the uploaded image URL
|
||||||
|
setImagePreview(getImageUrl(response.data.imageUrl));
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('Image upload error:', err);
|
||||||
|
setError(err.response?.data?.error || 'Failed to upload image');
|
||||||
|
// Reset on error
|
||||||
|
setImageFile(null);
|
||||||
|
setImagePreview(profileData?.profileImage ?
|
||||||
|
getImageUrl(profileData.profileImage) :
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -100,18 +138,24 @@ const Profile: React.FC = () => {
|
|||||||
setSuccess(null);
|
setSuccess(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updateData = {
|
// Don't send profileImage in the update data as it's handled separately
|
||||||
...formData,
|
const { profileImage, ...updateData } = formData;
|
||||||
profileImage: imagePreview || formData.profileImage
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await userAPI.updateProfile(updateData);
|
const response = await userAPI.updateProfile(updateData);
|
||||||
setProfileData(response.data);
|
setProfileData(response.data);
|
||||||
updateUser(response.data); // Update the auth context
|
updateUser(response.data); // Update the auth context
|
||||||
setSuccess('Profile updated successfully!');
|
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.response?.data?.message || 'Failed to update profile');
|
console.error('Profile update error:', err.response?.data);
|
||||||
|
const errorMessage = err.response?.data?.error || err.response?.data?.message || 'Failed to update profile';
|
||||||
|
const errorDetails = err.response?.data?.details;
|
||||||
|
|
||||||
|
if (errorDetails && Array.isArray(errorDetails)) {
|
||||||
|
const detailMessages = errorDetails.map((d: any) => `${d.field}: ${d.message}`).join(', ');
|
||||||
|
setError(`${errorMessage} - ${detailMessages}`);
|
||||||
|
} else {
|
||||||
|
setError(errorMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,10 +170,18 @@ const Profile: React.FC = () => {
|
|||||||
lastName: profileData.lastName || '',
|
lastName: profileData.lastName || '',
|
||||||
email: profileData.email || '',
|
email: profileData.email || '',
|
||||||
phone: profileData.phone || '',
|
phone: profileData.phone || '',
|
||||||
address: profileData.address || '',
|
address1: profileData.address1 || '',
|
||||||
|
address2: profileData.address2 || '',
|
||||||
|
city: profileData.city || '',
|
||||||
|
state: profileData.state || '',
|
||||||
|
zipCode: profileData.zipCode || '',
|
||||||
|
country: profileData.country || '',
|
||||||
profileImage: profileData.profileImage || ''
|
profileImage: profileData.profileImage || ''
|
||||||
});
|
});
|
||||||
setImagePreview(profileData.profileImage || null);
|
setImagePreview(profileData.profileImage ?
|
||||||
|
getImageUrl(profileData.profileImage) :
|
||||||
|
null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,7 +300,6 @@ const Profile: React.FC = () => {
|
|||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={!editing}
|
disabled={!editing}
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -266,30 +317,88 @@ const Profile: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="row mb-3">
|
||||||
<label htmlFor="address" className="form-label">Address</label>
|
<div className="col-md-6">
|
||||||
{editing ? (
|
<label htmlFor="address1" className="form-label">Address Line 1</label>
|
||||||
<AddressAutocomplete
|
|
||||||
id="address"
|
|
||||||
name="address"
|
|
||||||
value={formData.address}
|
|
||||||
onChange={(value) => {
|
|
||||||
setFormData(prev => ({ ...prev, address: value }));
|
|
||||||
}}
|
|
||||||
placeholder="Enter your address"
|
|
||||||
required={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
id="address"
|
id="address1"
|
||||||
name="address"
|
name="address1"
|
||||||
value={formData.address}
|
value={formData.address1}
|
||||||
disabled
|
onChange={handleChange}
|
||||||
readOnly
|
placeholder="123 Main Street"
|
||||||
|
disabled={!editing}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="address2" className="form-label">Address Line 2</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="address2"
|
||||||
|
name="address2"
|
||||||
|
value={formData.address2}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Apt, Suite, Unit, etc."
|
||||||
|
disabled={!editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row mb-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label htmlFor="city" className="form-label">City</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="city"
|
||||||
|
name="city"
|
||||||
|
value={formData.city}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={!editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3">
|
||||||
|
<label htmlFor="state" className="form-label">State</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="state"
|
||||||
|
name="state"
|
||||||
|
value={formData.state}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="CA"
|
||||||
|
disabled={!editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-3">
|
||||||
|
<label htmlFor="zipCode" className="form-label">ZIP Code</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="zipCode"
|
||||||
|
name="zipCode"
|
||||||
|
value={formData.zipCode}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="12345"
|
||||||
|
disabled={!editing}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label htmlFor="country" className="form-label">Country</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="country"
|
||||||
|
name="country"
|
||||||
|
value={formData.country}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="United States"
|
||||||
|
disabled={!editing}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editing ? (
|
{editing ? (
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
|
|
||||||
const API_BASE_URL = process.env.REACT_APP_API_URL;
|
const API_BASE_URL = process.env.REACT_APP_API_URL;
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_BASE_URL,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,12 @@ api.interceptors.response.use(
|
|||||||
(error) => {
|
(error) => {
|
||||||
if (error.response?.status === 401) {
|
if (error.response?.status === 401) {
|
||||||
// Only redirect to login if we have a token (user was logged in)
|
// Only redirect to login if we have a token (user was logged in)
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
// User was logged in but token expired/invalid
|
// User was logged in but token expired/invalid
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem("token");
|
||||||
window.location.href = '/login';
|
window.location.href = "/login";
|
||||||
}
|
}
|
||||||
// For non-authenticated users, just reject the error without redirecting
|
// For non-authenticated users, just reject the error without redirecting
|
||||||
// Let individual components handle 401 errors as needed
|
// Let individual components handle 401 errors as needed
|
||||||
@@ -37,42 +37,47 @@ api.interceptors.response.use(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const authAPI = {
|
export const authAPI = {
|
||||||
register: (data: any) => api.post('/auth/register', data),
|
register: (data: any) => api.post("/auth/register", data),
|
||||||
login: (data: any) => api.post('/auth/login', data),
|
login: (data: any) => api.post("/auth/login", data),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userAPI = {
|
export const userAPI = {
|
||||||
getProfile: () => api.get('/users/profile'),
|
getProfile: () => api.get("/users/profile"),
|
||||||
updateProfile: (data: any) => api.put('/users/profile', data),
|
updateProfile: (data: any) => api.put("/users/profile", data),
|
||||||
getPublicProfile: (userId: string) => api.get(`/users/${userId}`),
|
uploadProfileImage: (formData: FormData) =>
|
||||||
|
api.post("/users/profile/image", formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getPublicProfile: (id: string) => api.get(`/users/${id}`),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const itemAPI = {
|
export const itemAPI = {
|
||||||
getItems: (params?: any) => api.get('/items', { params }),
|
getItems: (params?: any) => api.get("/items", { params }),
|
||||||
getItem: (id: string) => api.get(`/items/${id}`),
|
getItem: (id: string) => api.get(`/items/${id}`),
|
||||||
createItem: (data: any) => api.post('/items', data),
|
createItem: (data: any) => api.post("/items", data),
|
||||||
updateItem: (id: string, data: any) => api.put(`/items/${id}`, data),
|
updateItem: (id: string, data: any) => api.put(`/items/${id}`, data),
|
||||||
deleteItem: (id: string) => api.delete(`/items/${id}`),
|
deleteItem: (id: string) => api.delete(`/items/${id}`),
|
||||||
getRecommendations: () => api.get('/items/recommendations'),
|
getRecommendations: () => api.get("/items/recommendations"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rentalAPI = {
|
export const rentalAPI = {
|
||||||
createRental: (data: any) => api.post('/rentals', data),
|
createRental: (data: any) => api.post("/rentals", data),
|
||||||
getMyRentals: () => api.get('/rentals/my-rentals'),
|
getMyRentals: () => api.get("/rentals/my-rentals"),
|
||||||
getMyListings: () => api.get('/rentals/my-listings'),
|
getMyListings: () => api.get("/rentals/my-listings"),
|
||||||
updateRentalStatus: (id: string, status: string) =>
|
updateRentalStatus: (id: string, status: string) =>
|
||||||
api.put(`/rentals/${id}/status`, { status }),
|
api.put(`/rentals/${id}/status`, { status }),
|
||||||
addReview: (id: string, data: any) =>
|
addReview: (id: string, data: any) => api.post(`/rentals/${id}/review`, data),
|
||||||
api.post(`/rentals/${id}/review`, data),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageAPI = {
|
export const messageAPI = {
|
||||||
getMessages: () => api.get('/messages'),
|
getMessages: () => api.get("/messages"),
|
||||||
getSentMessages: () => api.get('/messages/sent'),
|
getSentMessages: () => api.get("/messages/sent"),
|
||||||
getMessage: (id: string) => api.get(`/messages/${id}`),
|
getMessage: (id: string) => api.get(`/messages/${id}`),
|
||||||
sendMessage: (data: any) => api.post('/messages', data),
|
sendMessage: (data: any) => api.post("/messages", data),
|
||||||
markAsRead: (id: string) => api.put(`/messages/${id}/read`),
|
markAsRead: (id: string) => api.put(`/messages/${id}/read`),
|
||||||
getUnreadCount: () => api.get('/messages/unread/count'),
|
getUnreadCount: () => api.get("/messages/unread/count"),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ export interface User {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
address?: string;
|
address1?: string;
|
||||||
|
address2?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
zipCode?: string;
|
||||||
|
country?: string;
|
||||||
profileImage?: string;
|
profileImage?: string;
|
||||||
isVerified: boolean;
|
isVerified: boolean;
|
||||||
}
|
}
|
||||||
@@ -42,10 +47,16 @@ export interface Item {
|
|||||||
pricePerMonth?: number;
|
pricePerMonth?: number;
|
||||||
replacementCost: number;
|
replacementCost: number;
|
||||||
location: string;
|
location: string;
|
||||||
|
address1?: string;
|
||||||
|
address2?: string;
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
zipCode?: string;
|
||||||
|
country?: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
images: string[];
|
images: string[];
|
||||||
condition: 'excellent' | 'good' | 'fair' | 'poor';
|
condition: "excellent" | "good" | "fair" | "poor";
|
||||||
availability: boolean;
|
availability: boolean;
|
||||||
specifications: Record<string, any>;
|
specifications: Record<string, any>;
|
||||||
rules?: string;
|
rules?: string;
|
||||||
@@ -73,9 +84,9 @@ export interface Rental {
|
|||||||
startDate: string;
|
startDate: string;
|
||||||
endDate: string;
|
endDate: string;
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
status: 'pending' | 'confirmed' | 'active' | 'completed' | 'cancelled';
|
status: "pending" | "confirmed" | "active" | "completed" | "cancelled";
|
||||||
paymentStatus: 'pending' | 'paid' | 'refunded';
|
paymentStatus: "pending" | "paid" | "refunded";
|
||||||
deliveryMethod: 'pickup' | 'delivery';
|
deliveryMethod: "pickup" | "delivery";
|
||||||
deliveryAddress?: string;
|
deliveryAddress?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
@@ -86,4 +97,4 @@ export interface Rental {
|
|||||||
owner?: User;
|
owner?: User;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
13
frontend/src/utils/imageUrl.ts
Normal file
13
frontend/src/utils/imageUrl.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const getImageUrl = (imagePath: string): string => {
|
||||||
|
// Get the base URL without /api
|
||||||
|
const apiUrl = process.env.REACT_APP_API_URL || '';
|
||||||
|
const baseUrl = apiUrl.replace('/api', '');
|
||||||
|
|
||||||
|
// If imagePath already includes the full path, use it
|
||||||
|
if (imagePath.startsWith('/uploads/')) {
|
||||||
|
return `${baseUrl}${imagePath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, construct the full path
|
||||||
|
return `${baseUrl}/uploads/profiles/${imagePath}`;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user