# 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 = `
`;
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 => `
${this.logoRegistry.getLogo(brand.name)}
${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