refactor: Restructure category and product data fetching/syncing and add a product detail pop-up UI.
This commit is contained in:
144
index.html
144
index.html
@@ -411,6 +411,99 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Product Detail Popper */
|
||||
.product-popper {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
z-index: 1000;
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.product-popper.visible {
|
||||
display: block;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.popper-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
display: none;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.popper-overlay.visible {
|
||||
display: block;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
.popper-close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
padding: 0.5rem;
|
||||
line-height: 1;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.popper-close:hover {
|
||||
background: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.popper-content h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: #2d3748;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.popper-content {
|
||||
line-height: 1.6;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.product-name-link {
|
||||
cursor: pointer;
|
||||
color: #4a5568;
|
||||
transition: color 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.product-name-link:hover {
|
||||
color: #667eea;
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@@ -426,6 +519,14 @@
|
||||
</div>
|
||||
<button id="version-reload-btn" onclick="location.reload()">New version - [reload]</button>
|
||||
|
||||
<!-- Product Detail Popper -->
|
||||
<div id="popper-overlay" class="popper-overlay"></div>
|
||||
<div id="product-popper" class="product-popper">
|
||||
<button class="popper-close" onclick="closePopper()">×</button>
|
||||
<div id="popper-content" class="popper-content"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/socket.io/socket.io.js" async></script>
|
||||
<script>
|
||||
// Initialize socket when io is available (async)
|
||||
@@ -881,7 +982,12 @@
|
||||
}
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = 'product-name-link';
|
||||
span.textContent = p.cName;
|
||||
span.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
showProductDetails(p.kArtikel);
|
||||
};
|
||||
li.appendChild(span);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
@@ -941,6 +1047,44 @@
|
||||
if (node.children) collapseAllProducts(node.children);
|
||||
});
|
||||
}
|
||||
|
||||
// Product Details Popper Logic
|
||||
const popperOverlay = document.getElementById('popper-overlay');
|
||||
const productPopper = document.getElementById('product-popper');
|
||||
const popperContent = document.getElementById('popper-content');
|
||||
|
||||
function closePopper() {
|
||||
popperOverlay.classList.remove('visible');
|
||||
productPopper.classList.remove('visible');
|
||||
popperContent.innerHTML = '';
|
||||
}
|
||||
|
||||
popperOverlay.addEventListener('click', closePopper);
|
||||
|
||||
// Close on Escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closePopper();
|
||||
});
|
||||
|
||||
async function showProductDetails(id) {
|
||||
// Show loading state
|
||||
popperContent.innerHTML = '<div class="loading">Loading details...</div>';
|
||||
popperOverlay.classList.add('visible');
|
||||
productPopper.classList.add('visible');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/products/${id}/details`);
|
||||
if (!response.ok) throw new Error('Failed to load details');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// The description is in cBeschreibung
|
||||
popperContent.innerHTML = data.cBeschreibung || 'No description available.';
|
||||
} catch (err) {
|
||||
console.error('Error fetching product details:', err);
|
||||
popperContent.innerHTML = `<div class="error">❌ Failed to load details: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import categorySyncer from './syncers/category-syncer.js';
|
||||
import categorySyncer from './syncers/categories-syncer.js';
|
||||
import pictureSyncer from './syncers/picture-syncer.js';
|
||||
import categoryProductsSyncer from './syncers/category-products-syncer.js';
|
||||
import { startServer } from './server/server.js';
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
|
||||
export function registerProducts(app, cache, updateProductCache) {
|
||||
app.get('/api/categories/:id/products', async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
|
||||
// Lazy load if not in cache
|
||||
if (!cache.products.has(id)) {
|
||||
await updateProductCache(id);
|
||||
@@ -22,11 +26,43 @@ export function registerProducts(app, cache, updateProductCache) {
|
||||
// Set cache headers with ETag
|
||||
res.set('Cache-Control', 'public, max-age=60, must-revalidate');
|
||||
res.set('ETag', cached.etag);
|
||||
|
||||
|
||||
res.json(JSON.parse(cached.data));
|
||||
} catch (err) {
|
||||
console.error(`Error serving products for category ${req.params.id}:`, err);
|
||||
res.status(500).json({ error: 'Failed to load products' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/products/:id/details', async (req, res) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const filePath = path.join(process.cwd(), 'cache', 'details', `${id}.json`);
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(filePath, 'utf8');
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
// Generate ETag from rowversion if available, otherwise use content hash
|
||||
// The user example showed "bRowversion": "0x0000000000470394"
|
||||
const etag = parsed.bRowversion ? `"${parsed.bRowversion}"` : crypto.createHash('md5').update(data).digest('hex');
|
||||
|
||||
if (req.headers['if-none-match'] === etag) {
|
||||
return res.status(304).end();
|
||||
}
|
||||
|
||||
res.set('Cache-Control', 'public, max-age=60, must-revalidate');
|
||||
res.set('ETag', etag);
|
||||
res.json(parsed);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return res.status(404).json({ error: 'Product details not found' });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error serving details for product ${req.params.id}:`, err);
|
||||
res.status(500).json({ error: 'Failed to load product details' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { EventEmitter } from 'events';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { SyncQueueManager } from '../utils/sync-queue-manager.js';
|
||||
import { CategoryDataFetcher } from '../services/category-data-fetcher.js';
|
||||
import { CategoryDataFetcher } from '../services/categories-data-fetcher.js';
|
||||
import { buildTree, pruneTree, buildTranslationTemplate, formatTranslationTemplate } from '../utils/category-tree-utils.js';
|
||||
import { readTextFile } from '../utils/file-sync-utils.js';
|
||||
|
||||
@@ -2,7 +2,7 @@ import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { EventEmitter } from 'events';
|
||||
import { SyncQueueManager } from '../utils/sync-queue-manager.js';
|
||||
import { ProductDataFetcher } from '../services/product-data-fetcher.js';
|
||||
import { ProductDataFetcher } from '../services/category-products-data-fetcher.js';
|
||||
import { getExistingIds, deleteObsoleteFiles, writeJsonIfChanged, ensureDir } from '../utils/file-sync-utils.js';
|
||||
import pictureSyncer from './picture-syncer.js';
|
||||
import productDetailSyncer from './product-detail-syncer.js';
|
||||
|
||||
Reference in New Issue
Block a user