Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
986d159d20 1 2026-04-15 10:39:38 +00:00
20 changed files with 499 additions and 356 deletions

View File

@ -2,38 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="refresh" content="0; url=box-editor.html" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Box Editor</title> <title>Redirecting...</title>
<link href="css/style.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>
<div class="admin-page"> <p>Redirecting to <a href="box-editor.html">Box Data Editor</a>...</p>
<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> </body>
</html> </html>

58
backend/data/boxes.json Normal file
View File

@ -0,0 +1,58 @@
[
{
"id": "box-1",
"name": "Small Shipping Box",
"price": 9.99,
"image": "images/boxes/box-1.svg",
"description": "Compact corrugated box for light parts, samples, and small product shipments."
},
{
"id": "box-2",
"name": "Medium Mailer Box",
"price": 12.5,
"image": "images/boxes/box-2.svg",
"description": "Strong everyday mailer with extra room for inserts, labels, and protective padding."
},
{
"id": "box-3",
"name": "Heavy Duty Carton",
"price": 15.25,
"image": "images/boxes/box-3.svg",
"description": "Durable double-wall carton built for heavier items and warehouse handling."
},
{
"id": "box-4",
"name": "Retail Display Box",
"price": 18,
"image": "images/boxes/box-4.svg",
"description": "Clean presentation box designed for retail shelves, kits, and branded packaging."
},
{
"id": "box-5",
"name": "Long Parts Box",
"price": 14.75,
"image": "images/boxes/box-5.svg",
"description": "Extended-length box for narrow components, rods, trims, and specialty parts."
},
{
"id": "box-6",
"name": "Deep Storage Box",
"price": 16.5,
"image": "images/boxes/box-6.svg",
"description": "Deep format storage box for bulk inventory, stacked packing, and backroom use."
},
{
"id": "box-7",
"name": "Flat Pack Box",
"price": 11.95,
"image": "images/boxes/box-7.svg",
"description": "Low-profile box for folded materials, manuals, labels, and flat-pack assemblies."
},
{
"id": "box-8",
"name": "Large Bulk Box",
"price": 22,
"image": "images/boxes/box-8.svg",
"description": "Large-capacity box for bundled products, bulk orders, and industrial shipments."
}
]

62
backend/lib/boxesStore.js Normal file
View File

@ -0,0 +1,62 @@
const fs = require('fs');
const path = require('path');
const BOXES_FILE = path.join(__dirname, '..', 'data', 'boxes.json');
const DEFAULT_BOXES = Array.from({ length: 8 }, (_, index) => ({
id: `box-${index + 1}`,
name: `Box ${index + 1}`,
price: 0,
image: '',
description: '',
}));
function ensureBoxesFile() {
const dir = path.dirname(BOXES_FILE);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (!fs.existsSync(BOXES_FILE)) {
fs.writeFileSync(BOXES_FILE, JSON.stringify(DEFAULT_BOXES, null, 2));
}
}
function normalizeBox(box = {}, index = 0) {
return {
id: box.id || `box-${index + 1}`,
name: String(box.name || `Box ${index + 1}`).trim(),
price: Number(box.price || 0),
image: String(box.image || '').trim(),
description: String(box.description || '').trim(),
};
}
function readBoxes() {
ensureBoxesFile();
try {
const raw = fs.readFileSync(BOXES_FILE, 'utf8');
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
return DEFAULT_BOXES.map(normalizeBox);
}
const padded = Array.from({ length: 8 }, (_, index) => normalizeBox(parsed[index] || {}, index));
return padded;
} catch (error) {
console.error('Error reading boxes file:', error);
return DEFAULT_BOXES.map(normalizeBox);
}
}
function writeBoxes(boxes = []) {
ensureBoxesFile();
const normalized = Array.from({ length: 8 }, (_, index) => normalizeBox(boxes[index] || {}, index));
fs.writeFileSync(BOXES_FILE, JSON.stringify(normalized, null, 2));
return normalized;
}
module.exports = {
BOXES_FILE,
readBoxes,
writeBoxes,
};

View File

@ -0,0 +1,38 @@
const express = require('express');
const { readBoxes, writeBoxes } = require('../lib/boxesStore');
const router = express.Router();
const EDITOR_CODE = process.env.BOX_EDITOR_CODE || process.env.ADMIN_CODE || '1234';
function requireEditorCode(req, res, next) {
const code = req.headers['x-editor-code'] || req.body.editorCode || req.query.editorCode;
if (code !== EDITOR_CODE) {
return res.status(401).json({ success: false, message: 'Invalid editor code' });
}
next();
}
router.get('/', (req, res) => {
res.json({
success: true,
boxes: readBoxes(),
file: 'backend/data/boxes.json',
});
});
router.put('/', requireEditorCode, (req, res) => {
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 savedBoxes = writeBoxes(boxes);
res.json({
success: true,
message: 'Box data saved successfully.',
boxes: savedBoxes,
file: 'backend/data/boxes.json',
});
});
module.exports = router;

View File

@ -7,7 +7,6 @@ require('dotenv').config();
const app = express(); const app = express();
app.set('trust proxy', true); app.set('trust proxy', true);
// Middleware
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
@ -58,39 +57,28 @@ app.post(['/.wf_graphql/apollo', '/.wf_graphql/usys/apollo'], (req, res) => {
}); });
}); });
// Serve static files from the root directory app.use('/api/boxes', require('./routes/boxRoutes'));
app.use(express.static(path.join(__dirname, '..'))); app.use('/api/auth', require('./routes/authRoutes'));
app.use('/api/users', require('./routes/userRoutes'));
app.use('/api/products', require('./routes/productRoutes'));
app.use('/api/cart', require('./routes/cartRoutes'));
app.use('/api/orders', require('./routes/orderRoutes'));
// Basic route
app.get('/', (req, res) => {
res.json({ message: 'Welcome to Mom\'s Web API' });
});
// Health check
app.get('/api/health', (req, res) => { app.get('/api/health', (req, res) => {
res.json({ status: 'Server is running' }); res.json({ status: 'Server is running' });
}); });
// Routes app.use(express.static(path.join(__dirname, '..')));
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'));
// Error handling middleware
app.use((err, req, res, next) => { app.use((err, req, res, next) => {
console.error(err.stack); console.error(err.stack);
res.status(500).json({ message: 'Internal Server Error' }); res.status(500).json({ message: 'Internal Server Error' });
}); });
// 404 handler
app.use((req, res) => { app.use((req, res) => {
res.status(404).json({ message: 'Route not found' }); res.status(404).json({ message: 'Route not found' });
}); });
// Start server
const PORT = process.env.PORT || 5000; const PORT = process.env.PORT || 5000;
const HOST = process.env.HOST || '0.0.0.0'; const HOST = process.env.HOST || '0.0.0.0';
app.listen(PORT, HOST, () => { app.listen(PORT, HOST, () => {

39
box-editor.html Normal file
View 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>Box Data 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>Box data editor</h1>
<p>Update the name, price, picture, and description for all 8 boxes.</p>
</div>
<a class="auth-tab admin-home-link" href="order.html">View boxes</a>
</div>
<div class="admin-code-row">
<label for="editor-code">Editor code</label>
<input class="auth-input" id="editor-code" type="password" placeholder="Enter editor code" autocomplete="off" />
<button class="auth-submit admin-small-button" id="load-boxes" type="button">Load boxes</button>
</div>
<p id="editor-message" class="auth-note">Tip: you can also edit <strong>backend/data/boxes.json</strong> directly in the codebase.</p>
<form id="editor-form" class="admin-grid" style="display:none;"></form>
<div class="admin-actions" id="editor-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="index.html">Back to home</a>
</div>
</div>
</div>
<script src="js/box-editor.js"></script>
</body>
</html>

View File

@ -65,13 +65,7 @@
<a class="nav-link w-nav-link" href="index.html#call-store"> <a class="nav-link w-nav-link" href="index.html#call-store">
Contact Contact
</a> </a>
<a class="nav-link w-nav-link" href="admin.html">
Admin
</a>
</nav> </nav>
<a class="auth-link" href="login.html">
Login / Register
</a>
<div class="w-commerce-commercecartwrapper" data-node-type="commerce-cart-wrapper" data-open-product="" data-wf-cart-query="query Dynamo3 { <div class="w-commerce-commercecartwrapper" data-node-type="commerce-cart-wrapper" data-open-product="" data-wf-cart-query="query Dynamo3 {
database { database {
id id

9
images/boxes/box-1.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#f59e0b" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#f59e0b"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 1</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

9
images/boxes/box-2.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#fb7185" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#fb7185"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 2</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

9
images/boxes/box-3.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#60a5fa" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#60a5fa"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 3</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

9
images/boxes/box-4.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#34d399" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#34d399"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 4</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

9
images/boxes/box-5.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#a78bfa" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#a78bfa"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 5</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

9
images/boxes/box-6.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#f97316" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#f97316"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 6</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

9
images/boxes/box-7.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#22c55e" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#22c55e"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 7</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

9
images/boxes/box-8.svg Normal file
View File

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" viewBox="0 0 640 480" fill="none">
<rect width="640" height="480" rx="36" fill="#fff7ed"/>
<rect x="110" y="150" width="420" height="210" rx="24" fill="#06b6d4" opacity="0.18"/>
<path d="M170 170L320 110L470 170L320 230L170 170Z" fill="#06b6d4"/>
<path d="M170 170V310L320 370V230L170 170Z" fill="#d97706" opacity="0.78"/>
<path d="M470 170V310L320 370V230L470 170Z" fill="#92400e" opacity="0.88"/>
<path d="M245 140L395 200" stroke="#fff" stroke-width="10" stroke-linecap="round" opacity="0.55"/>
<text x="320" y="430" text-anchor="middle" fill="#7c2d12" font-size="34" font-family="Arial, Helvetica, sans-serif" font-weight="700">BOX 8</text>
</svg>

After

Width:  |  Height:  |  Size: 730 B

View File

@ -70,13 +70,7 @@
<a class="nav-link w-nav-link" href="#call-store"> <a class="nav-link w-nav-link" href="#call-store">
Contact Contact
</a> </a>
<a class="nav-link w-nav-link" href="admin.html">
Admin
</a>
</nav> </nav>
<a class="auth-link" href="login.html">
Login / Register
</a>
<div class="w-commerce-commercecartwrapper" data-node-type="commerce-cart-wrapper" data-open-product="" data-wf-cart-query="query Dynamo3 { <div class="w-commerce-commercecartwrapper" data-node-type="commerce-cart-wrapper" data-open-product="" data-wf-cart-query="query Dynamo3 {
database { database {
id id
@ -1160,8 +1154,6 @@
</div> </div>
<script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="js/jquery-3.5.1.min.dc5e7f18c8.js" type="text/javascript"> <script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="js/jquery-3.5.1.min.dc5e7f18c8.js" type="text/javascript">
</script> </script>
<script src="js/api.js" type="text/javascript">
</script>
<script crossorigin="anonymous" integrity="sha384-O3WhxmHD5kHQSrCGxUx9O9aqDChbj1vVrhT3htBjpSnAOHYYJ9ESiPlOvc9TRncW" src="js/webflow.schunk.b5641674105b3a84.js" type="text/javascript"> <script crossorigin="anonymous" integrity="sha384-O3WhxmHD5kHQSrCGxUx9O9aqDChbj1vVrhT3htBjpSnAOHYYJ9ESiPlOvc9TRncW" src="js/webflow.schunk.b5641674105b3a84.js" type="text/javascript">
</script> </script>
<script crossorigin="anonymous" integrity="sha384-4AhZ6V15MqQcYDdfPXvfst2Y4s5C3Rtj7m/IcGSRXAN4ocxqL/Fag0UYpfUlMcst" src="js/webflow.schunk.8635f867f0e7bd37.js" type="text/javascript"> <script crossorigin="anonymous" integrity="sha384-4AhZ6V15MqQcYDdfPXvfst2Y4s5C3Rtj7m/IcGSRXAN4ocxqL/Fag0UYpfUlMcst" src="js/webflow.schunk.8635f867f0e7bd37.js" type="text/javascript">

138
js/box-editor.js Normal file
View File

@ -0,0 +1,138 @@
const EDITOR_CODE_KEY = 'boxEditorCode';
const editorCodeInput = document.getElementById('editor-code');
const loadButton = document.getElementById('load-boxes');
const saveButton = document.getElementById('save-boxes');
const editorForm = document.getElementById('editor-form');
const editorActions = document.getElementById('editor-actions');
const editorMessage = document.getElementById('editor-message');
editorCodeInput.value = localStorage.getItem(EDITOR_CODE_KEY) || '';
function setEditorMessage(message) {
editorMessage.innerHTML = message;
}
function apiBase() {
return `${window.location.protocol}//${window.location.host}/api/boxes`;
}
function boxCard(box, index) {
const imageValue = box.image || '';
const preview = imageValue
? `<img class="admin-image-preview" src="${imageValue}" alt="${box.name || `Box ${index + 1}`} preview" />`
: '<div class="admin-image-placeholder">No picture</div>';
return `
<section class="admin-box-card" data-index="${index}" data-id="${box.id || `box-${index + 1}`}">
<div class="admin-box-title">
<h2>${box.name || `Box ${index + 1}`}</h2>
<span>${box.id || `box-${index + 1}`}</span>
</div>
${preview}
<label>
Box name
<input class="auth-input editor-name" type="text" value="${box.name || ''}" />
</label>
<label>
Price
<input class="auth-input editor-price" type="number" min="0" step="0.01" value="${Number(box.price || 0).toFixed(2)}" />
</label>
<label>
Picture URL or relative path
<input class="auth-input editor-image" type="text" placeholder="images/boxes/box-1.svg or https://..." value="${imageValue}" />
</label>
<label>
Description
<textarea class="auth-input editor-description" rows="4" placeholder="Describe this box">${box.description || ''}</textarea>
</label>
</section>
`;
}
function bindPreviewListeners() {
editorForm.querySelectorAll('.editor-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('.editor-name').value || 'Box'} preview`;
} else {
replacement.className = 'admin-image-placeholder';
replacement.textContent = 'No picture';
}
oldPreview.replaceWith(replacement);
});
});
}
function renderBoxes(boxes) {
editorForm.innerHTML = boxes.map(boxCard).join('');
editorForm.style.display = 'grid';
editorActions.style.display = 'flex';
bindPreviewListeners();
}
async function loadBoxes() {
const code = editorCodeInput.value.trim();
localStorage.setItem(EDITOR_CODE_KEY, code);
setEditorMessage('Loading boxes...');
const response = await fetch(apiBase());
const data = await response.json();
if (!data.success) {
setEditorMessage(data.message || 'Could not load boxes.');
return;
}
renderBoxes(data.boxes);
setEditorMessage(`Boxes loaded. You can also edit <strong>${data.file}</strong> directly in the backend code.`);
}
function collectBoxes() {
return Array.from(editorForm.querySelectorAll('.admin-box-card')).map((card, index) => ({
id: card.dataset.id || `box-${index + 1}`,
name: card.querySelector('.editor-name').value.trim() || `Box ${index + 1}`,
price: Number(card.querySelector('.editor-price').value || 0),
image: card.querySelector('.editor-image').value.trim(),
description: card.querySelector('.editor-description').value.trim(),
}));
}
async function saveBoxes() {
const code = editorCodeInput.value.trim();
if (!code) {
setEditorMessage('Enter the editor code before saving.');
return;
}
localStorage.setItem(EDITOR_CODE_KEY, code);
setEditorMessage('Saving boxes...');
const response = await fetch(apiBase(), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-editor-code': code,
},
body: JSON.stringify({ boxes: collectBoxes() }),
});
const data = await response.json();
if (data.success) {
renderBoxes(data.boxes);
setEditorMessage(`Saved. The storefront is now reading from <strong>${data.file}</strong>.`);
} else {
setEditorMessage(data.message || 'Save failed.');
}
}
loadButton.addEventListener('click', loadBoxes);
saveButton.addEventListener('click', saveBoxes);
if (editorCodeInput.value) {
loadBoxes();
}

View File

@ -13,30 +13,88 @@
async function getBoxes() { async function getBoxes() {
try { try {
const response = await fetch('/api/admin/boxes'); const response = await fetch('/api/boxes');
const data = await response.json(); const data = await response.json();
return data && data.success && Array.isArray(data.boxes) ? data.boxes : []; return data && data.success && Array.isArray(data.boxes) ? data.boxes : [];
} catch (error) { } catch (error) {
console.error('Could not load box data:', error);
return []; return [];
} }
} }
function cleanPublicNavigation() {
document.querySelectorAll('a[href="admin.html"], a[href="login.html"]').forEach(link => link.remove());
document.querySelectorAll('.auth-link, .w-commerce-commercecartwrapper').forEach(element => element.remove());
}
function updatePublicCopy(isOrderPage) {
const header = document.querySelector('.header-h1');
if (header && isOrderPage) {
header.innerHTML = 'Browse our <span class="brand-span">box catalog</span> and request the right packaging for your products.';
}
const titleWrap = document.querySelector('.content-section .title-wrap-centre');
if (titleWrap) {
const title = titleWrap.querySelector('h2');
const text = titleWrap.querySelector('p');
if (title) {
title.textContent = isOrderPage ? 'Available Boxes' : 'Featured Boxes';
}
if (text) {
text.textContent = isOrderPage
? 'All box cards below are loaded from backend/data/boxes.json, so prices, pictures, and descriptions can be updated in one place.'
: 'Your featured box cards on this page are also loaded from the backend box data file.';
}
}
}
function applyBoxDataToItems(items, boxes) {
items.forEach((item, index) => {
const box = boxes[index] || {};
const title = item.querySelector('h6');
const price = item.querySelector('.price');
const description = item.querySelector('.paragraph');
const imageWrap = item.querySelector('.food-image-square');
const image = item.querySelector('.food-image');
const addToCart = item.querySelector('.add-to-cart');
if (title) title.textContent = box.name || `Box ${index + 1}`;
if (price) price.textContent = `$${Number(box.price || 0).toFixed(2)}`;
if (description) description.textContent = box.description || 'Update this description in backend/data/boxes.json.';
if (imageWrap && image && box.image) {
image.src = box.image;
image.alt = box.name || `Box ${index + 1}`;
imageWrap.removeAttribute('href');
}
item.querySelectorAll('a[href]').forEach(link => link.removeAttribute('href'));
item.querySelectorAll('.quantity, .w-commerce-commerceaddtocarterror, .w-commerce-commerceaddtocartoutofstock').forEach(el => el.remove());
if (addToCart) {
addToCart.innerHTML = '<div class="button button-space" style="display:inline-block;">Update in Box Editor</div>';
}
});
}
async function simplifyMenuBoxes() { async function simplifyMenuBoxes() {
const path = window.location.pathname; const path = window.location.pathname;
const isMainPage = path === '/' || path.endsWith('/index.html'); const isMainPage = path === '/' || path.endsWith('/index.html');
const isOrderPage = path.endsWith('/order.html'); const isOrderPage = path.endsWith('/order.html');
if (!isMainPage && !isOrderPage) return; if (!isMainPage && !isOrderPage) return;
const limit = isOrderPage ? 8 : 4;
const boxes = await getBoxes(); const boxes = await getBoxes();
if (!boxes.length) return;
cleanPublicNavigation();
updatePublicCopy(isOrderPage);
const tabs = document.querySelector('.w-tabs'); const tabs = document.querySelector('.w-tabs');
if (!tabs) return; if (!tabs) return;
const tabMenu = tabs.querySelector('.tab-menu-round'); const tabMenu = tabs.querySelector('.tab-menu-round');
if (tabMenu) tabMenu.remove(); if (tabMenu) tabMenu.remove();
const items = Array.from(tabs.querySelectorAll('.menu-item'));
const selectedItems = items.slice(0, limit);
const firstPane = tabs.querySelector('.w-tab-pane'); const firstPane = tabs.querySelector('.w-tab-pane');
const firstList = firstPane && firstPane.querySelector('.w-dyn-items'); const firstList = firstPane && firstPane.querySelector('.w-dyn-items');
if (!firstPane || !firstList) return; if (!firstPane || !firstList) return;
@ -50,41 +108,29 @@
} }
}); });
const itemTemplate = Array.from(firstList.querySelectorAll('.menu-item'));
const limit = isOrderPage ? 8 : 4;
const selectedBoxes = boxes.slice(0, limit);
const template = itemTemplate[0];
if (!template) return;
firstList.innerHTML = ''; firstList.innerHTML = '';
selectedItems.forEach((item, index) => { selectedBoxes.forEach((box, index) => {
const box = boxes[index] || {}; const clone = template.cloneNode(true);
const imageWrap = item.querySelector('.food-image-square'); applyBoxDataToItems([clone], [box]);
const image = item.querySelector('.food-image'); firstList.appendChild(clone);
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);
}); });
firstPane.querySelectorAll('.pagination').forEach(element => element.remove()); firstPane.querySelectorAll('.pagination').forEach(element => element.remove());
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', simplifyMenuBoxes); document.addEventListener('DOMContentLoaded', () => {
cleanPublicNavigation();
simplifyMenuBoxes();
});
} else { } else {
cleanPublicNavigation();
simplifyMenuBoxes(); simplifyMenuBoxes();
} }
})(); })();

View File

@ -2,135 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta http-equiv="refresh" content="0; url=index.html" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Login / Register</title> <title>Redirecting...</title>
<link href="css/style.css" rel="stylesheet" type="text/css" />
</head> </head>
<body> <body>
<div class="auth-page"> <p>This page is no longer used. Return to <a href="index.html">Home</a>.</p>
<div class="auth-card">
<h1>Login or register</h1>
<p>Access your account and start ordering with a clean, secure experience.</p>
<div class="auth-tabs">
<button class="auth-tab active" type="button" data-target="login">Login</button>
<button class="auth-tab" type="button" data-target="register">Register</button>
</div>
<form id="login-form" class="auth-form">
<div>
<div class="auth-field">
<label for="login-email">Email</label>
<input class="auth-input" id="login-email" type="email" placeholder="you@example.com" autocomplete="email" required />
</div>
<div class="auth-field">
<label for="login-password">Password</label>
<input class="auth-input" id="login-password" type="password" placeholder="••••••••" autocomplete="current-password" required />
</div>
<button class="auth-submit" type="submit">Login</button>
</div>
</form>
<form id="register-form" class="auth-form" style="display:none;">
<div>
<div class="auth-field">
<label for="register-firstname">First name</label>
<input class="auth-input" id="register-firstname" type="text" placeholder="First name" autocomplete="given-name" required />
</div>
<div class="auth-field">
<label for="register-lastname">Last name</label>
<input class="auth-input" id="register-lastname" type="text" placeholder="Last name" autocomplete="family-name" required />
</div>
<div class="auth-field">
<label for="register-email">Email</label>
<input class="auth-input" id="register-email" type="email" placeholder="you@example.com" autocomplete="email" required />
</div>
<div class="auth-field">
<label for="register-password">Password</label>
<input class="auth-input" id="register-password" type="password" placeholder="••••••••" autocomplete="new-password" required />
</div>
<div class="auth-field">
<label for="register-confirm">Confirm password</label>
<input class="auth-input" id="register-confirm" type="password" placeholder="••••••••" autocomplete="new-password" required />
</div>
<button class="auth-submit" type="submit">Register</button>
</div>
</form>
<p id="auth-message" class="auth-note">Use your email and password to access your account.</p>
<p class="auth-note"><a href="index.html">Back to home</a></p>
</div>
</div>
<script src="js/api.js"></script>
<script>
const tabs = document.querySelectorAll('.auth-tab');
const loginForm = document.getElementById('login-form');
const registerForm = document.getElementById('register-form');
const authMessage = document.getElementById('auth-message');
function setMessage(message) {
authMessage.textContent = message;
}
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(btn => btn.classList.remove('active'));
tab.classList.add('active');
const target = tab.dataset.target;
if (target === 'login') {
loginForm.style.display = 'block';
registerForm.style.display = 'none';
setMessage('Use your email and password to access your account.');
} else {
loginForm.style.display = 'none';
registerForm.style.display = 'block';
setMessage('Create an account with a password of at least 6 characters.');
}
});
});
loginForm.addEventListener('submit', async (event) => {
event.preventDefault();
setMessage('Logging in...');
const email = document.getElementById('login-email').value.trim();
const password = document.getElementById('login-password').value;
const result = await login(email, password);
if (result && result.success) {
setMessage('Login successful. Opening the order page...');
window.location.href = 'order.html';
} else {
setMessage('Login failed. Check your email and password.');
}
});
registerForm.addEventListener('submit', async (event) => {
event.preventDefault();
const firstName = document.getElementById('register-firstname').value.trim();
const lastName = document.getElementById('register-lastname').value.trim();
const email = document.getElementById('register-email').value.trim();
const password = document.getElementById('register-password').value;
const passwordConfirm = document.getElementById('register-confirm').value;
if (password !== passwordConfirm) {
setMessage('Passwords do not match.');
return;
}
if (password.length < 6) {
setMessage('Password must be at least 6 characters.');
return;
}
setMessage('Creating your account...');
const result = await register(firstName, lastName, email, password, passwordConfirm);
if (result && result.success) {
setMessage('Registration successful. Opening the order page...');
window.location.href = 'order.html';
} else {
setMessage('Registration failed. Try a different email or password.');
}
});
</script>
</body> </body>
</html> </html>

View File

@ -68,13 +68,7 @@
<a class="nav-link w-nav-link" href="index.html#call-store"> <a class="nav-link w-nav-link" href="index.html#call-store">
Contact Contact
</a> </a>
<a class="nav-link w-nav-link" href="admin.html">
Admin
</a>
</nav> </nav>
<a class="auth-link" href="login.html">
Login / Register
</a>
<div class="w-commerce-commercecartwrapper" data-node-type="commerce-cart-wrapper" data-open-product="" data-wf-cart-query="query Dynamo3 { <div class="w-commerce-commercecartwrapper" data-node-type="commerce-cart-wrapper" data-open-product="" data-wf-cart-query="query Dynamo3 {
database { database {
id id
@ -1332,133 +1326,6 @@
</div> </div>
<script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="js/jquery-3.5.1.min.dc5e7f18c8.js" type="text/javascript"> <script crossorigin="anonymous" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" src="js/jquery-3.5.1.min.dc5e7f18c8.js" type="text/javascript">
</script> </script>
<script src="js/api.js" type="text/javascript">
</script>
<script>
// Load products when page loads
document.addEventListener('DOMContentLoaded', async function() {
console.log('Loading products...');
try {
const productsData = await getAllProducts();
if (productsData && productsData.success && productsData.products) {
displayProducts(productsData.products);
} else {
console.error('Failed to load products:', productsData);
// Show fallback message
showFallbackProducts();
}
} catch (error) {
console.error('Error loading products:', error);
// Show fallback for development
showFallbackProducts();
}
});
function showFallbackProducts() {
displayProducts([]);
}
function displayProducts(products) {
const productGrid = document.querySelector('.w-tab-pane.w--tab-active .order-collection');
if (!productGrid) {
console.error('Active tab product grid not found');
return;
}
productGrid.innerHTML = '';
const boxes = Array.from({ length: 8 }, (_, index) => {
const product = products[index] || {};
return {
_id: product._id || `box-${index + 1}`,
name: `box${index + 1}`,
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"
data-product-id="${product._id}"
data-product-name="${product.name}">
Add to Cart
</button>
</div>
</div>
</div>
</div>
`;
productGrid.insertAdjacentHTML('beforeend', productHTML);
});
// Add event listeners to Add to Cart buttons
document.querySelectorAll('.add-to-cart-btn').forEach(button => {
button.addEventListener('click', async function() {
const productId = this.getAttribute('data-product-id');
const productName = this.getAttribute('data-product-name');
const quantityInput = document.getElementById(`quantity-${productId}`);
const quantity = parseInt(quantityInput.value) || 1;
console.log(`Adding ${quantity} x ${productName} to cart`);
try {
const result = await addToCart(productId, quantity);
if (result) {
alert(`${quantity} x ${productName} added to cart!`);
// Update cart count if needed
updateCartCount();
} else {
alert('Failed to add item to cart - API may be blocked by browser security');
}
} catch (error) {
console.error('Cart API error:', error);
alert('Cart functionality blocked by browser security. This is expected on HTTP sites.');
}
});
});
}
// Function to update cart count
async function updateCartCount() {
try {
const cart = await getCart();
if (cart && cart.items) {
const totalItems = cart.items.reduce((sum, item) => sum + item.quantity, 0);
const cartCountElement = document.querySelector('.cart-quantity');
if (cartCountElement) {
cartCountElement.textContent = totalItems;
}
}
} catch (error) {
console.error('Error updating cart count:', error);
}
}
// Load cart count on page load
document.addEventListener('DOMContentLoaded', function() {
updateCartCount();
});
</script>
<script crossorigin="anonymous" integrity="sha384-4AhZ6V15MqQcYDdfPXvfst2Y4s5C3Rtj7m/IcGSRXAN4ocxqL/Fag0UYpfUlMcst" src="js/webflow.schunk.8635f867f0e7bd37.js" type="text/javascript"> <script crossorigin="anonymous" integrity="sha384-4AhZ6V15MqQcYDdfPXvfst2Y4s5C3Rtj7m/IcGSRXAN4ocxqL/Fag0UYpfUlMcst" src="js/webflow.schunk.8635f867f0e7bd37.js" type="text/javascript">
</script> </script>
<script crossorigin="anonymous" integrity="sha384-ID0YQsmVv1RvR08bKK9/r3E6ht8sEBszDbo80TPdSDneZNcUQIGiXgNQMe2H2WQl" src="js/webflow.61cfa502.51ab170f9bd4cb2e.js" type="text/javascript"> <script crossorigin="anonymous" integrity="sha384-ID0YQsmVv1RvR08bKK9/r3E6ht8sEBszDbo80TPdSDneZNcUQIGiXgNQMe2H2WQl" src="js/webflow.61cfa502.51ab170f9bd4cb2e.js" type="text/javascript">