Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions api/custom-box.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const express = require('express');
const router = express.Router();
const CustomBox = require('../models/CustomBox');

// POST - Create a new custom box order
router.post('/custom-box', async (req, res) => {
try {
const { boxSize, items, totalPrice, sessionId, userId } = req.body;

// Validation
if (!boxSize || ![4, 6, 12].includes(boxSize)) {
return res.status(400).json({
success: false,
error: 'Invalid box size. Choose 4, 6, or 12.'
});
}

if (!items || items.length !== boxSize) {
return res.status(400).json({
success: false,
error: `Box must have exactly ${boxSize} items. Currently has ${items?.length || 0}.`
});
}

if (!totalPrice || totalPrice <= 0) {
return res.status(400).json({
success: false,
error: 'Invalid total price.'
});
}

// Create custom box in database
const customBox = new CustomBox({
boxSize,
items,
totalPrice,
sessionId: sessionId || `session_${Date.now()}`,
userId: userId || 'guest',
createdAt: new Date()
});

await customBox.save();

res.status(201).json({
success: true,
message: 'Custom box created successfully!',
data: {
id: customBox._id,
orderId: customBox.orderId,
boxSize: customBox.boxSize,
items: customBox.items,
totalPrice: customBox.totalPrice,
status: customBox.status,
createdAt: customBox.createdAt
}
});

} catch (error) {
console.error('Error creating custom box:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});

// GET - Get all custom boxes for a session/user
router.get('/custom-box/session/:sessionId', async (req, res) => {
try {
const boxes = await CustomBox.find({
sessionId: req.params.sessionId
}).sort({ createdAt: -1 });

res.json({
success: true,
count: boxes.length,
data: boxes
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});

// GET - Get single custom box by ID
router.get('/custom-box/:id', async (req, res) => {
try {
const box = await CustomBox.findById(req.params.id);
if (!box) {
return res.status(404).json({ success: false, error: 'Custom box not found' });
}
res.json({ success: true, data: box });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});

// PUT - Update custom box status
router.put('/custom-box/:id/status', async (req, res) => {
try {
const { status } = req.body;
const validStatuses = ['pending', 'confirmed', 'completed', 'cancelled'];

if (!validStatuses.includes(status)) {
return res.status(400).json({ success: false, error: 'Invalid status' });
}

const box = await CustomBox.findByIdAndUpdate(
req.params.id,
{ status },
{ new: true }
);

if (!box) {
return res.status(404).json({ success: false, error: 'Custom box not found' });
}

res.json({ success: true, data: box });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});

// DELETE - Remove custom box
router.delete('/custom-box/:id', async (req, res) => {
try {
const box = await CustomBox.findByIdAndDelete(req.params.id);
if (!box) {
return res.status(404).json({ success: false, error: 'Custom box not found' });
}
res.json({ success: true, message: 'Custom box removed successfully' });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});

// GET - Get custom box statistics (for admin)
router.get('/admin/custom-box-stats', async (req, res) => {
try {
const totalBoxes = await CustomBox.countDocuments();
const totalRevenue = await CustomBox.aggregate([
{ $match: { status: 'completed' } },
{ $group: { _id: null, total: { $sum: '$totalPrice' } } }
]);

const popularItems = await CustomBox.aggregate([
{ $unwind: '$items' },
{ $group: { _id: '$items.name', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
{ $limit: 5 }
]);

res.json({
success: true,
data: {
totalBoxes,
totalRevenue: totalRevenue[0]?.total || 0,
popularItems
}
});
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});

module.exports = router;
3 changes: 3 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const productRoutes = require('./routes/productRoutes');
const orderRoutes = require('./routes/orderRoutes');
const adminAuth = require('../middlewares/adminAuth');
const { getStats } = require('./controllers/orderController');
const customBoxRoutes = require('./custom-box');


const app = express();
const PORT = process.env.PORT || 3000;
Expand Down Expand Up @@ -57,6 +59,7 @@ app.use('/api/admin', adminRoutes);
app.use('/api', otpRoutes);
app.use('/api/products', productRoutes);
app.use('/api/orders', orderRoutes);
app.use('/api', customBoxRoutes);
app.get('/api/stats', adminAuth, getStats);

// ─── STATIC FALLBACK ────────────────────────────────────────────────────────────
Expand Down
200 changes: 200 additions & 0 deletions assets/css/customizer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* Brownie Box Customizer Styles */
.brownie-customizer {
max-width: 1200px;
margin: 2rem auto;
padding: 2rem;
background: #fdf8f0;
border-radius: 24px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}

.size-selector {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 2rem;
flex-wrap: wrap;
}

.size-btn {
padding: 12px 24px;
font-size: 1.1rem;
font-weight: bold;
border: 2px solid #8B4513;
background: white;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
}

.size-btn.active {
background: #8B4513;
color: white;
transform: scale(1.05);
}

.box-grid {
display: grid;
gap: 1rem;
justify-content: center;
margin: 2rem 0;
padding: 2rem;
background: #f5e6d3;
border-radius: 20px;
min-height: 400px;
}

.box-grid[data-size="4"] {
grid-template-columns: repeat(2, 1fr);
max-width: 400px;
margin: 0 auto;
}

.box-grid[data-size="6"] {
grid-template-columns: repeat(3, 1fr);
max-width: 600px;
margin: 0 auto;
}

.box-grid[data-size="12"] {
grid-template-columns: repeat(4, 1fr);
max-width: 800px;
margin: 0 auto;
}

.slot {
aspect-ratio: 1;
background: white;
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
border: 3px solid #deb887;
position: relative;
overflow: hidden;
}

.slot.empty {
background: #fff9f0;
border: 3px dashed #deb887;
cursor: pointer;
}

.slot.filled {
background: linear-gradient(135deg, #5c3317, #8B4513);
color: white;
animation: pop 0.3s ease-out;
}

@keyframes pop {
0% { transform: scale(0.8); opacity: 0; }
80% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}

.slot-content {
text-align: center;
}

.slot-emoji {
font-size: 2rem;
}

.slot-name {
font-size: 0.8rem;
margin-top: 0.5rem;
}

.slot-price {
font-size: 0.7rem;
margin-top: 0.25rem;
}

.remove-slot {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.5);
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
font-size: 12px;
cursor: pointer;
}

.flavour-selector {
display: flex;
gap: 1rem;
flex-wrap: wrap;
justify-content: center;
margin: 2rem 0;
}

.flavour-btn {
padding: 12px 20px;
background: white;
border: 2px solid #8B4513;
border-radius: 40px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: bold;
}

.flavour-btn:hover {
background: #8B4513;
color: white;
transform: translateY(-3px);
}

.counter {
text-align: center;
font-size: 1.2rem;
margin: 1rem 0;
font-weight: bold;
}

.reset-btn {
padding: 10px 20px;
background: #ff6b6b;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
margin-right: 1rem;
}

.add-to-cart-btn {
padding: 16px 32px;
background: #ccc;
color: white;
border: none;
border-radius: 12px;
font-size: 1.2rem;
font-weight: bold;
cursor: not-allowed;
transition: all 0.3s ease;
}

.add-to-cart-btn.active {
background: #4CAF50;
cursor: pointer;
animation: glow 1.5s infinite;
}

@keyframes glow {
0% { box-shadow: 0 0 5px #4CAF50; }
50% { box-shadow: 0 0 20px #4CAF50; }
100% { box-shadow: 0 0 5px #4CAF50; }
}

.button-group {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 2rem;
}
Loading