diff --git a/index.html b/index.html index c40b57f..c3905c1 100644 --- a/index.html +++ b/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; + } @@ -426,6 +519,14 @@ + +
+
+ +
+
+ + diff --git a/src/index.js b/src/index.js index aa5e4b2..75043b3 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; diff --git a/src/server/routes/products.js b/src/server/routes/products.js index feb1d40..d7914d0 100644 --- a/src/server/routes/products.js +++ b/src/server/routes/products.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' }); + } + }); } diff --git a/src/services/category-data-fetcher.js b/src/services/categories-data-fetcher.js similarity index 100% rename from src/services/category-data-fetcher.js rename to src/services/categories-data-fetcher.js diff --git a/src/services/product-data-fetcher.js b/src/services/category-products-data-fetcher.js similarity index 100% rename from src/services/product-data-fetcher.js rename to src/services/category-products-data-fetcher.js diff --git a/src/syncers/category-syncer.js b/src/syncers/categories-syncer.js similarity index 98% rename from src/syncers/category-syncer.js rename to src/syncers/categories-syncer.js index af7daa8..62403b8 100644 --- a/src/syncers/category-syncer.js +++ b/src/syncers/categories-syncer.js @@ -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'; diff --git a/src/syncers/category-products-syncer.js b/src/syncers/category-products-syncer.js index 3b7567e..8a648eb 100644 --- a/src/syncers/category-products-syncer.js +++ b/src/syncers/category-products-syncer.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';