Add an admin interface to edit product details for all boxes
Create a new admin page and backend endpoints to allow authorized users to modify product price, image, and description for the eight boxes. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 375ec6d3-d5af-4f82-ab81-5c60fd4a86a3 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 534b4c21-8691-4e0a-ba0c-0091bb20606a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/147e665c-8c0d-48ec-b0ad-fdc89cd4460f/375ec6d3-d5af-4f82-ab81-5c60fd4a86a3/e238nM8 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
171fd243de
commit
8f32ec7d16
39
admin.html
Normal file
39
admin.html
Normal file
@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Admin Box Editor</title>
|
||||
<link href="css/style.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="admin-page">
|
||||
<div class="admin-panel">
|
||||
<div class="admin-header">
|
||||
<div>
|
||||
<h1>Administrator box editor</h1>
|
||||
<p>Change the price, picture, and description for all 8 boxes.</p>
|
||||
</div>
|
||||
<a class="auth-tab admin-home-link" href="index.html">Back to home</a>
|
||||
</div>
|
||||
|
||||
<div class="admin-code-row">
|
||||
<label for="admin-code">Admin code</label>
|
||||
<input class="auth-input" id="admin-code" type="password" placeholder="Enter admin code" autocomplete="off" />
|
||||
<button class="auth-submit admin-small-button" id="load-boxes" type="button">Load boxes</button>
|
||||
</div>
|
||||
|
||||
<p id="admin-message" class="auth-note">Enter the admin code to edit box details.</p>
|
||||
|
||||
<form id="admin-form" class="admin-grid" style="display:none;"></form>
|
||||
|
||||
<div class="admin-actions" id="admin-actions" style="display:none;">
|
||||
<button class="auth-submit" id="save-boxes" type="button">Save all box changes</button>
|
||||
<a class="auth-tab admin-home-link" href="order.html">View order page</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
@ -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();
|
||||
|
||||
71
backend/routes/adminRoutes.js
Normal file
71
backend/routes/adminRoutes.js
Normal file
@ -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;
|
||||
@ -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'));
|
||||
|
||||
|
||||
@ -65,6 +65,9 @@
|
||||
<a class="nav-link w-nav-link" href="index.html#call-store">
|
||||
Contact
|
||||
</a>
|
||||
<a class="nav-link w-nav-link" href="admin.html">
|
||||
Admin
|
||||
</a>
|
||||
</nav>
|
||||
<a class="auth-link" href="login.html">
|
||||
Login / Register
|
||||
|
||||
132
css/style.css
132
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;
|
||||
|
||||
@ -70,6 +70,9 @@
|
||||
<a class="nav-link w-nav-link" href="#call-store">
|
||||
Contact
|
||||
</a>
|
||||
<a class="nav-link w-nav-link" href="admin.html">
|
||||
Admin
|
||||
</a>
|
||||
</nav>
|
||||
<a class="auth-link" href="login.html">
|
||||
Login / Register
|
||||
|
||||
138
js/admin.js
Normal file
138
js/admin.js
Normal file
@ -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
|
||||
? `<img class="admin-image-preview" src="${imageValue}" alt="box${boxNumber} preview" />`
|
||||
: '<div class="admin-image-placeholder">No picture</div>';
|
||||
|
||||
return `
|
||||
<section class="admin-box-card" data-index="${index}" data-id="${box._id || `box-${boxNumber}`}">
|
||||
<div class="admin-box-title">
|
||||
<h2>box${boxNumber}</h2>
|
||||
<span>${box.sku || `BOX-${String(boxNumber).padStart(3, '0')}`}</span>
|
||||
</div>
|
||||
${imagePreview}
|
||||
<label>
|
||||
Price
|
||||
<input class="auth-input admin-price" type="number" min="0" step="0.01" value="${Number(box.price || 0).toFixed(2)}" />
|
||||
</label>
|
||||
<label>
|
||||
Picture URL
|
||||
<input class="auth-input admin-image" type="url" placeholder="https://..." value="${imageValue}" />
|
||||
</label>
|
||||
<label>
|
||||
Description
|
||||
<textarea class="auth-input admin-description" rows="4" placeholder="Describe this box">${box.description || ''}</textarea>
|
||||
</label>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
|
||||
15
order.html
15
order.html
@ -68,6 +68,9 @@
|
||||
<a class="nav-link w-nav-link" href="index.html#call-store">
|
||||
Contact
|
||||
</a>
|
||||
<a class="nav-link w-nav-link" href="admin.html">
|
||||
Admin
|
||||
</a>
|
||||
</nav>
|
||||
<a class="auth-link" href="login.html">
|
||||
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
|
||||
? `<img class="box-card-image" src="${product.image}" alt="${product.name}" />`
|
||||
: '';
|
||||
const descriptionHTML = product.description
|
||||
? `<p class="paragraph box-card-description">${product.description}</p>`
|
||||
: '';
|
||||
const productHTML = `
|
||||
<div class="menu-item w-dyn-item w-col w-col-6" role="listitem">
|
||||
<div class="food-card">
|
||||
${imageHTML}
|
||||
<div class="food-card-content">
|
||||
<div class="food-title-wrap w-inline-block">
|
||||
<h6>${product.name}</h6>
|
||||
<div class="price">$${product.price.toFixed(2)} USD</div>
|
||||
</div>
|
||||
${descriptionHTML}
|
||||
<div class="add-to-cart">
|
||||
<input class="quantity" type="hidden" min="1" value="1" id="quantity-${product._id}" />
|
||||
<button class="order-button w-button add-to-cart-btn"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user