From e17f7853f67ca4b8ded90c494dd7f3cff43b9bc3 Mon Sep 17 00:00:00 2001 From: Kaviyasree-N Date: Sat, 6 Jun 2026 11:34:39 +0530 Subject: [PATCH] Brownie box customizer feature is added --- api/custom-box.js | 165 ++ api/index.js | 3 + assets/css/customizer.css | 200 +++ assets/js/customizer.js | 228 +++ models/CustomBox.js | 54 + public/about.html | 2 + public/birthday.html | 2 + public/contact.html | 2 + public/index.html | 2 + public/products.html | 3253 +++++++++++++++++-------------------- public/shared-cart.css | 283 ++++ public/shared-cart.js | 220 +++ 12 files changed, 2616 insertions(+), 1798 deletions(-) create mode 100644 api/custom-box.js create mode 100644 assets/css/customizer.css create mode 100644 assets/js/customizer.js create mode 100644 models/CustomBox.js create mode 100644 public/shared-cart.css create mode 100644 public/shared-cart.js 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) => ` +
+ ${slot ? ` +
+
${slot.emoji}
+
${slot.name}
+
₹${slot.price}
+
+ + ` : ` +
+
+ `} +
+ `).join(''); + + this.updateCounter(); + } + + attachEventListeners() { + document.querySelectorAll('.size-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const newSize = parseInt(e.target.dataset.size); + if (newSize !== this.boxSize) { + if (this.slots.some(slot => slot !== null) && + !confirm('Changing box size will reset your current selection. Continue?')) { + return; + } + this.boxSize = newSize; + this.slots = []; + this.renderSizeSelector(); + this.renderBox(); + } + }); + }); + + document.querySelectorAll('.flavour-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + this.selectedFlavour = JSON.parse(e.currentTarget.dataset.flavour); + document.querySelectorAll('.flavour-btn').forEach(b => { + b.style.background = 'white'; + b.style.color = '#333'; + }); + e.currentTarget.style.background = '#8B4513'; + e.currentTarget.style.color = 'white'; + }); + }); + + const boxGrid = document.getElementById('box-grid'); + if (boxGrid) { + boxGrid.addEventListener('click', (e) => { + const slotDiv = e.target.closest('.slot'); + if (!slotDiv) return; + + const index = parseInt(slotDiv.dataset.index); + + if (e.target.classList.contains('remove-slot')) { + this.removeFromBox(index); + } + else if (slotDiv.classList.contains('empty') && this.selectedFlavour) { + this.addToBox(index); + } + else if (slotDiv.classList.contains('filled')) { + if (confirm('Remove this brownie from your box?')) { + this.removeFromBox(index); + } + } + }); + } + + const resetBtn = document.getElementById('reset-btn'); + if (resetBtn) { + resetBtn.addEventListener('click', () => { + if (confirm('Clear your entire brownie box?')) { + this.slots = []; + this.renderBox(); + } + }); + } + + const addToCartBtn = document.getElementById('add-to-cart-btn'); + if (addToCartBtn) { + addToCartBtn.addEventListener('click', () => { + if (this.isBoxFull()) { + this.addToCart(); + } + }); + } + } + + addToBox(index) { + if (this.slots[index] !== null) { + alert('This slot is already filled! Click on a filled slot to remove it.'); + return; + } + + if (this.getFilledCount() >= this.boxSize) { + alert('Your box is full! Remove some brownies first.'); + return; + } + + this.slots[index] = { ...this.selectedFlavour }; + this.renderBox(); + } + + removeFromBox(index) { + if (this.slots[index]) { + this.slots[index] = null; + this.renderBox(); + } + } + + getFilledCount() { + return this.slots.filter(slot => slot !== null).length; + } + + updateCounter() { + const filled = this.getFilledCount(); + const total = this.boxSize; + const counter = document.getElementById('counter'); + const addToCartBtn = document.getElementById('add-to-cart-btn'); + + if (counter) { + counter.innerHTML = `📦 ${filled} of ${total} slots filled`; + counter.style.color = filled === total ? '#4CAF50' : '#333'; + } + + if (addToCartBtn) { + if (filled === total) { + addToCartBtn.classList.add('active'); + addToCartBtn.disabled = false; + addToCartBtn.textContent = '✨ Add Custom Box to Cart ✨'; + } else { + addToCartBtn.classList.remove('active'); + addToCartBtn.disabled = true; + addToCartBtn.textContent = `➕ Add ${total - filled} more brownie${total - filled !== 1 ? 's' : ''}`; + } + } + } + + isBoxFull() { + return this.getFilledCount() === this.boxSize; + } + + addToCart() { + const customBox = { + id: `custom-box-${Date.now()}`, + name: `${this.boxSize}-Pack Custom Brownie Box`, + items: this.slots.filter(s => s !== null), + totalPrice: this.slots.reduce((sum, slot) => sum + (slot?.price || 0), 0), + quantity: 1, + type: 'custom-box' + }; + + let cart = JSON.parse(localStorage.getItem('cart') || '[]'); + cart.push(customBox); + localStorage.setItem('cart', JSON.stringify(cart)); + + alert(`🎉 Custom ${this.boxSize}-Pack box added to cart! Total: ₹${customBox.totalPrice}`); + + if (confirm('Box added! Create another custom box?')) { + this.slots = []; + this.renderBox(); + } + } +} + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + window.brownieCustomizer = new BrownieBoxCustomizer(); +}); \ No newline at end of file diff --git a/models/CustomBox.js b/models/CustomBox.js new file mode 100644 index 00000000..04a1cea8 --- /dev/null +++ b/models/CustomBox.js @@ -0,0 +1,54 @@ +const mongoose = require('mongoose'); + +const customBoxSchema = new mongoose.Schema({ + boxSize: { + type: Number, + required: true, + enum: [4, 6, 12] + }, + items: [{ + name: { type: String, required: true }, + price: { type: Number, required: true }, + emoji: { type: String }, + id: { type: String } + }], + totalPrice: { + type: Number, + required: true + }, + userId: { + type: String, + default: 'guest' + }, + sessionId: { + type: String, + required: true + }, + status: { + type: String, + enum: ['pending', 'confirmed', 'completed', 'cancelled'], + default: 'pending' + }, + orderId: { + type: String, + unique: true + }, + createdAt: { + type: Date, + default: Date.now + } +}); + +// Generate unique order ID before saving +customBoxSchema.pre('save', async function(next) { + if (!this.orderId) { + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const count = await this.constructor.countDocuments(); + this.orderId = `CUSTOM-${year}${month}-${String(count + 1).padStart(4, '0')}`; + } + next(); +}); + +module.exports = mongoose.model('CustomBox', customBoxSchema); \ No newline at end of file diff --git a/public/about.html b/public/about.html index a25ffe53..ffd71cd6 100644 --- a/public/about.html +++ b/public/about.html @@ -6,6 +6,7 @@ About — Brownie Bliss + @@ -2271,5 +2272,6 @@

CONTACT

bbCursor.style.opacity = "1"; }); + diff --git a/public/birthday.html b/public/birthday.html index 11859e88..a41fb6be 100644 --- a/public/birthday.html +++ b/public/birthday.html @@ -10,6 +10,7 @@ href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Cormorant+Garamond:wght@300;400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet"> + + diff --git a/public/contact.html b/public/contact.html index 7ab68f9a..4d97eb9c 100644 --- a/public/contact.html +++ b/public/contact.html @@ -12,6 +12,7 @@ /> + + diff --git a/public/index.html b/public/index.html index e6ed7686..77a1527e 100644 --- a/public/index.html +++ b/public/index.html @@ -10,6 +10,7 @@ href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&family=Cormorant+Garamond:wght@300;400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet"> + + \ No newline at end of file diff --git a/public/products.html b/public/products.html index af2f77cf..2b73d6fc 100644 --- a/public/products.html +++ b/public/products.html @@ -1,1885 +1,1542 @@ - + - - - Brownie Bliss — Our Full Range - - - - - - - + + + Brownie Bliss — Build Your Own Box + + + + + + + + + - - -
- ✦   DELICIOUS HOMEMADE BROWNIES, CAKES & COOKIES  ·  - HAPPINESS IN EVERY BITE   ✦ -
-
- -
-
- Home - Our Products - Birthday Cakes - Contact - Track Order -
- - + .customizer-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--gold), var(--brown-medium), var(--gold)); + } -
- -
- + .section-badge { + display: inline-block; + background: linear-gradient(135deg, var(--gold), var(--gold-dark)); + color: var(--brown-dark); + padding: 6px 16px; + border-radius: var(--radius-full); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 1px; + margin-bottom: 20px; + text-transform: uppercase; + } - -
+ .section-title { + font-family: 'Playfair Display', serif; + font-size: 2.5rem; + color: var(--brown-dark); + margin-bottom: 12px; + transition: color 0.3s ease; + } - -
-
+ .section-subtitle { + color: var(--gray); + margin-bottom: 40px; + transition: color 0.3s ease; + } - -
+ /* Size Selector */ + .size-selector { + display: flex; + justify-content: center; + gap: 20px; + margin-bottom: 48px; + flex-wrap: wrap; + } - -
+ .size-card { + background: var(--gray-light); + border: 2px solid transparent; + border-radius: var(--radius-lg); + padding: 20px 32px; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + min-width: 140px; + color: var(--brown-dark); + } -