Skip to content

Commit 580e4e4

Browse files
committed
feat: Implement notification UI for auction
1 parent 29d3ed1 commit 580e4e4

File tree

10 files changed

+498
-60
lines changed

10 files changed

+498
-60
lines changed

backend/src/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import healthRoutes from './routes/healthRoutes';
66
import userRoutes from "./routes/user.routes";
77
import productRoutes from "./routes/product.routes";
88
import cartRoutes from "./routes/cartRoutes";
9+
import auctionRoutes from "./routes/auctionRoutes";
910
import mongoose from "mongoose";
1011
import { requestLogger } from "./middleware/loggerMiddleware";
1112
import { errorHandler } from "./middleware/errorMiddleware";
@@ -37,7 +38,7 @@ app.use('/api/health', healthRoutes);
3738
app.use("/api/products", productRoutes);
3839
app.use("/api/users", userRoutes);
3940
app.use("/api/cart", cartRoutes);
40-
app.use("/api/users", userRoutes);
41+
app.use("/api/auctions", auctionRoutes);
4142

4243
// MongoDB connect (optional)
4344
const mongoUri = process.env.MONGO_URI;
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { Request, Response } from "express";
2+
import Auction from "../models/auction.model";
3+
import Bid from "../models/bid.model";
4+
import Product from "../models/product.model";
5+
import mongoose from "mongoose";
6+
7+
//new auction
8+
export const createAuction = async (req: Request, res: Response): Promise<void> => {
9+
try {
10+
const userId = req.user?.id;
11+
if (!userId) {
12+
res.status(401).json({ message: "Unauthorized" });
13+
return;
14+
}
15+
const { productId, startPrice, minIncrement, durationHours } = req.body;
16+
if (!productId || !startPrice) {
17+
res.status(400).json({ message: "Product ID and start price are required" });
18+
return;
19+
}
20+
//if product exists
21+
const product = await Product.findById(productId);
22+
if (!product) {
23+
res.status(404).json({ message: "Product not found" });
24+
return;
25+
}
26+
const existingAuction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(productId), status: "active" });
27+
if (existingAuction) {
28+
res.status(400).json({ message: "An active auction already exists for this product" });
29+
return;
30+
}
31+
const expiresAt = new Date();
32+
expiresAt.setHours(expiresAt.getHours() + (durationHours || 48));
33+
34+
const auction = new Auction({
35+
productId: new mongoose.Types.ObjectId(productId),
36+
itemName: product.name,
37+
itemDescription: product.description,
38+
startPrice,
39+
currentHighestBid: startPrice,
40+
minIncrement: minIncrement || 100,
41+
seller: new mongoose.Types.ObjectId(userId),
42+
expiresAt,
43+
status: "active",
44+
});
45+
await auction.save();
46+
res.status(201).json(auction);
47+
} catch (error: any) {
48+
console.error("Create auction error:", error);
49+
res.status(500).json({ message: error.message || "Server error" });
50+
}
51+
};
52+
53+
export const placeBid = async (req: Request, res: Response): Promise<void> => {
54+
try {
55+
const userId = req.user?.id;
56+
if (!userId) {
57+
res.status(401).json({ message: "Unauthorized" });
58+
return;
59+
}
60+
61+
const { id } = req.params;
62+
const { amount } = req.body;
63+
64+
if (!amount || amount <= 0) {
65+
res.status(400).json({ message: "Valid bid amount is required" });
66+
return;
67+
}
68+
let auction = await Auction.findById(id);
69+
if (!auction) {
70+
auction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(id), status: "active" });
71+
}
72+
73+
if (!auction) {
74+
res.status(404).json({ message: "Auction not found or not active" });
75+
return;
76+
}
77+
78+
if (auction.status !== "active") {
79+
res.status(400).json({ message: "Auction is not active" });
80+
return;
81+
}
82+
83+
// Check if expired
84+
if (new Date() > auction.expiresAt) {
85+
auction.status = "expired";
86+
await auction.save();
87+
res.status(400).json({ message: "Auction has expired" });
88+
return;
89+
}
90+
if (auction.seller.toString() === userId) {
91+
res.status(400).json({ message: "You cannot bid on your own auction" });
92+
return;
93+
}
94+
const minBid = auction.currentHighestBid + (auction.minIncrement || 100);
95+
if (amount < minBid) {
96+
res.status(400).json({ message: `Bid must be at least ₹${minBid}` });
97+
return;
98+
}
99+
const bid = new Bid({
100+
auctionId: auction._id,
101+
bidder: new mongoose.Types.ObjectId(userId),
102+
amount,
103+
});
104+
105+
await bid.save();
106+
auction.currentHighestBid = amount;
107+
auction.highestBidder = new mongoose.Types.ObjectId(userId);
108+
await auction.save();
109+
110+
res.status(201).json({
111+
message: "Bid placed successfully",
112+
bid,
113+
auction: {
114+
currentHighestBid: auction.currentHighestBid,
115+
highestBidder: auction.highestBidder,
116+
},
117+
});
118+
} catch (error: any) {
119+
console.error("Place bid error:", error);
120+
res.status(500).json({ message: error.message || "Server error" });
121+
}
122+
};
123+
export const acceptHighestBid = async (req: Request, res: Response): Promise<void> => {
124+
try {
125+
const userId = req.user?.id;
126+
if (!userId) {
127+
res.status(401).json({ message: "Unauthorized" });
128+
return;
129+
}
130+
131+
const { id } = req.params;
132+
let auction = await Auction.findById(id);
133+
if (!auction) {
134+
auction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(id), status: "active" });
135+
}
136+
137+
if (!auction) {
138+
res.status(404).json({ message: "Auction not found" });
139+
return;
140+
}
141+
142+
if (auction.seller.toString() !== userId) {
143+
res.status(403).json({ message: "Only the seller can accept bids" });
144+
return;
145+
}
146+
147+
if (!auction.highestBidder) {
148+
res.status(400).json({ message: "No bids to accept" });
149+
return;
150+
}
151+
// Mark auction as sold
152+
auction.status = "sold";
153+
auction.soldTo = auction.highestBidder;
154+
auction.soldPrice = auction.currentHighestBid;
155+
await auction.save();
156+
157+
res.status(200).json({
158+
message: "Bid accepted successfully",
159+
auction,
160+
});
161+
} catch (error: any) {
162+
console.error("Accept bid error:", error);
163+
res.status(500).json({ message: error.message || "Server error" });
164+
}
165+
};
166+
167+
// Get auction by product ID
168+
export const getAuctionByProductId = async (req: Request, res: Response): Promise<void> => {
169+
try {
170+
const { productId } = req.params;
171+
172+
const auction = await Auction.findOne({
173+
productId: new mongoose.Types.ObjectId(productId),
174+
status: "active",
175+
}).populate("seller", "name email").populate("highestBidder", "name email");
176+
177+
if (!auction) {
178+
res.status(404).json({ message: "No active auction found for this product" });
179+
return;
180+
}
181+
182+
//time left
183+
const now = new Date();
184+
const expiresAt = new Date(auction.expiresAt);
185+
const timeLeftMs = expiresAt.getTime() - now.getTime();
186+
const timeLeftHours = Math.max(0, Math.floor(timeLeftMs / (1000 * 60 * 60)));
187+
const timeLeftMinutes = Math.max(0, Math.floor((timeLeftMs % (1000 * 60 * 60)) / (1000 * 60)));
188+
189+
res.status(200).json({
190+
...auction.toObject(),
191+
timeLeft: timeLeftMs > 0 ? `${timeLeftHours}h ${timeLeftMinutes}m` : "Expired",
192+
isExpired: timeLeftMs <= 0,
193+
});
194+
} catch (error: any) {
195+
console.error("Get auction error:", error);
196+
res.status(500).json({ message: error.message || "Server error" });
197+
}
198+
};
199+
200+
// Get all bids for an auction
201+
export const getAuctionBids = async (req: Request, res: Response): Promise<void> => {
202+
try {
203+
const { id } = req.params;
204+
205+
let auction = await Auction.findById(id);
206+
if (!auction) {
207+
auction = await Auction.findOne({ productId: new mongoose.Types.ObjectId(id) });
208+
}
209+
210+
if (!auction) {
211+
res.status(404).json({ message: "Auction not found" });
212+
return;
213+
}
214+
215+
const bids = await Bid.find({ auctionId: auction._id })
216+
.populate("bidder", "name email")
217+
.sort({ createdAt: -1 });
218+
219+
res.status(200).json(bids);
220+
} catch (error: any) {
221+
console.error("Get bids error:", error);
222+
res.status(500).json({ message: error.message || "Server error" });
223+
}
224+
};
225+

backend/src/models/auction.model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import mongoose from "mongoose";
22

33
const auctionSchema = new mongoose.Schema({
4+
productId: {
5+
type: mongoose.Schema.Types.ObjectId,
6+
ref: "Product",
7+
required: true,
8+
},
49
itemName: {
510
type: String,
611
required: true,
@@ -15,6 +20,11 @@ const auctionSchema = new mongoose.Schema({
1520
required: true,
1621
min: 0,
1722
},
23+
minIncrement: {
24+
type: Number,
25+
default: 100,
26+
min: 1,
27+
},
1828
currentHighestBid: {
1929
type: Number,
2030
default: function () { return this.startPrice; },

backend/src/routes/auctionRoutes.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ import {
33
createAuction,
44
placeBid,
55
acceptHighestBid,
6+
getAuctionByProductId,
7+
getAuctionBids,
68
} from "../controllers/auction.controller";
7-
import { authenticate, authorizeRole } from "../middleware/authMiddleware";
9+
import { authenticate } from "../middleware/authMiddleware";
810

911
const router = express.Router();
1012

11-
// Create a new auction (only sellers)
12-
router.post("/", authenticate, authorizeRole("seller"), createAuction);
13+
router.get("/product/:productId", getAuctionByProductId);
14+
router.get("/:id/bids", getAuctionBids);
15+
router.post("/", authenticate, createAuction);
1316

14-
// Place a bid (only buyers)
15-
router.post("/:id/bid", authenticate, authorizeRole("buyer"), placeBid);
17+
// Place a bid (authenticated)
18+
router.post("/:id/bid", authenticate, placeBid);
1619

17-
// Seller accepts the highest bid (only sellers)
18-
router.post("/:id/accept", authenticate, authorizeRole("seller"), acceptHighestBid);
20+
// Seller accepts the highest bid (authenticated)
21+
router.post("/:id/accept", authenticate, acceptHighestBid);
1922

2023
export default router;

frontend/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import { CartProvider } from "./components/CartContext";
1313
import Cart from "./pages/Cart";
1414
import Payment from "./pages/Payment";
1515
import Dashboard from "./pages/Dashboard";
16+
import { NotificationProvider } from "./components/NotificationContext";
1617

1718
const queryClient = new QueryClient();
1819

1920
const App = () => (
2021
<QueryClientProvider client={queryClient}>
2122
<TooltipProvider>
2223
<Toaster />
24+
<NotificationProvider>
2325
<CartProvider>
2426
<BrowserRouter>
2527
<Navbar />
@@ -38,6 +40,7 @@ const App = () => (
3840
</div>
3941
</BrowserRouter>
4042
</CartProvider>
43+
</NotificationProvider>
4144
</TooltipProvider>
4245
</QueryClientProvider>
4346
);

0 commit comments

Comments
 (0)