diff --git a/admin.html b/admin.html new file mode 100644 index 0000000..ecebe80 --- /dev/null +++ b/admin.html @@ -0,0 +1,39 @@ + + + + + + Admin Box Editor + + + +
+
+
+
+

Administrator box editor

+

Change the price, picture, and description for all 8 boxes.

+
+ Back to home +
+ +
+ + + +
+ +

Enter the admin code to edit box details.

+ + + + +
+
+ + + + \ No newline at end of file diff --git a/backend/controllers/productController.js b/backend/controllers/productController.js index c7b6aa7..290e946 100644 --- a/backend/controllers/productController.js +++ b/backend/controllers/productController.js @@ -31,6 +31,8 @@ exports.getAllProducts = async (req, res) => { products.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } else if (sortBy === 'rating') { products.sort((a, b) => b.rating - a.rating); + } else if (products.length && products.every(product => String(product.sku || '').startsWith('BOX-'))) { + products.sort((a, b) => String(a.sku).localeCompare(String(b.sku))); } else { products.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); } diff --git a/backend/data/products.json b/backend/data/products.json index b101357..6b69bfb 100644 --- a/backend/data/products.json +++ b/backend/data/products.json @@ -1,117 +1,146 @@ [ { - "_id": "1775465493816", - "name": "Classic Burger", - "description": "Juicy beef patty with lettuce, tomato, onion, and our special sauce", - "price": 12.99, - "salePrice": 9.99, - "category": "Food", - "image": "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=300", - "images": [ - "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=300" - ], - "stock": 50, - "sku": "BURGER-001", + "_id": "box-1", + "name": "box1", + "description": "Description for box1", + "price": 9.2, + "salePrice": null, + "category": "Box", + "image": "", + "images": [], + "stock": 100, + "sku": "BOX-001", "rating": 0, "reviewCount": 0, - "tags": [ - "popular", - "beef" - ], + "tags": [], "isActive": true, "createdAt": "2026-04-06T08:51:33.816Z", - "updatedAt": "2026-04-06T08:51:33.816Z" + "updatedAt": "2026-04-15T00:00:00.000Z" }, { - "_id": "1775465493818", - "name": "Margherita Pizza", - "description": "Fresh mozzarella, tomato sauce, and basil on our signature crust", - "price": 15.99, + "_id": "box-2", + "name": "box2", + "description": "Description for box2", + "price": 10, "salePrice": null, - "category": "Food", - "image": "https://images.unsplash.com/photo-1513104890138-7c749659a591?w=300", - "images": [ - "https://images.unsplash.com/photo-1513104890138-7c749659a591?w=300" - ], - "stock": 30, - "sku": "PIZZA-001", - "rating": 0, - "reviewCount": 0, - "tags": [ - "vegetarian", - "classic" - ], - "isActive": true, - "createdAt": "2026-04-06T08:51:33.818Z", - "updatedAt": "2026-04-06T08:51:33.818Z" - }, - { - "_id": "1775465493818", - "name": "Caesar Salad", - "description": "Crisp romaine lettuce with parmesan cheese, croutons, and Caesar dressing", - "price": 8.99, - "salePrice": null, - "category": "Salads", - "image": "https://images.unsplash.com/photo-1550304943-4f24f54ddde9?w=300", - "images": [ - "https://images.unsplash.com/photo-1550304943-4f24f54ddde9?w=300" - ], - "stock": 40, - "sku": "SALAD-001", - "rating": 0, - "reviewCount": 0, - "tags": [ - "healthy", - "vegetarian" - ], - "isActive": true, - "createdAt": "2026-04-06T08:51:33.818Z", - "updatedAt": "2026-04-06T08:51:33.818Z" - }, - { - "_id": "1775465493819", - "name": "French Fries", - "description": "Golden crispy fries served with ketchup", - "price": 4.99, - "salePrice": null, - "category": "Sides", - "image": "https://images.unsplash.com/photo-1573080496219-bb080dd4f877?w=300", - "images": [ - "https://images.unsplash.com/photo-1573080496219-bb080dd4f877?w=300" - ], + "category": "Box", + "image": "", + "images": [], "stock": 100, - "sku": "FRIES-001", + "sku": "BOX-002", "rating": 0, "reviewCount": 0, - "tags": [ - "side", - "crispy" - ], + "tags": [], + "isActive": true, + "createdAt": "2026-04-06T08:51:33.817Z", + "updatedAt": "2026-04-15T00:00:00.000Z" + }, + { + "_id": "box-3", + "name": "box3", + "description": "Description for box3", + "price": 8.5, + "salePrice": null, + "category": "Box", + "image": "", + "images": [], + "stock": 100, + "sku": "BOX-003", + "rating": 0, + "reviewCount": 0, + "tags": [], + "isActive": true, + "createdAt": "2026-04-06T08:51:33.818Z", + "updatedAt": "2026-04-15T00:00:00.000Z" + }, + { + "_id": "box-4", + "name": "box4", + "description": "Description for box4", + "price": 11, + "salePrice": null, + "category": "Box", + "image": "", + "images": [], + "stock": 100, + "sku": "BOX-004", + "rating": 0, + "reviewCount": 0, + "tags": [], "isActive": true, "createdAt": "2026-04-06T08:51:33.819Z", - "updatedAt": "2026-04-06T08:51:33.819Z" + "updatedAt": "2026-04-15T00:00:00.000Z" }, { - "_id": "1775465493820", - "name": "Chocolate Milkshake", - "description": "Rich and creamy chocolate milkshake topped with whipped cream", + "_id": "box-5", + "name": "box5", + "description": "Description for box5", "price": 6.99, "salePrice": null, - "category": "Drinks", - "image": "https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=300", - "images": [ - "https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=300" - ], - "stock": 25, - "sku": "SHAKE-001", + "category": "Box", + "image": "", + "images": [], + "stock": 100, + "sku": "BOX-005", "rating": 0, "reviewCount": 0, - "tags": [ - "sweet", - "cold" - ], + "tags": [], "isActive": true, "createdAt": "2026-04-06T08:51:33.820Z", - "updatedAt": "2026-04-06T08:51:33.820Z" + "updatedAt": "2026-04-15T00:00:00.000Z" + }, + { + "_id": "box-6", + "name": "box6", + "description": "Description for box6", + "price": 4.99, + "salePrice": null, + "category": "Box", + "image": "", + "images": [], + "stock": 100, + "sku": "BOX-006", + "rating": 0, + "reviewCount": 0, + "tags": [], + "isActive": true, + "createdAt": "2026-04-06T08:51:33.821Z", + "updatedAt": "2026-04-15T00:00:00.000Z" + }, + { + "_id": "box-7", + "name": "box7", + "description": "Description for box7", + "price": 5.99, + "salePrice": null, + "category": "Box", + "image": "", + "images": [], + "stock": 100, + "sku": "BOX-007", + "rating": 0, + "reviewCount": 0, + "tags": [], + "isActive": true, + "createdAt": "2026-04-06T08:51:33.822Z", + "updatedAt": "2026-04-15T00:00:00.000Z" + }, + { + "_id": "box-8", + "name": "box8", + "description": "Description for box8", + "price": 7.99, + "salePrice": null, + "category": "Box", + "image": "", + "images": [], + "stock": 100, + "sku": "BOX-008", + "rating": 0, + "reviewCount": 0, + "tags": [], + "isActive": true, + "createdAt": "2026-04-06T08:51:33.823Z", + "updatedAt": "2026-04-15T00:00:00.000Z" } ] \ No newline at end of file diff --git a/backend/data/users.json b/backend/data/users.json index a9a535c..77651a2 100644 --- a/backend/data/users.json +++ b/backend/data/users.json @@ -1,6 +1,7 @@ [ { "_id": "1775634815531", + "id": "1775634815531", "firstName": "Tornike", "lastName": "Gerantia", "email": "tornikegerantia@gmail.com", @@ -10,8 +11,8 @@ "role": "user", "isActive": true, "profileImage": null, - "lastLogin": null, + "lastLogin": "2026-04-15T00:22:28.632Z", "createdAt": "2026-04-08T07:53:35.531Z", - "updatedAt": "2026-04-08T07:53:35.613Z" + "updatedAt": "2026-04-15T00:22:28.632Z" } ] \ No newline at end of file diff --git a/backend/models/productModel.js b/backend/models/productModel.js index 6e1b6ee..109e4db 100644 --- a/backend/models/productModel.js +++ b/backend/models/productModel.js @@ -21,12 +21,12 @@ class Product { this._id = data._id || Date.now().toString(); this.name = data.name; this.description = data.description; - this.price = data.price; + this.price = Number(data.price || 0); this.salePrice = data.salePrice || null; this.category = data.category; this.image = data.image; this.images = data.images || [data.image]; - this.stock = data.stock; + this.stock = Number(data.stock || 0); this.sku = data.sku; this.rating = data.rating || 0; this.reviewCount = data.reviewCount || 0; @@ -158,6 +158,17 @@ Product.findByIdAndUpdate = async (id, updateData) => { } }; +Product.replaceAll = async (productsData) => { + try { + const products = productsData.map(data => new Product(data)); + fs.writeFileSync(PRODUCTS_FILE, JSON.stringify(products, null, 2)); + return products; + } catch (error) { + console.error('Error replacing products:', error); + throw error; + } +}; + Product.findByIdAndDelete = async (id) => { try { const products = await Product.find(); diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js new file mode 100644 index 0000000..b3a8453 --- /dev/null +++ b/backend/routes/adminRoutes.js @@ -0,0 +1,71 @@ +const express = require('express'); +const Product = require('../models/productModel'); + +const router = express.Router(); + +const ADMIN_CODE = process.env.ADMIN_CODE || '1234'; + +function requireAdminCode(req, res, next) { + const code = req.headers['x-admin-code'] || req.body.adminCode || req.query.adminCode; + if (code !== ADMIN_CODE) { + return res.status(401).json({ success: false, message: 'Invalid admin code' }); + } + next(); +} + +function normalizeBox(product, index) { + const boxNumber = index + 1; + return { + _id: product._id || `box-${boxNumber}`, + name: `box${boxNumber}`, + description: product.description || '', + price: Number(product.price || 0), + salePrice: null, + category: 'Box', + image: product.image || '', + images: product.image ? [product.image] : [], + stock: Number(product.stock || 100), + sku: `BOX-${String(boxNumber).padStart(3, '0')}`, + rating: Number(product.rating || 0), + reviewCount: Number(product.reviewCount || 0), + tags: Array.isArray(product.tags) ? product.tags : [], + isActive: true, + createdAt: product.createdAt || new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; +} + +router.get('/boxes', async (req, res) => { + try { + const products = await Product.find({ isActive: true }); + const boxes = Array.from({ length: 8 }, (_, index) => { + const sku = `BOX-${String(index + 1).padStart(3, '0')}`; + const product = products.find(item => item.sku === sku) || products[index] || {}; + return normalizeBox(product, index); + }); + + res.json({ success: true, boxes }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.put('/boxes', requireAdminCode, async (req, res) => { + try { + const boxes = Array.isArray(req.body.boxes) ? req.body.boxes : []; + if (boxes.length !== 8) { + return res.status(400).json({ success: false, message: 'Exactly 8 boxes are required' }); + } + + const existingProducts = await Product.find(); + const nonBoxProducts = existingProducts.filter(product => !String(product.sku || '').startsWith('BOX-')); + const normalizedBoxes = boxes.map((box, index) => normalizeBox(box, index)); + await Product.replaceAll([...normalizedBoxes, ...nonBoxProducts]); + + res.json({ success: true, message: 'Boxes saved successfully', boxes: normalizedBoxes }); + } catch (error) { + res.status(500).json({ success: false, message: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index d607982..d1b4daf 100644 --- a/backend/server.js +++ b/backend/server.js @@ -75,6 +75,7 @@ app.get('/api/health', (req, res) => { app.use('/api/auth', require('./routes/authRoutes')); app.use('/api/users', require('./routes/userRoutes')); app.use('/api/products', require('./routes/productRoutes')); +app.use('/api/admin', require('./routes/adminRoutes')); app.use('/api/cart', require('./routes/cartRoutes')); app.use('/api/orders', require('./routes/orderRoutes')); diff --git a/company.html b/company.html index 40296a3..914a756 100644 --- a/company.html +++ b/company.html @@ -65,6 +65,9 @@ Contact + + Admin + Login / Register diff --git a/css/style.css b/css/style.css index fb3b107..c8d8a07 100644 --- a/css/style.css +++ b/css/style.css @@ -391,6 +391,138 @@ html { font-size: 0.95rem; } +.admin-page { + min-height: 100vh; + padding: 3rem 1.5rem; + background: linear-gradient(180deg, #f9ffeb 0%, #f0ffdb 55%, #ffffff 100%); +} + +.admin-panel { + width: min(100%, 1180px); + margin: 0 auto; + background: rgba(255, 255, 255, 0.96); + border: 1px solid rgba(34, 84, 0, 0.12); + border-radius: 28px; + box-shadow: 0 36px 80px rgba(38, 88, 6, 0.12); + padding: 2rem; +} + +.admin-header, +.admin-code-row, +.admin-actions { + display: flex; + gap: 1rem; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; +} + +.admin-header h1 { + margin: 0 0 0.5rem; + color: #1d4700; +} + +.admin-header p { + margin: 0; + color: #4f6f31; +} + +.admin-code-row { + margin-top: 1.5rem; + justify-content: flex-start; +} + +.admin-code-row label { + color: #4b640d; + font-weight: 700; +} + +.admin-code-row .auth-input { + max-width: 260px; +} + +.admin-small-button { + width: auto; +} + +.admin-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.admin-box-card { + border: 1px solid rgba(34, 84, 0, 0.12); + border-radius: 22px; + padding: 1rem; + display: grid; + gap: 1rem; +} + +.admin-box-card label { + display: grid; + gap: 0.5rem; + color: #4b640d; + font-weight: 700; +} + +.admin-box-title { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; +} + +.admin-box-title h2 { + margin: 0; + color: #1d4700; +} + +.admin-box-title span { + color: #5b742e; +} + +.admin-image-preview, +.admin-image-placeholder { + width: 100%; + height: 180px; + border-radius: 18px; +} + +.admin-image-preview { + object-fit: cover; +} + +.admin-image-placeholder { + display: flex; + align-items: center; + justify-content: center; + border: 1px dashed rgba(34, 84, 0, 0.24); + color: #5b742e; +} + +.admin-actions { + margin-top: 1.5rem; + justify-content: flex-start; +} + +.admin-home-link { + text-align: center; + text-decoration: none; +} + +.box-card-image { + width: 92px; + height: 92px; + object-fit: cover; + border-radius: 14px; + margin-right: 24px; +} + +.box-card-description { + margin-top: 8px; +} + body { color: #333; background-color: #fff; diff --git a/index.html b/index.html index 684d737..a86fbaf 100644 --- a/index.html +++ b/index.html @@ -70,6 +70,9 @@ Contact + + Admin + Login / Register diff --git a/js/admin.js b/js/admin.js new file mode 100644 index 0000000..4e03cfd --- /dev/null +++ b/js/admin.js @@ -0,0 +1,138 @@ +const ADMIN_CODE_KEY = 'adminBoxCode'; +const adminCodeInput = document.getElementById('admin-code'); +const loadButton = document.getElementById('load-boxes'); +const saveButton = document.getElementById('save-boxes'); +const adminForm = document.getElementById('admin-form'); +const adminActions = document.getElementById('admin-actions'); +const adminMessage = document.getElementById('admin-message'); + +adminCodeInput.value = localStorage.getItem(ADMIN_CODE_KEY) || ''; + +function setAdminMessage(message) { + adminMessage.textContent = message; +} + +function apiBase() { + return `${window.location.protocol}//${window.location.host}/api`; +} + +function boxCard(box, index) { + const boxNumber = index + 1; + const imageValue = box.image || ''; + const imagePreview = imageValue + ? `box${boxNumber} preview` + : '
No picture
'; + + return ` +
+
+

box${boxNumber}

+ ${box.sku || `BOX-${String(boxNumber).padStart(3, '0')}`} +
+ ${imagePreview} + + + +
+ `; +} + +function renderBoxes(boxes) { + adminForm.innerHTML = boxes.map(boxCard).join(''); + adminForm.style.display = 'grid'; + adminActions.style.display = 'flex'; + + adminForm.querySelectorAll('.admin-image').forEach(input => { + input.addEventListener('input', () => { + const card = input.closest('.admin-box-card'); + const oldPreview = card.querySelector('.admin-image-preview, .admin-image-placeholder'); + const value = input.value.trim(); + const replacement = document.createElement(value ? 'img' : 'div'); + if (value) { + replacement.className = 'admin-image-preview'; + replacement.src = value; + replacement.alt = `${card.querySelector('h2').textContent} preview`; + } else { + replacement.className = 'admin-image-placeholder'; + replacement.textContent = 'No picture'; + } + oldPreview.replaceWith(replacement); + }); + }); +} + +async function loadBoxes() { + const code = adminCodeInput.value.trim(); + localStorage.setItem(ADMIN_CODE_KEY, code); + setAdminMessage('Loading boxes...'); + + const response = await fetch(`${apiBase()}/admin/boxes`); + const data = await response.json(); + if (!data.success) { + setAdminMessage(data.message || 'Could not load boxes.'); + return; + } + + renderBoxes(data.boxes); + setAdminMessage('Boxes loaded. Make changes, then save.'); +} + +function collectBoxes() { + return Array.from(adminForm.querySelectorAll('.admin-box-card')).map((card, index) => { + const boxNumber = index + 1; + return { + _id: card.dataset.id || `box-${boxNumber}`, + name: `box${boxNumber}`, + sku: `BOX-${String(boxNumber).padStart(3, '0')}`, + price: Number(card.querySelector('.admin-price').value || 0), + image: card.querySelector('.admin-image').value.trim(), + description: card.querySelector('.admin-description').value.trim(), + stock: 100, + isActive: true, + }; + }); +} + +async function saveBoxes() { + const code = adminCodeInput.value.trim(); + if (!code) { + setAdminMessage('Enter the admin code before saving.'); + return; + } + + localStorage.setItem(ADMIN_CODE_KEY, code); + setAdminMessage('Saving boxes...'); + + const response = await fetch(`${apiBase()}/admin/boxes`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'x-admin-code': code, + }, + body: JSON.stringify({ boxes: collectBoxes() }), + }); + const data = await response.json(); + + if (data.success) { + renderBoxes(data.boxes); + setAdminMessage('Saved. The order page and home menu now use these box details.'); + } else { + setAdminMessage(data.message || 'Save failed.'); + } +} + +loadButton.addEventListener('click', loadBoxes); +saveButton.addEventListener('click', saveBoxes); + +if (adminCodeInput.value) { + loadBoxes(); +} \ No newline at end of file diff --git a/js/replit-webflow-compat.js b/js/replit-webflow-compat.js index ddec5f9..c8c8754 100644 --- a/js/replit-webflow-compat.js +++ b/js/replit-webflow-compat.js @@ -11,13 +11,24 @@ } }); - function simplifyMenuBoxes() { + async function getBoxes() { + try { + const response = await fetch('/api/admin/boxes'); + const data = await response.json(); + return data && data.success && Array.isArray(data.boxes) ? data.boxes : []; + } catch (error) { + return []; + } + } + + async function simplifyMenuBoxes() { const path = window.location.pathname; const isMainPage = path === '/' || path.endsWith('/index.html'); const isOrderPage = path.endsWith('/order.html'); if (!isMainPage && !isOrderPage) return; const limit = isOrderPage ? 8 : 4; + const boxes = await getBoxes(); const tabs = document.querySelector('.w-tabs'); if (!tabs) return; @@ -41,12 +52,29 @@ firstList.innerHTML = ''; selectedItems.forEach((item, index) => { - item.querySelectorAll('.food-image-square, .paragraph').forEach(element => element.remove()); + const box = boxes[index] || {}; + const imageWrap = item.querySelector('.food-image-square'); + const image = item.querySelector('.food-image'); + if (imageWrap && image && box.image) { + image.src = box.image; + image.alt = `box${index + 1}`; + imageWrap.removeAttribute('href'); + } else if (imageWrap) { + imageWrap.remove(); + } + const paragraph = item.querySelector('.paragraph'); + if (paragraph && box.description) { + paragraph.textContent = box.description; + } else if (paragraph) { + paragraph.remove(); + } item.querySelectorAll('.quantity').forEach(element => { element.type = 'hidden'; }); const title = item.querySelector('h6'); if (title) title.textContent = `box${index + 1}`; + const price = item.querySelector('.price'); + if (price) price.textContent = `$${Number(box.price || 0).toFixed(2)} USD`; item.querySelectorAll('a[href]').forEach(link => link.removeAttribute('href')); firstList.appendChild(item); }); diff --git a/order.html b/order.html index ab50409..f162e1e 100644 --- a/order.html +++ b/order.html @@ -68,6 +68,9 @@
Contact + + Admin + Login / Register @@ -1370,19 +1373,29 @@ return { _id: product._id || `box-${index + 1}`, name: `box${index + 1}`, - price: Number(product.price || 0) + price: Number(product.price || 0), + image: product.image || '', + description: product.description || '' }; }); boxes.forEach(product => { + const imageHTML = product.image + ? `${product.name}` + : ''; + const descriptionHTML = product.description + ? `

${product.description}

` + : ''; const productHTML = `