# Design Document: Blacklist Feature ## Overview Die Blacklist-Funktion erweitert die Amazon Product Bar Extension um die Möglichkeit, Markennamen zu verwalten und Produkte dieser Marken visuell in der Product_Bar zu kennzeichnen. Die Funktion nutzt einen neuen Menüpunkt "Blacklist" im StaggeredMenu, speichert Daten im Local Storage und zeigt Marken-Logos bei geblacklisteten Produkten an. ## Architecture ```mermaid graph TD A[StaggeredMenu] --> B[Blacklist Menu Item] B --> C[Blacklist Panel Manager] C --> D[Blacklist Storage Manager] D --> E[Local Storage] C --> F[Brand Input UI] C --> G[Brand List UI] H[Product Card Detector] --> I[Brand Extractor] I --> J[Blacklist Matcher] J --> D J --> K[Brand Icon Manager] K --> L[Product Bar] M[Brand Logo Registry] --> K ``` Die Blacklist-Funktion besteht aus: 1. **Blacklist Panel Manager** - UI-Verwaltung für das Blacklist-Panel 2. **Blacklist Storage Manager** - CRUD-Operationen für geblacklistete Marken 3. **Brand Extractor** - Extraktion von Markennamen aus Produktkarten 4. **Blacklist Matcher** - Case-insensitive Vergleich von Marken 5. **Brand Icon Manager** - Verwaltung der Marken-Icons in Product_Bars 6. **Brand Logo Registry** - Vordefinierte Logos für bekannte Marken ## Components and Interfaces ### 1. Blacklist Storage Manager ```javascript // BlacklistStorageManager.js class BlacklistStorageManager { constructor() { this.STORAGE_KEY = 'amazon_ext_blacklist'; } // Speichert eine Marke in der Blacklist async addBrand(brandName) { const brands = await this.getBrands(); const normalizedName = brandName.trim(); // Case-insensitive Duplikat-Check const exists = brands.some(b => b.name.toLowerCase() === normalizedName.toLowerCase() ); if (exists) { throw new Error('Brand already exists'); } brands.push({ id: this.generateId(), name: normalizedName, addedAt: new Date().toISOString() }); await this.saveBrands(brands); return brands; } // Holt alle geblacklisteten Marken async getBrands() { const data = localStorage.getItem(this.STORAGE_KEY); return data ? JSON.parse(data) : []; } // Löscht eine Marke aus der Blacklist async deleteBrand(brandId) { const brands = await this.getBrands(); const filtered = brands.filter(b => b.id !== brandId); await this.saveBrands(filtered); return filtered; } // Prüft ob eine Marke geblacklistet ist (case-insensitive) async isBrandBlacklisted(brandName) { const brands = await this.getBrands(); return brands.some(b => b.name.toLowerCase() === brandName.toLowerCase() ); } // Speichert Marken im Local Storage async saveBrands(brands) { localStorage.setItem(this.STORAGE_KEY, JSON.stringify(brands)); // Event für UI-Updates emittieren if (window.amazonExtEventBus) { window.amazonExtEventBus.emit('blacklist:updated', brands); } } generateId() { return 'bl_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } } ``` ### 2. Brand Extractor ```javascript // BrandExtractor.js class BrandExtractor { // Extrahiert Markennamen aus einer Produktkarte extractBrand(productCard) { // Methode 1: "by [Brand]" Text const byBrandElement = productCard.querySelector('.a-row.a-size-base.a-color-secondary'); if (byBrandElement) { const byMatch = byBrandElement.textContent.match(/by\s+([^,\n]+)/i); if (byMatch) { return byMatch[1].trim(); } } // Methode 2: Brand-Link const brandLink = productCard.querySelector('a[href*="/stores/"], .a-link-normal[href*="brand="]'); if (brandLink) { return brandLink.textContent.trim(); } // Methode 3: Aus Produkttitel extrahieren (erstes Wort oft die Marke) const titleElement = productCard.querySelector('h2 a span, .a-text-normal'); if (titleElement) { const title = titleElement.textContent.trim(); const firstWord = title.split(/\s+/)[0]; // Nur wenn es wie ein Markenname aussieht (Großbuchstabe am Anfang) if (firstWord && /^[A-Z]/.test(firstWord)) { return firstWord; } } return null; } } ``` ### 3. Brand Logo Registry ```javascript // BrandLogoRegistry.js class BrandLogoRegistry { constructor() { // Vordefinierte SVG-Logos für bekannte Marken this.logos = { 'nike': this.createNikeLogo(), 'adidas': this.createAdidasLogo(), 'puma': this.createPumaLogo(), 'apple': this.createAppleLogo(), 'samsung': this.createSamsungLogo() }; this.defaultBlockedIcon = this.createBlockedIcon(); } // Holt Logo für eine Marke (case-insensitive) getLogo(brandName) { const normalized = brandName.toLowerCase(); return this.logos[normalized] || this.defaultBlockedIcon; } // Prüft ob ein spezifisches Logo existiert hasLogo(brandName) { return brandName.toLowerCase() in this.logos; } createNikeLogo() { return ` `; } createAdidasLogo() { return ` `; } createPumaLogo() { return ` `; } createAppleLogo() { return ` `; } createSamsungLogo() { return ` `; } createBlockedIcon() { return ` `; } } ``` ### 4. Brand Icon Manager ```javascript // BrandIconManager.js class BrandIconManager { constructor(blacklistStorage, brandExtractor, logoRegistry) { this.blacklistStorage = blacklistStorage; this.brandExtractor = brandExtractor; this.logoRegistry = logoRegistry; } // Aktualisiert alle Product_Bars auf der Seite async updateAllBars() { const productBars = document.querySelectorAll('.amazon-ext-product-bar'); const brands = await this.blacklistStorage.getBrands(); const blacklistedNames = brands.map(b => b.name.toLowerCase()); productBars.forEach(bar => { const productCard = bar.closest('[data-asin]'); if (!productCard) return; const brand = this.brandExtractor.extractBrand(productCard); if (brand && blacklistedNames.includes(brand.toLowerCase())) { this.addBrandIcon(bar, brand); } else { this.removeBrandIcon(bar); } }); } // Fügt Brand-Icon zu einer Product_Bar hinzu addBrandIcon(productBar, brandName) { let iconContainer = productBar.querySelector('.brand-icon'); if (!iconContainer) { iconContainer = document.createElement('div'); iconContainer.className = 'brand-icon'; productBar.insertBefore(iconContainer, productBar.firstChild); } const logo = this.logoRegistry.getLogo(brandName); iconContainer.innerHTML = logo; iconContainer.title = `Blacklisted: ${brandName}`; iconContainer.style.display = 'flex'; } // Entfernt Brand-Icon von einer Product_Bar removeBrandIcon(productBar) { const iconContainer = productBar.querySelector('.brand-icon'); if (iconContainer) { iconContainer.style.display = 'none'; } } // Fügt Icon zu allen Produkten einer bestimmten Marke hinzu async addIconForBrand(brandName) { const productBars = document.querySelectorAll('.amazon-ext-product-bar'); productBars.forEach(bar => { const productCard = bar.closest('[data-asin]'); if (!productCard) return; const brand = this.brandExtractor.extractBrand(productCard); if (brand && brand.toLowerCase() === brandName.toLowerCase()) { this.addBrandIcon(bar, brand); } }); } // Entfernt Icon von allen Produkten einer bestimmten Marke async removeIconForBrand(brandName) { const productBars = document.querySelectorAll('.amazon-ext-product-bar'); productBars.forEach(bar => { const productCard = bar.closest('[data-asin]'); if (!productCard) return; const brand = this.brandExtractor.extractBrand(productCard); if (brand && brand.toLowerCase() === brandName.toLowerCase()) { this.removeBrandIcon(bar); } }); } } ``` ### 5. Blacklist Panel Manager ```javascript // BlacklistPanelManager.js class BlacklistPanelManager { constructor(blacklistStorage, logoRegistry) { this.blacklistStorage = blacklistStorage; this.logoRegistry = logoRegistry; this.container = null; } createBlacklistContent() { const container = document.createElement('div'); container.className = 'amazon-ext-blacklist-content'; container.innerHTML = `

Blacklist

Markennamen hinzufügen, um Produkte zu markieren

`; this.container = container; this.setupEventListeners(); this.loadBrands(); return container; } setupEventListeners() { const input = this.container.querySelector('.brand-input'); const addBtn = this.container.querySelector('.add-brand-btn'); addBtn.addEventListener('click', () => this.handleAddBrand()); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.handleAddBrand(); }); } async handleAddBrand() { const input = this.container.querySelector('.brand-input'); const brandName = input.value.trim(); if (!brandName) { this.showMessage('Bitte einen Markennamen eingeben', 'error'); return; } try { await this.blacklistStorage.addBrand(brandName); input.value = ''; this.showMessage(`"${brandName}" zur Blacklist hinzugefügt`, 'success'); this.loadBrands(); } catch (error) { if (error.message === 'Brand already exists') { this.showMessage('Diese Marke ist bereits in der Blacklist', 'error'); } else { this.showMessage('Fehler beim Speichern', 'error'); } } } async loadBrands() { const brands = await this.blacklistStorage.getBrands(); const listContainer = this.container.querySelector('.brand-list'); if (brands.length === 0) { listContainer.innerHTML = '

Keine Marken in der Blacklist

'; return; } listContainer.innerHTML = brands.map(brand => `
${brand.name}
`).join(''); // Delete-Button Event Listeners listContainer.querySelectorAll('.delete-brand-btn').forEach(btn => { btn.addEventListener('click', (e) => { const brandId = e.target.dataset.id; this.handleDeleteBrand(brandId); }); }); } async handleDeleteBrand(brandId) { const brands = await this.blacklistStorage.getBrands(); const brand = brands.find(b => b.id === brandId); await this.blacklistStorage.deleteBrand(brandId); this.showMessage(`"${brand?.name}" entfernt`, 'success'); this.loadBrands(); } showMessage(text, type) { const messageEl = this.container.querySelector('.blacklist-message'); messageEl.textContent = text; messageEl.className = `blacklist-message ${type}`; messageEl.style.display = 'block'; setTimeout(() => { messageEl.style.display = 'none'; }, 3000); } showBlacklistPanel() { this.loadBrands(); } hideBlacklistPanel() { // Cleanup wenn nötig } } ``` ## Data Models ### BlacklistedBrand ```typescript interface BlacklistedBrand { id: string; // Eindeutige ID (bl_timestamp_random) name: string; // Markenname (originale Schreibweise) addedAt: string; // ISO-Timestamp der Hinzufügung } ``` ### Local Storage Structure ```json { "amazon_ext_blacklist": [ { "id": "bl_1699123456789_abc123def", "name": "Nike", "addedAt": "2024-01-15T10:30:00.000Z" }, { "id": "bl_1699123456790_xyz789ghi", "name": "Adidas", "addedAt": "2024-01-15T10:31:00.000Z" } ] } ``` ### CSS Styling ```css /* Blacklist Panel Styles */ .amazon-ext-blacklist-content { color: white; padding: 2rem; height: 100%; overflow-y: auto; } .blacklist-header h2 { margin: 0 0 0.5rem 0; font-size: 2rem; font-weight: 700; } .blacklist-description { color: #888; margin: 0 0 1.5rem 0; } .add-brand-form { display: flex; gap: 1rem; margin-bottom: 2rem; } .add-brand-form .brand-input { flex: 1; padding: 0.75rem; border: 1px solid #333; background: #222; color: white; border-radius: 4px; font-size: 1rem; } .add-brand-form .add-brand-btn { padding: 0.75rem 1.5rem; background: #ff9900; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; } .add-brand-form .add-brand-btn:hover { background: #e68a00; } .brand-list { display: flex; flex-direction: column; gap: 0.75rem; } .brand-item { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1rem; background: #222; border-radius: 4px; border: 1px solid #333; } .brand-logo { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; color: #ff9900; } .brand-name { flex: 1; font-size: 1rem; } .delete-brand-btn { background: none; border: none; color: #888; font-size: 1.5rem; cursor: pointer; padding: 0 0.5rem; line-height: 1; } .delete-brand-btn:hover { color: #ff4444; } .empty-message { color: #666; text-align: center; padding: 2rem; } .blacklist-message { padding: 0.75rem 1rem; border-radius: 4px; margin-top: 1rem; text-align: center; } .blacklist-message.success { background: #1a4d1a; color: #4ade4a; } .blacklist-message.error { background: #4d1a1a; color: #ff6b6b; } /* Brand Icon in Product Bar */ .amazon-ext-product-bar .brand-icon { position: absolute; left: 5px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; display: flex; align-items: center; justify-content: center; color: #ff4444; } ``` ## Correctness Properties *A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.* ### Property 1: Brand Saving Round-Trip *For any* valid brand name, saving it to the blacklist and then retrieving all brands should include that brand with the same name. **Validates: Requirements 2.2, 2.3** ### Property 2: Case-Insensitive Comparison *For any* two brand name strings that differ only in letter case (e.g., "Nike" vs "nike" vs "NIKE"), the `isBrandBlacklisted` function should return the same result for both. **Validates: Requirements 4.1, 4.2** ### Property 3: Duplicate Prevention *For any* brand name already in the blacklist, attempting to add a case-variant of that name should throw an error and not increase the blacklist size. **Validates: Requirements 2.5, 4.3** ### Property 4: Whitespace Trimming *For any* brand name with leading or trailing whitespace, the saved brand name should have no leading or trailing whitespace. **Validates: Requirements 2.6** ### Property 5: Original Case Preservation *For any* brand name saved to the blacklist, the retrieved brand name should preserve the exact original case as entered. **Validates: Requirements 4.4** ### Property 6: Brand List Rendering Completeness *For any* set of N saved brands, the rendered brand list should contain exactly N brand items. **Validates: Requirements 3.1** ### Property 7: Logo Selection Consistency *For any* brand name, if the brand has a predefined logo in the registry, `getLogo` should return that specific logo; otherwise, it should return the default blocked icon. **Validates: Requirements 3.2, 6.3, 7.3, 7.4** ### Property 8: Delete Button Presence *For any* rendered brand item in the blacklist panel, it should contain exactly one delete button element. **Validates: Requirements 3.3** ### Property 9: Deletion Completeness *For any* brand deleted from the blacklist, it should no longer appear in storage or in the UI after deletion. **Validates: Requirements 3.4** ### Property 10: Brand Extraction Determinism *For any* product card DOM element with brand information, the `extractBrand` function should return a non-null string representing the brand. **Validates: Requirements 5.1, 5.2, 5.3** ### Property 11: No Marking Without Brand *For any* product card where brand extraction returns null, no blacklist icon should be added to the product bar. **Validates: Requirements 5.4** ### Property 12: Blacklist Icon Display *For any* product whose extracted brand matches a blacklisted brand (case-insensitive), the product bar should display a brand icon. **Validates: Requirements 6.1** ### Property 13: Real-Time Icon Updates *For any* brand added to or removed from the blacklist, all visible product bars with matching brands should immediately reflect the change (icon added or removed). **Validates: Requirements 6.4, 6.5** ## Error Handling | Scenario | Handling | |----------|----------| | Empty brand name entered | Display error message, prevent saving | | Brand already exists | Display "already exists" message, prevent duplicate | | Local storage quota exceeded | Display warning, suggest cleanup | | Brand extraction fails | Skip blacklist marking for that product | | Invalid DOM structure | Graceful degradation, log warning | | Logo not found for brand | Use default blocked icon | ## Testing Strategy ### Unit Tests - BlacklistStorageManager CRUD operations - Case-insensitive comparison logic - Whitespace trimming - BrandExtractor with various DOM structures - BrandLogoRegistry logo retrieval - BlacklistPanelManager UI rendering ### Property-Based Tests - **Property 1**: Generate random brand names, save and retrieve - **Property 2**: Generate brand name pairs differing only in case - **Property 3**: Generate brands, add twice with case variants - **Property 4**: Generate brand names with various whitespace patterns - **Property 5**: Generate brand names with mixed case, verify preservation - **Property 6**: Generate sets of brands, verify list count - **Property 7**: Test known brands and unknown brands for logo selection - **Property 8**: Render brand items, verify delete button presence - **Property 9**: Add and delete brands, verify complete removal - **Property 10**: Generate product card DOMs with brand info - **Property 11**: Generate product cards without brand info - **Property 12**: Generate products with blacklisted brands - **Property 13**: Add/remove brands, verify icon updates ### Integration Tests - End-to-end: Add brand → see icon on matching products → delete brand → icon removed - Menu navigation: Open menu → click Blacklist → verify panel content - Persistence: Add brands → reload page → verify brands persist ### Testing Framework - Jest für Unit Tests - fast-check für Property-Based Tests - JSDOM für DOM-Simulation ### Test Configuration - Minimum 100 Iterationen pro Property Test - Tag-Format: **Feature: blacklist-feature, Property {number}: {property_text}** - Jede Correctness Property wird durch einen einzelnen Property-Based Test implementiert