diff --git a/index.html b/index.html index 50057ea..2c823ad 100644 --- a/index.html +++ b/index.html @@ -127,6 +127,17 @@ .product-item { padding: 0.25rem 0; border-bottom: 1px solid #eee; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .product-image { + width: 32px; + height: 32px; + object-fit: cover; + border-radius: 4px; + background: #f0f0f0; } .product-item:last-child { @@ -261,7 +272,21 @@ products.slice(0, 3).forEach(p => { const li = document.createElement('li'); li.className = 'product-item'; - li.textContent = `📦 ${p.cName}`; + + // Image + if (p.images && p.images.length > 0) { + const img = document.createElement('img'); + img.className = 'product-image'; + img.src = `/img/prod/${p.images[0]}.avif`; + img.alt = p.cName; + img.onerror = () => img.style.display = 'none'; + li.appendChild(img); + } + + const span = document.createElement('span'); + span.textContent = p.cName; + li.appendChild(span); + ul.appendChild(li); }); diff --git a/src/server/routes/images.js b/src/server/routes/images.js index 873a69a..4d3c4f7 100644 --- a/src/server/routes/images.js +++ b/src/server/routes/images.js @@ -4,13 +4,36 @@ export function registerImages(app, cacheDir) { app.get('/img/cat/:id.avif', (req, res) => { const { id } = req.params; const imagePath = path.join(cacheDir, 'img', 'categories', `${id}.avif`); + const resolvedPath = path.resolve(imagePath); - // Cache images for 1 year (immutable content) - res.set('Cache-Control', 'public, max-age=31536000, immutable'); - - res.sendFile(path.resolve(imagePath), (err) => { + res.sendFile(resolvedPath, { + headers: { + 'Cache-Control': 'public, max-age=31536000, immutable' + } + }, (err) => { if (err) { - res.status(404).send('Image not found'); + if (!res.headersSent) { + res.status(404).send('Image not found'); + } + } + }); + }); + + // Product images + app.get('/img/prod/:id.avif', (req, res) => { + const { id } = req.params; + const imagePath = path.join(cacheDir, 'img', 'products', `${id}.avif`); + const resolvedPath = path.resolve(imagePath); + res.sendFile(resolvedPath, { + headers: { + 'Cache-Control': 'public, max-age=31536000, immutable' + } + }, (err) => { + if (err) { + console.error(`❌ Error serving image ${resolvedPath}:`, err); + if (!res.headersSent) { + res.status(404).send('Image not found'); + } } }); }); diff --git a/src/syncers/category-products-syncer.js b/src/syncers/category-products-syncer.js index 1c65bb2..4bf192e 100644 --- a/src/syncers/category-products-syncer.js +++ b/src/syncers/category-products-syncer.js @@ -2,6 +2,7 @@ import fs from 'fs/promises'; import path from 'path'; import { EventEmitter } from 'events'; import { createConnection } from '../utils/database.js'; +import pictureSyncer from './picture-syncer.js'; class CategoryProductsSyncer extends EventEmitter { constructor() { @@ -90,6 +91,8 @@ class CategoryProductsSyncer extends EventEmitter { async _fetchAndWriteProducts(ids, dir) { let pool; + const globalImageIds = new Set(); + try { pool = await createConnection(); @@ -116,6 +119,31 @@ class CategoryProductsSyncer extends EventEmitter { ORDER BY a.bRowversion DESC, ab.bRowversion DESC `); + // Collect all kArtikel IDs to fetch images + const artikelIds = new Set(); + result.recordset.forEach(r => artikelIds.add(r.kArtikel)); + + // Fetch images for these articles + let productImages = new Map(); // kArtikel -> kBild[] + if (artikelIds.size > 0) { + const artikelList = Array.from(artikelIds).join(','); + const imagesResult = await pool.request().query(` + SELECT kArtikel, kBild + FROM tArtikelbildPlattform + WHERE kShop = ${process.env.JTL_SHOP_ID} + AND kPlattform = ${process.env.JTL_PLATTFORM_ID} + AND kArtikel IN (${artikelList}) + ORDER BY nNr ASC + `); + + imagesResult.recordset.forEach(r => { + if (!productImages.has(r.kArtikel)) { + productImages.set(r.kArtikel, []); + } + productImages.get(r.kArtikel).push(r.kBild); + }); + } + // Group results by kKategorie const productsByCategory = {}; @@ -126,9 +154,13 @@ class CategoryProductsSyncer extends EventEmitter { for (const record of result.recordset) { if (productsByCategory[record.kKategorie]) { + const images = productImages.get(record.kArtikel) || []; + images.forEach(imgId => globalImageIds.add(imgId)); + productsByCategory[record.kKategorie].push({ kArtikel: record.kArtikel, - cName: record.cName + cName: record.cName, + images: images }); } } @@ -160,6 +192,13 @@ class CategoryProductsSyncer extends EventEmitter { console.log(`⏳ Processed products for ${processed}/${ids.length} categories...`); } } + + // Sync all collected images at once + if (globalImageIds.size > 0) { + console.log(`🖼️ Syncing ${globalImageIds.size} product images...`); + await pictureSyncer.syncImages(Array.from(globalImageIds), 'products'); + } + } catch (err) { console.error('❌ Error fetching products:', err); } finally {