refactor: Restructure category and product data fetching/syncing and add a product detail pop-up UI.

This commit is contained in:
sebseb7
2025-11-24 15:08:42 +01:00
parent b4d202bb23
commit d251daa075
7 changed files with 185 additions and 5 deletions

View File

@@ -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>

View File

@@ -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';

View File

@@ -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' });
}
});
}

View File

@@ -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';

View File

@@ -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';