diff --git a/api/custom-box.js b/api/custom-box.js new file mode 100644 index 00000000..31833062 --- /dev/null +++ b/api/custom-box.js @@ -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; \ No newline at end of file diff --git a/api/index.js b/api/index.js index d233e239..c17a8495 100644 --- a/api/index.js +++ b/api/index.js @@ -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; @@ -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 ──────────────────────────────────────────────────────────── diff --git a/assets/css/customizer.css b/assets/css/customizer.css new file mode 100644 index 00000000..2b9b00d2 --- /dev/null +++ b/assets/css/customizer.css @@ -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; +} \ No newline at end of file diff --git a/assets/js/customizer.js b/assets/js/customizer.js new file mode 100644 index 00000000..d36887e3 --- /dev/null +++ b/assets/js/customizer.js @@ -0,0 +1,228 @@ +// Brownie Box Customizer - Core Logic +class BrownieBoxCustomizer { + constructor() { + this.boxSize = 4; + this.slots = []; + this.flavours = [ + { id: 'walnut', name: 'Walnut Bliss', emoji: '🌰', price: 399 }, + { id: 'redvelvet', name: 'Red Velvet', emoji: '❤️', price: 449 }, + { id: 'chocolate', name: 'Double Chocolate', emoji: '🍫', price: 399 }, + { id: 'caramel', name: 'Salted Caramel', emoji: '🍯', price: 429 }, + { id: 'oreo', name: 'Cookies & Cream', emoji: '🍪', price: 419 } + ]; + this.selectedFlavour = null; + this.init(); + } + + init() { + this.renderSizeSelector(); + this.renderFlavourSelector(); + this.renderBox(); + this.attachEventListeners(); + } + + renderSizeSelector() { + const container = document.getElementById('size-selector'); + if (!container) return; + + container.innerHTML = ` + + + + `; + } + + renderFlavourSelector() { + const container = document.getElementById('flavour-selector'); + if (!container) return; + + container.innerHTML = this.flavours.map(flavour => ` + + `).join(''); + } + + renderBox() { + const container = document.getElementById('box-grid'); + if (!container) return; + + container.setAttribute('data-size', this.boxSize); + + while (this.slots.length < this.boxSize) { + this.slots.push(null); + } + while (this.slots.length > this.boxSize) { + this.slots.pop(); + } + + container.innerHTML = this.slots.map((slot, index) => ` +