diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..304332f --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,10 @@ +PORT=5000 +MONGO_URI=mongodb://localhost:27017/uniloot + +# JWT configuration +JWT_SECRET=supersecretkey123 +JWT_REFRESH_SECRET=anothersecretkey456 + +# Optional +NODE_ENV=development +FIREBASE_STORAGE_BUCKET=get-from-firebaseconsole \ No newline at end of file diff --git a/backend/firebase-service-account.json b/backend/firebase-service-account.json new file mode 100644 index 0000000..b62ceab --- /dev/null +++ b/backend/firebase-service-account.json @@ -0,0 +1,3 @@ +{ + "get it from your firebase console to use store image feature" +} \ No newline at end of file diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts new file mode 100644 index 0000000..113e05c --- /dev/null +++ b/backend/src/controllers/auth.ts @@ -0,0 +1,39 @@ +import jwt, { SignOptions, JwtPayload } from "jsonwebtoken"; +import dotenv from "dotenv"; +dotenv.config(); + +if (!process.env.JWT_SECRET || !process.env.JWT_REFRESH_SECRET) { + throw new Error("JWT secrets missing in environment variables"); +} +const accessSecret = process.env.JWT_SECRET as string; +const refreshSecret = process.env.JWT_REFRESH_SECRET as string; + +export interface TokenPayload extends JwtPayload { + sub: string; + username?: string; + role?: string; +} +export function signAccessToken(payload: TokenPayload) { + const options: SignOptions = { expiresIn: process.env.ACCESS_TOKEN_EXPIRY as any }; + + const cleanPayload = { ...payload }; + delete cleanPayload.iat; + delete cleanPayload.exp; + + return jwt.sign(cleanPayload, accessSecret, options); +} +export function signRefreshToken(payload: TokenPayload) { + const options: SignOptions = { expiresIn: process.env.REFRESH_TOKEN_EXPIRY as any }; + + const cleanPayload = { ...payload }; + delete cleanPayload.iat; + delete cleanPayload.exp; + + return jwt.sign(cleanPayload, refreshSecret, options); +} +export function verifyAccessToken(token: string): TokenPayload { + return jwt.verify(token, accessSecret) as TokenPayload; +} +export function verifyRefreshToken(token: string): TokenPayload { + return jwt.verify(token, refreshSecret) as TokenPayload; +} \ No newline at end of file diff --git a/backend/src/controllers/product.controller.ts b/backend/src/controllers/product.controller.ts index 2392fc5..35c21da 100644 --- a/backend/src/controllers/product.controller.ts +++ b/backend/src/controllers/product.controller.ts @@ -3,23 +3,26 @@ import Product from "../models/product.model"; export const createProduct = async (req: Request, res: Response) => { try { - const { name, description, price, category, stock } = req.body; + const { name, description, price, category, stock, imageUrl, condition } = req.body; - if (!name || !description || !price) { - return res.status(400).json({ message: "Missing required fields" }); + if (!name || !description || !price || !category || !stock) { + return res.status(400).json({ error: "Missing required field(s)" }); } - const product = await Product.create({ + const product = new Product({ name, description, price, category, stock, + imageUrl, + condition, }); - return res.status(201).json(product); - } catch (error) { - console.error("Create Product Error:", error); - res.status(500).json({ message: "Server error" }); + + await product.save(); + res.status(201).json(product); + } catch (err: any) { + res.status(500).json({ error: err.message || "Server error" }); } }; diff --git a/backend/src/middleware/authMiddleware.ts b/backend/src/middleware/authMiddleware.ts index 7daa66b..9c8f2b0 100644 --- a/backend/src/middleware/authMiddleware.ts +++ b/backend/src/middleware/authMiddleware.ts @@ -1,28 +1,38 @@ -// src/middlewares/authMiddleware.ts import { Request, Response, NextFunction } from "express"; import jwt from "jsonwebtoken"; -import { User } from "../models/user.model"; +const SECRET_KEY = process.env.JWT_SECRET || "your_secret_key"; -const JWT_SECRET = process.env.JWT_SECRET || "secretkey"; - -export interface AuthRequest extends Request { +export interface AuthenticatedRequest extends Request { user?: any; } -export const protect = async (req: AuthRequest, res: Response, next: NextFunction) => { - let token; - - if (req.headers.authorization && req.headers.authorization.startsWith("Bearer")) { - token = req.headers.authorization.split(" ")[1]; +export const authenticate = (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res.status(401).json({ message: "Unauthorized" }); } - - if (!token) return res.status(401).json({ message: "Not authorized, no token" }); - + const token = authHeader.split(" ")[1]; try { - const decoded = jwt.verify(token, JWT_SECRET) as { id: string }; - req.user = await User.findById(decoded.id).select("-password"); + const decoded = jwt.verify(token, SECRET_KEY); + req.user = decoded; next(); } catch (error) { - res.status(401).json({ message: "Not authorized, token failed" }); + return res.status(401).json({ message: "Invalid token" }); } }; + +// For legacy support in /api/users route expecting 'protect' +export const protect = authenticate; + +// Role-based guard +export const authorizeRole = (role: string) => { + return (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + if (!req.user) { + return res.status(401).json({ error: 'Not authenticated' }); + } + if (req.user.role !== role) { + return res.status(403).json({ error: 'Forbidden' }); + } + next(); + }; +}; \ No newline at end of file diff --git a/backend/src/middleware/uploadMiddleware.ts b/backend/src/middleware/uploadMiddleware.ts new file mode 100644 index 0000000..6993985 --- /dev/null +++ b/backend/src/middleware/uploadMiddleware.ts @@ -0,0 +1,19 @@ +import multer from "multer"; + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 2 * 1024 * 1024 }, + fileFilter: (_req: any, file: { mimetype: string; }, cb: (arg0: Error | null, arg1: boolean | undefined) => void) => { + if ( + file.mimetype === "image/jpeg" || + file.mimetype === "image/png" || + file.mimetype === "image/webp" + ) { + cb(null, true); + } else { + cb(new Error("Invalid file type. Only JPEG, PNG, WEBP allowed.")); + } + }, +}); + +export default upload; \ No newline at end of file diff --git a/backend/src/models/product.model.ts b/backend/src/models/product.model.ts index 978d01c..597bbf4 100644 --- a/backend/src/models/product.model.ts +++ b/backend/src/models/product.model.ts @@ -6,6 +6,7 @@ export interface IProduct extends Document { price: number; category?: string; stock: number; + imageUrl?: string; createdAt: Date; updatedAt: Date; } @@ -17,8 +18,9 @@ const productSchema = new Schema( price: { type: Number, required: true }, category: { type: String }, stock: { type: Number, default: 0 }, + imageUrl: { type: String }, }, { timestamps: true } ); -export default mongoose.model("Product", productSchema); +export default mongoose.model("Product", productSchema); \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index a547bf3..83e1c00 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -6,7 +6,7 @@ yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* - +.env node_modules dist dist-ssr diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8ab2fb0..2425b49 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -43,6 +43,7 @@ "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", + "firebase": "^12.5.0", "framer-motion": "^12.23.24", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", @@ -1113,6 +1114,618 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@firebase/ai": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.5.0.tgz", + "integrity": "sha512-OXv/jZLRjV9jTejWA4KOvW8gM1hNsLvQSCPwKhi2CEfe0Nap3rM6z+Ial0PGqXga0WgzhpypEvJOFvaAUFX3kg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.19", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.19.tgz", + "integrity": "sha512-3wU676fh60gaiVYQEEXsbGS4HbF2XsiBphyvvqDbtC1U4/dO4coshbYktcCHq+HFaGIK07iHOh4pME0hEq1fcg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.25.tgz", + "integrity": "sha512-fdzoaG0BEKbqksRDhmf4JoyZf16Wosrl0Y7tbZtJyVDOOwziE0vrFjmZuTdviL0yhak+Nco6rMsUUbkbD+qb6Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.19", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.5.tgz", + "integrity": "sha512-zyNY77xJOGwcuB+xCxF8z8lSiHvD4ox7BCsqLEHEvgqQoRjxFZ0fkROR6NV5QyXmCqRLodMM8J5d2EStOocWIw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.0.tgz", + "integrity": "sha512-XAvALQayUMBJo58U/rxW02IhsesaxxfWVmVkauZvGEz3vOAjMEQnzFlyblqkc2iAaO82uJ2ZVyZv9XzPfxjJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.0.tgz", + "integrity": "sha512-UfK2Q8RJNjYM/8MFORltZRG9lJj11k0nW84rrffiKvcJxLf1jf6IEjCIkCamykHE73C6BwqhVfhIBs69GXQV0g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.0", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.5.tgz", + "integrity": "sha512-lVG/nRnXaot0rQSZazmTNqy83ti9O3+kdwoaE0d5wahRIWNoDirbIMcGVjDDgdmf4IE6FYreWOMh0L3DV1475w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@firebase/app": "0.14.5", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.1.tgz", + "integrity": "sha512-I0o2ZiZMnMTOQfqT22ur+zcGDVSAfdNZBHo26/Tfi8EllfR1BO7aTVo2rt/ts8o/FWsK8pOALLeVBGhZt8w/vg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.11.1", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", + "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.0.tgz", + "integrity": "sha512-wR9En2A+WESUHexjmRHkqtaVH94WLNKt6rmeqZhSLBybg4Wyf0Umk04SZsS6sBq4102ZsDBFwoqMqJYj2IoDSg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.11.tgz", + "integrity": "sha512-G258eLzAD6im9Bsw+Qm1Z+P4x0PGNQ45yeUuuqe5M9B1rn0RJvvsQCRHXgE52Z+n9+WX1OJd/crcuunvOGc7Vw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.0.tgz", + "integrity": "sha512-gM6MJFae3pTyNLoc9VcJNuaUDej0ctdjn3cVtILo3D5lpp0dmUHHLFN/pUKe7ImyeB1KAvRlEYxvIHNF04Filg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.0.tgz", + "integrity": "sha512-8nYc43RqxScsePVd1qe1xxvWNf0OBnbwHxmXJ7MHSuuTVYFO3eLyLW3PiCKJ9fHnmIz4p4LbieXwz+qtr9PZDg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/database": "1.1.0", + "@firebase/database-types": "1.0.16", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.16.tgz", + "integrity": "sha512-xkQLQfU5De7+SPhEGAXFBnDryUWhhlFXelEg2YeZOQMCdoe7dL64DDAd77SQsR+6uoXIZY5MB4y/inCs4GTfcw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.13.0" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.2.tgz", + "integrity": "sha512-iuA5+nVr/IV/Thm0Luoqf2mERUvK9g791FZpUJV1ZGXO6RL2/i/WFJUj5ZTVXy5pRjpWYO+ZzPcReNrlilmztA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "@firebase/webchannel-wrapper": "1.0.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.2.tgz", + "integrity": "sha512-cy7ov6SpFBx+PHwFdOOjbI7kH00uNKmIFurAn560WiPCZXy9EMnil1SOG7VF4hHZKdenC+AHtL4r3fNpirpm0w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.1.tgz", + "integrity": "sha512-sUeWSb0rw5T+6wuV2o9XNmh9yHxjFI9zVGFnjFi+n7drTEWpl7ZTz1nROgGrSu472r+LAaj+2YaSicD4R8wfbw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.7.0", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.1.tgz", + "integrity": "sha512-AxxUBXKuPrWaVNQ8o1cG1GaCAtXT8a0eaTDfqgS5VsRYLAR0ALcfqDLwo/QyijZj1w8Qf8n3Qrfy/+Im245hOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/functions": "0.13.1", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.19", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.19.tgz", + "integrity": "sha512-nGDmiwKLI1lerhwfwSHvMR9RZuIH5/8E3kgUWnVRqqL7kGVSktjLTWEMva7oh5yxQ3zXfIlIwJwMcaM5bK5j8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.19", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.19.tgz", + "integrity": "sha512-khfzIY3EI5LePePo7vT19/VEIH1E3iYsHknI/6ek9T8QCozAZshWT9CjlwOzZrKvTHMeNcbpo/VSOSIWDSjWdQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.0.tgz", + "integrity": "sha512-cGskaAvkrnh42b3BA3doDWeBmuHFO/Mx5A83rbRDYakPjO9bJtRL3dX7javzc2Rr/JHZf4HlterTW2lUkfeN4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.23.tgz", + "integrity": "sha512-cfuzv47XxqW4HH/OcR5rM+AlQd1xL/VhuaeW/wzMW1LFrsFcTn0GND/hak1vkQc2th8UisBcrkVcQAnOnKwYxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.13.0", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.23.tgz", + "integrity": "sha512-SN857v/kBUvlQ9X/UjAqBoQ2FEaL1ZozpnmL1ByTe57iXkmnVVFm9KqAsTfmf+OEwWI4kJJe9NObtN/w22lUgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/messaging": "0.12.23", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.9.tgz", + "integrity": "sha512-UzybENl1EdM2I1sjYm74xGt/0JzRnU/0VmfMAKo2LSpHJzaj77FCLZXmYQ4oOuE+Pxtt8Wy2BVJEENiZkaZAzQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.22.tgz", + "integrity": "sha512-xLKxaSAl/FVi10wDX/CHIYEUP13jXUjinL+UaNXT9ByIvxII5Ne5150mx6IgM8G6Q3V+sPiw9C8/kygkyHUVxg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/performance": "0.7.9", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.7.0.tgz", + "integrity": "sha512-dX95X6WlW7QlgNd7aaGdjAIZUiQkgWgNS+aKNu4Wv92H1T8Ue/NDUjZHd9xb8fHxLXIHNZeco9/qbZzr500MjQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/installations": "0.6.19", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.20.tgz", + "integrity": "sha512-P/ULS9vU35EL9maG7xp66uljkZgcPMQOxLj3Zx2F289baTKSInE6+YIkgHEi1TwHoddC/AFePXPpshPlEFkbgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-types": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.0.tgz", + "integrity": "sha512-vI3bqLoF14L/GchtgayMiFpZJF+Ao3uR8WCde0XpYNkSokDpAKca2DxvcfeZv7lZUqkUwQPL2wD83d3vQ4vvrg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.0.tgz", + "integrity": "sha512-xWWbb15o6/pWEw8H01UQ1dC5U3rf8QTAzOChYyCpafV6Xki7KVp3Yaw2nSklUwHEziSWE9KoZJS7iYeyqWnYFA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.0.tgz", + "integrity": "sha512-vDzhgGczr1OfcOy285YAPur5pWDEvD67w4thyeCUh6Ys0izN9fNYtA1MJERmNBfqjqu0lg0FM5GLbw0Il21M+g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/storage": "0.14.0", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.13.0.tgz", + "integrity": "sha512-0AZUyYUfpMNcztR5l09izHwXkZpghLgCUaAGjtMwXnCg3bj4ml5VgiwqOMOxJ+Nw4qN/zJAaOQBcJ7KGkWStqQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.5.tgz", + "integrity": "sha512-+uGNN7rkfn41HLO0vekTFhTxk61eKa8mTpRGLO0QSqlQdKvIoGAvLp3ppdVIWbTGYJWM6Kp0iN+PjMIOcnVqTw==", + "license": "Apache-2.0" + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -1151,6 +1764,37 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.15", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", + "integrity": "sha512-nqE7Hc0AzI+euzUwDAy0aY5hCp10r734gMGRdU+qOPX0XSceI2ULrcXB5U2xSc5VkWwalCj4M7GzCAygZl2KoQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@hookform/resolvers": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", @@ -1473,6 +2117,70 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4180,7 +4888,6 @@ "version": "24.8.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.0.tgz", "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==", - "devOptional": true, "license": "MIT", "peer": true, "dependencies": { @@ -4710,7 +5417,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -4985,6 +5691,20 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -5014,7 +5734,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -5027,7 +5746,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -5388,6 +6106,12 @@ "embla-carousel": "8.6.0" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -5438,7 +6162,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5748,6 +6471,18 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -5790,6 +6525,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/firebase": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.5.0.tgz", + "integrity": "sha512-Ak8JcpH7FL6kiv0STwkv5+3CYEROO9iFWSx7OCZVvc4kIIABAIyAGs1mPGaHRxGUIApFZdMCXA7baq17uS6Mow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.5.0", + "@firebase/analytics": "0.10.19", + "@firebase/analytics-compat": "0.2.25", + "@firebase/app": "0.14.5", + "@firebase/app-check": "0.11.0", + "@firebase/app-check-compat": "0.4.0", + "@firebase/app-compat": "0.5.5", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.11.1", + "@firebase/auth-compat": "0.6.1", + "@firebase/data-connect": "0.3.11", + "@firebase/database": "1.1.0", + "@firebase/database-compat": "2.1.0", + "@firebase/firestore": "4.9.2", + "@firebase/firestore-compat": "0.4.2", + "@firebase/functions": "0.13.1", + "@firebase/functions-compat": "0.4.1", + "@firebase/installations": "0.6.19", + "@firebase/installations-compat": "0.2.19", + "@firebase/messaging": "0.12.23", + "@firebase/messaging-compat": "0.2.23", + "@firebase/performance": "0.7.9", + "@firebase/performance-compat": "0.2.22", + "@firebase/remote-config": "0.7.0", + "@firebase/remote-config-compat": "0.2.20", + "@firebase/storage": "0.14.0", + "@firebase/storage-compat": "0.4.0", + "@firebase/util": "1.13.0" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.11.1.tgz", + "integrity": "sha512-Mea0G/BwC1D0voSG+60Ylu3KZchXAFilXQ/hJXWCw3gebAu+RDINZA0dJMNeym7HFxBaBaByX8jSa7ys5+F2VA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.0", + "@firebase/logger": "0.5.0", + "@firebase/util": "1.13.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -5876,6 +6671,15 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-nonce": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", @@ -5967,6 +6771,12 @@ "node": ">=18" } }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6020,6 +6830,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -6094,6 +6910,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6569,6 +7394,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6582,6 +7413,12 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "license": "MIT" }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7075,6 +7912,30 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -7467,6 +8328,15 @@ "node": ">=8" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7566,6 +8436,26 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7702,6 +8592,20 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -8072,7 +8976,6 @@ "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { @@ -8423,6 +9326,12 @@ "node": ">=18" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", @@ -8433,6 +9342,29 @@ "node": ">=20" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -8525,6 +9457,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -8564,6 +9513,15 @@ "dev": true, "license": "MIT" }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -8571,6 +9529,33 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index eca8891..91b6313 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -46,6 +46,7 @@ "cmdk": "^1.1.1", "date-fns": "^3.6.0", "embla-carousel-react": "^8.6.0", + "firebase": "^12.5.0", "framer-motion": "^12.23.24", "input-otp": "^1.4.2", "lucide-react": "^0.462.0", diff --git a/frontend/public/Placeholder.png b/frontend/public/Placeholder.png new file mode 100644 index 0000000..d87ae2e Binary files /dev/null and b/frontend/public/Placeholder.png differ diff --git a/frontend/src/firebaseConfig.ts b/frontend/src/firebaseConfig.ts new file mode 100644 index 0000000..469d8cd --- /dev/null +++ b/frontend/src/firebaseConfig.ts @@ -0,0 +1,12 @@ +import { initializeApp } from "firebase/app"; +import { getStorage } from "firebase/storage"; + +const firebaseConfig = { + +//get yours from firebase console + +}; + +// Initialize Firebase +const app = initializeApp(firebaseConfig); +export const storage = getStorage(app); \ No newline at end of file diff --git a/frontend/src/pages/Browse.tsx b/frontend/src/pages/Browse.tsx index 1dc5574..776e722 100644 --- a/frontend/src/pages/Browse.tsx +++ b/frontend/src/pages/Browse.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../components/ui/card"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; @@ -7,9 +7,6 @@ import { useNavigate } from "react-router-dom"; import { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle } from "../components/ui/drawer"; import { useCart } from "../components/CartContext"; -// 🔹 BACKEND INTEGRATION (commented for now) -// Uncomment when you want to fetch from backend instead of using dummy data -/* const Browse = () => { const navigate = useNavigate(); const [searchQuery, setSearchQuery] = useState(""); @@ -18,41 +15,39 @@ const Browse = () => { const [maxPrice, setMaxPrice] = useState(""); const [sortOrder, setSortOrder] = useState(""); const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const [products, setProducts] = useState([]); + const { addItem } = useCart(); + const demoProducts = [ + { id: "6", name: "Desk Lamp", price: "₹15", category: "Electronics", seller: "Thor", images: ["/lamp1.jpg"] }, + { id: "7", name: "Organic Chemistry Notes", price: "₹25", category: "Notes", seller: "Wanda", images: ["/notes2.jpg"] }, + { id: "8", name: "Bluetooth Headphones", price: "₹150", category: "Electronics", seller: "Natasha", images: ["/headphones.jpg"] }, + { id: "4", name: "Scientific Calculator", price: "₹30", category: "Electronics", seller: "Peter", images: ["/calc1.jpg"] }, + { id: "1", name: "Calculus Textbook", price: "₹45", category: "Books", seller: "Shubham", images: ["/book1.jpg"] }, + { id: "2", name: "Laptop Stand", price: "₹20", category: "Electronics", seller: "Steve Rogers", images: ["/laptop1.jpg"] }, + { id: "3", name: "Study Notes - Physics", price: "₹10", category: "Notes", seller: "Bruce Banner", images: ["/notes1.jpg"] }, + { id: "5", name: "C++ Programming Book", price: "₹499", category: "Books", seller: "Tony Stark", images: ["/book2.jpg"] }, + ]; + const [products, setProducts] = useState(demoProducts); + + // Fetching products from backend API useEffect(() => { const fetchProducts = async () => { try { - const queryParams = new URLSearchParams({ - search: searchQuery, - category: selectedCategory, - minPrice, - maxPrice, - sort: sortOrder, - }).toString(); - - const res = await fetch(`http://localhost:5000/api/products?${queryParams}`); + const res = await fetch("http://localhost:5000/api/products"); + if (!res.ok) throw new Error("Failed to fetch products"); const data = await res.json(); - setProducts(data); + + setProducts((prev) => { + const existingIds = new Set(prev.map((p) => p._id || p.id)); + const newOnes = data.filter((p: any) => !existingIds.has(p._id)); + return [...prev, ...newOnes]; + }); } catch (error) { console.error("Error fetching products:", error); } }; - fetchProducts(); - }, [searchQuery, selectedCategory, minPrice, maxPrice, sortOrder]); -*/ - -const Browse = () => { - const navigate = useNavigate(); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedCategory, setSelectedCategory] = useState(""); - const [minPrice, setMinPrice] = useState(""); - const [maxPrice, setMaxPrice] = useState(""); - const [sortOrder, setSortOrder] = useState(""); - const [isDrawerOpen, setIsDrawerOpen] = useState(false); - - const { addItem } = useCart(); + }, []); const applyFilters = () => { alert(`Applied Filters: @@ -67,43 +62,42 @@ const Browse = () => { setSortOrder(""); }; - const products = [ - { id: "6", name: "Desk Lamp", price: "₹15", category: "Electronics", seller: "Thor", images: ["/lamp1.jpg"] }, - { id: "7", name: "Organic Chemistry Notes", price: "₹25", category: "Notes", seller: "Wanda", images: ["/notes2.jpg"] }, - { id: "8", name: "Bluetooth Headphones", price: "₹150", category: "Electronics", seller: "Natasha", images: ["/headphones.jpg"] }, - { id: "4", name: "Scientific Calculator", price: "₹30", category: "Electronics", seller: "Peter", images: ["/calc1.jpg"] }, - { id: "1", name: "Calculus Textbook", price: "₹45", category: "Books", seller: "Shubham", images: ["/book1.jpg"] }, - { id: "2", name: "Laptop Stand", price: "₹20", category: "Electronics", seller: "Steve Rogers", images: ["/laptop1.jpg"] }, - { id: "3", name: "Study Notes - Physics", price: "₹10", category: "Notes", seller: "Bruce Banner", images: ["/notes1.jpg"] }, - { id: "5", name: "C++ Programming Book", price: "₹499", category: "Books", seller: "Tony Stark", images: ["/book2.jpg"] }, - ]; + // Normalize product price (for robust filtering and sorting) + const getPrice = (product: any): number => { + // Accepts backend number or demo string with ₹ + if (typeof product.price === "number") return product.price; + if (typeof product.price === "string") { + const v = product.price.replace(/[^\d.]/g, ""); + return v ? parseFloat(v) : 0; + } + return 0; + }; const filteredProducts = products .filter(product => { - const price = parseFloat(product.price.replace("₹", "")); - const matchesCategory = selectedCategory ? product.category.toLowerCase() === selectedCategory.toLowerCase() : true; + const price = getPrice(product); + const matchesCategory = selectedCategory ? product.category?.toLowerCase() === selectedCategory.toLowerCase() : true; const matchesSearch = product.name.toLowerCase().includes(searchQuery.toLowerCase()); const matchesPrice = (!minPrice || price >= parseFloat(minPrice)) && (!maxPrice || price <= parseFloat(maxPrice)); return matchesCategory && matchesSearch && matchesPrice; }) .sort((a, b) => { - const priceA = parseFloat(a.price.replace("₹", "")); - const priceB = parseFloat(b.price.replace("₹", "")); + const priceA = getPrice(a); + const priceB = getPrice(b); if (sortOrder === "lowToHigh") return priceA - priceB; if (sortOrder === "highToLow") return priceB - priceA; return 0; }); const handleAddToCart = async (product: any) => { - const price = parseFloat(product.price.replace("₹", "")); + const price = getPrice(product); await addItem({ - productId: String(product.id), + productId: String(product.id || product._id), name: product.name, price, quantity: 1, - image: product.images?.[0], + image: product.imageUrl || (product.images?.[0]), }); - // Optionally notify the user alert(`${product.name} added to cart`); }; @@ -131,10 +125,8 @@ const Browse = () => { - Filter Products -
-
@@ -158,7 +149,6 @@ const Browse = () => { setMaxPrice(e.target.value)} className="border-2 border-blue-200 rounded-lg" />
-
-
@@ -175,17 +164,18 @@ const Browse = () => {
-
- {filteredProducts.length > 0 ? filteredProducts.map((product) => ( - navigate(`/product/${product.id}`)} className="hover:shadow-xl transition-shadow duration-300 border-2 border-blue-100 cursor-pointer"> - {product.name} + {filteredProducts.length > 0 ? filteredProducts.map((product: any) => ( + navigate(`/product/${product.id || product._id}`)} className="hover:shadow-xl transition-shadow duration-300 border-2 border-blue-100 cursor-pointer"> + {product.name} {product.name} Category: {product.category} -

{product.price}

+

+ ₹{getPrice(product)} +

Seller: {product.seller}

@@ -200,7 +190,6 @@ const Browse = () => {
)} -
@@ -208,5 +197,4 @@ const Browse = () => { ); }; - -export default Browse; +export default Browse; \ No newline at end of file diff --git a/frontend/src/pages/ProductDetail.tsx b/frontend/src/pages/ProductDetail.tsx index a5a4208..72ab156 100644 --- a/frontend/src/pages/ProductDetail.tsx +++ b/frontend/src/pages/ProductDetail.tsx @@ -1,35 +1,31 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { Button } from "../components/ui/button"; import { ArrowLeft } from "lucide-react"; import { useCart } from "../components/CartContext"; -/** - * Updated ProductDetail.tsx - * - Converted to TSX - * - "Buy Now" adds to cart via CartContext and navigates to /payment - * - Auction "Place Bid" attempts to call backend auction endpoint (POST /api/auctions/:id/bid) - * and falls back to local alert if backend not reachable. - */ - type AuctionInfo = { isAuction: boolean; highestBid?: number; minIncrement?: number; timeLeft?: string; }; +type SellerInfo = { name: string; Used?: string; email?: string }; type Product = { - id: number; + id: number | string; title: string; + name?: string; price: number; description: string; images: string[]; - seller: { name: string; Used?: string; email?: string }; + seller: SellerInfo; auction: AuctionInfo; + imageUrl?: string; + [key: string]: any; }; -const allProducts: Product[] = [ +const demoProducts: Product[] = [ { id: 1, title: "Calculus Textbook", price: 45, description: "A comprehensive calculus guide ideal for engineering and science students. Covers differential, integral, and multivariable calculus with step-by-step examples and university-level exercises. Perfect for semester preparation and concept building.", images: ["/book1.jpg"], seller: { name: "Shubham", Used: "1.2 year", email: "xyz@gmail.com" }, auction: { isAuction: true, highestBid: 50, minIncrement: 5, timeLeft: "10h 45m" } }, { id: 2, title: "Laptop Stand", price: 20, description: "Ergonomic aluminum laptop stand that improves posture and cooling. Lightweight, foldable, and ideal for long study sessions in hostels or libraries. Adjustable height ensures comfort while typing or attending online classes.", images: ["/laptop1.jpg"], seller: { name: "Steve Rogers", Used: "12 months", email: "xyz@gmail.com" }, auction: { isAuction: false } }, { id: 3, title: "Study Notes - Physics", price: 10, description: "Handwritten physics notes neatly organized chapter-wise, covering Mechanics, Thermodynamics, Electromagnetism, and Modern Physics. Simplified concepts and formulas for quick revision and last-minute preparation.", images: ["/notes1.jpg"], seller: { name: "Bruce Banner", Used: "1 year", email: "xyz@gmail.com" }, auction: { isAuction: false } }, @@ -45,13 +41,58 @@ export default function ProductDetailsPage() { const navigate = useNavigate(); const { addItem } = useCart(); - const product = allProducts.find((p) => p.id === Number(id)); - if (!product) return
Product not found.
; - - const [mainImage, setMainImage] = useState(product.images[0]); - const [bid, setBid] = useState(product.auction.isAuction ? (product.auction.highestBid || 0) + (product.auction.minIncrement || 1) : 0); + const [product, setProduct] = useState(null); + const [mainImage, setMainImage] = useState("/placeholder.png"); + const [bid, setBid] = useState(0); const [isBidding, setIsBidding] = useState(false); + // Load product from backend + useEffect(() => { + let isMounted = true; + async function fetchProduct() { + try { + const res = await fetch(`http://localhost:5000/api/products/${id}`); + if (!res.ok) throw new Error("Backend product not found"); + const prod = await res.json(); + + const prodNorm: Product = { + id: prod._id ?? prod.id, + title: prod.name ?? prod.title ?? "", + price: prod.price, + description: prod.description ?? "", + images: prod.imageUrl ? [prod.imageUrl] : [], + seller: { name: "User", Used: "-", email: prod.sellerEmail || "" }, + auction: { isAuction: false }, + imageUrl: prod.imageUrl, + ...prod + }; + if (isMounted) { + setProduct(prodNorm); + setMainImage(prodNorm.images?.[0] ?? prodNorm.imageUrl ?? "/placeholder.png"); + } + } catch { + // fallback to dummy + const local = demoProducts.find((p) => String(p.id) === String(id)); + if (isMounted && local) { + setProduct(local); + setMainImage(local.images?.[0] ?? "/placeholder.png"); + } else if (isMounted) { + setProduct(null); + } + } + } + fetchProduct(); + return () => { isMounted = false; }; + }, [id]); + + useEffect(() => { + if (product?.auction?.isAuction) { + setBid((product.auction.highestBid || 0) + (product.auction.minIncrement || 1)); + } + }, [product]); + + if (!product) return
Product not found.
; + const placeBid = async () => { if (!product.auction.isAuction) { alert("This item is not in auction."); @@ -65,11 +106,9 @@ export default function ProductDetailsPage() { setIsBidding(true); try { - // Try to hit backend auction endpoint const token = localStorage.getItem("accessToken"); const headers: any = { "Content-Type": "application/json" }; if (token) headers["Authorization"] = `Bearer ${token}`; - const res = await fetch(`/api/auctions/${product.id}/bid`, { method: "POST", headers, @@ -78,36 +117,39 @@ export default function ProductDetailsPage() { if (res.ok) { alert(`Bid of ₹${bid} placed successfully!`); - // optimistically update local product highestBid - product.auction.highestBid = bid; - setBid((bid || 0) + (product.auction.minIncrement || 1)); + setProduct((prod) => prod ? ({ + ...prod, + auction: { ...prod.auction, highestBid: bid }, + }) : null); + setBid(bid + (product.auction.minIncrement || 1)); } else { - // backend responded with error const errText = await res.text(); alert(`Failed to place bid: ${errText || res.statusText}`); } - } catch (err) { - // Fallback offline behavior + } catch { alert(`Could not reach server. Local simulated bid of ₹${bid} placed.`); - product.auction.highestBid = bid; - setBid((bid || 0) + (product.auction.minIncrement || 1)); + setProduct((prod) => prod ? ({ + ...prod, + auction: { ...prod.auction, highestBid: bid }, + }) : null); + setBid(bid + (product.auction.minIncrement || 1)); } finally { setIsBidding(false); } }; const handleBuyNow = async () => { - // add to cart via context then navigate to payment await addItem({ productId: String(product.id), - name: product.title, + name: product.title ?? product.name ?? "", price: product.price, quantity: 1, - image: product.images?.[0], + image: product.images?.[0] || product.imageUrl || "/placeholder.png", }); navigate("/payment"); }; + // Render UI return (
@@ -115,31 +157,27 @@ export default function ProductDetailsPage() {
-
-
- {product.title} + {product.title
- {product.images.map((img, idx) => ( + {(product.images || []).map((img, idx) => ( {`thumb-${idx}`} setMainImage(img)} /> ))}
-
-

{product.title}

+

{product.title ?? product.name}

{product.description}

- - {product.auction.isAuction ? ( + {product.auction?.isAuction ? (

Highest Bid: ₹{product.auction.highestBid}

-

⏰ Time left: {product.auction.timeLeft}

- setBid(Number(e.target.value))} className="border-2 border-blue-200 focus:border-blue-500 rounded-lg w-full p-2 mt-1" min={(product.auction.highestBid || 0) + (product.auction.minIncrement || 1)} /> +

⏰ {product.auction.timeLeft ? `Time left: ${product.auction.timeLeft}` : ""}

+ setBid(Number(e.target.value))} className="border-2 border-blue-200 focus:border-blue-500 rounded-lg w-full p-2 mt-1" min={(product.auction.highestBid || 0) + (product.auction.minIncrement || 1)} />
)} -

Seller Info

-

👤 Name: {product.seller.name}

-

⭐ Used: {product.seller.Used}

-

📧 Email: {product.seller.email}

+

👤 Name: {product.seller?.name ?? "Unknown"}

+

⭐ Used: {product.seller?.Used ?? "-"}

+

📧 Email: {product.seller?.email ?? "-"}

diff --git a/frontend/src/pages/Sell.tsx b/frontend/src/pages/Sell.tsx index e400e84..259f4b2 100644 --- a/frontend/src/pages/Sell.tsx +++ b/frontend/src/pages/Sell.tsx @@ -7,7 +7,10 @@ import { Label } from "../components/ui/label"; import { Textarea } from "../components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select"; import { toast } from "../hooks/use-toast"; -import { Upload, DollarSign } from "lucide-react"; +import { Upload, X as XIcon, DollarSign } from "lucide-react"; +import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; +import { storage } from "../firebaseConfig"; + interface SellFormData { title: string; @@ -17,11 +20,17 @@ interface SellFormData { condition: string; } +const MAX_IMAGE_SIZE = 2 * 1024 * 1024; // 2MB +const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]; + const Sell = () => { const [isLoading, setIsLoading] = useState(false); const [selectedCategory, setSelectedCategory] = useState(""); const [selectedCondition, setSelectedCondition] = useState(""); - + const [imageFile, setImageFile] = useState(null); + const [imagePreview, setImagePreview] = useState(""); + const [firebaseImageUrl, setFirebaseImageUrl] = useState(""); + const { register, handleSubmit, @@ -29,32 +38,85 @@ const Sell = () => { reset, } = useForm(); + function handleImageChange(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + if (!ALLOWED_TYPES.includes(file.type)) { + toast({ title: "Invalid file", description: "Only JPG, PNG, WEBP allowed.", variant: "destructive" }); + return; + } + if (file.size > MAX_IMAGE_SIZE) { + toast({ title: "Image too large", description: "Max size is 2MB.", variant: "destructive" }); + return; + } + setImageFile(file); + setImagePreview(URL.createObjectURL(file)); + } + + function handleRemoveImage() { + setImageFile(null); + setImagePreview(""); + setFirebaseImageUrl(""); + } + + //Upload image to Firebase Storage + async function uploadImageToFirebase(file: File): Promise { + return new Promise((resolve, reject) => { + const storageRef = ref(storage, `products/${Date.now()}-${file.name}`); + const uploadTask = uploadBytesResumable(storageRef, file); + uploadTask.on( + "state_changed", + null, + (error) => reject(error), + async () => { + const downloadURL = await getDownloadURL(uploadTask.snapshot.ref); + resolve(downloadURL); + } + ); + }); + } + const onSubmit = async (data: SellFormData) => { setIsLoading(true); try { - // Mock API call - replace with actual API later - await new Promise(resolve => setTimeout(resolve, 1500)); - - const listingData = { - ...data, - category: selectedCategory, - condition: selectedCondition, - }; - - console.log("Listing created:", listingData); - + let imageUrl = ""; + if (imageFile) { + imageUrl = await uploadImageToFirebase(imageFile); + setFirebaseImageUrl(imageUrl); + } + //Now send product data to backend + const res = await fetch("http://localhost:5000/api/products", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: data.title, + description: data.description, + price: data.price, + category: selectedCategory, + condition: selectedCondition, + stock: 1, + imageUrl + }), + }); + + if (!res.ok) throw new Error("Failed to create product"); + const product = await res.json(); + toast({ title: "Success!", description: "Your item has been listed successfully.", }); - + reset(); setSelectedCategory(""); setSelectedCondition(""); - } catch (error) { + setImageFile(null); + setImagePreview(""); + setFirebaseImageUrl(""); + } catch (error: any) { toast({ title: "Error", - description: "Failed to list your item. Please try again.", + description: error.message || "Failed to list your item. Please try again.", variant: "destructive", }); } finally { @@ -63,15 +125,13 @@ const Sell = () => { }; return ( -
- {/* Background blobs */} +
- - + Sell Your Item @@ -80,7 +140,6 @@ const Sell = () => { List your items and connect with buyers on campus -
{/* Title */} @@ -188,16 +247,37 @@ const Sell = () => {
- {/* Image Upload Placeholder */} + {/* Image Upload */}
-
- -

- Click to upload images (Coming soon) -

+
+ {imagePreview ? ( +
+ preview + +
+ ) : ( + <> + +

+ Click to upload images (Max 2MB, JPG/PNG/WEBP) +

+ + )} + +
@@ -229,4 +309,4 @@ const Sell = () => { ); }; -export default Sell; +export default Sell; \ No newline at end of file