- Add .gitignore to exclude node_modules, dist, logs, and system files - Add comprehensive project documentation including README, deployment guide, and development setup - Add .kiro project specifications for amazon-product-bar-extension, appwrite-cloud-storage, appwrite-userid-repair, blacklist-feature, and enhanced-item-management - Add .kiro steering documents for product, structure, styling, and tech guidelines - Add VSCode settings configuration for consistent development environment - Add manifest.json and babel/vite configuration for extension build setup - Add complete source code implementation including AppWrite integration, storage managers, UI components, and services - Add comprehensive test suite with Jest configuration and 30+ test files covering all major modules - Add test HTML files for integration testing and validation - Add coverage reports and build validation scripts - Add AppWrite setup and repair documentation for database schema management - Add migration guides and responsive accessibility implementation documentation - Establish foundation for Amazon product bar extension with full feature set including blacklist management, enhanced item workflows, and real-time synchronization
321 lines
8.7 KiB
JavaScript
321 lines
8.7 KiB
JavaScript
import { BlacklistStorageManager } from './BlacklistStorageManager.js';
|
||
import BrandLogoRegistry from './BrandLogoRegistry.js';
|
||
|
||
/**
|
||
* BlacklistPanelManager - Manages the Blacklist Panel UI and functionality
|
||
*
|
||
* Provides UI for adding/removing brands from the blacklist and displays
|
||
* the current list of blacklisted brands with their logos.
|
||
*
|
||
* Requirements: 2.1, 2.4, 3.1, 3.2, 3.3, 3.4
|
||
*/
|
||
export class BlacklistPanelManager {
|
||
constructor(blacklistStorage = null, logoRegistry = null) {
|
||
this.blacklistStorage = blacklistStorage || new BlacklistStorageManager();
|
||
this.logoRegistry = logoRegistry || new BrandLogoRegistry();
|
||
this.container = null;
|
||
this.eventBus = null;
|
||
|
||
// Initialize event bus connection
|
||
this.initializeEventBus();
|
||
}
|
||
|
||
/**
|
||
* Initializes connection to the global event bus
|
||
*/
|
||
initializeEventBus() {
|
||
if (typeof window !== 'undefined' && window.amazonExtEventBus) {
|
||
this.eventBus = window.amazonExtEventBus;
|
||
this.setupEventListeners();
|
||
} else {
|
||
setTimeout(() => {
|
||
if (typeof window !== 'undefined' && window.amazonExtEventBus) {
|
||
this.eventBus = window.amazonExtEventBus;
|
||
this.setupEventListeners();
|
||
}
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sets up event listeners for blacklist updates
|
||
*/
|
||
setupEventListeners() {
|
||
if (!this.eventBus) return;
|
||
|
||
this.eventBus.on('blacklist:updated', () => {
|
||
this.loadBrands();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Emits an event through the event bus
|
||
* @param {string} eventName - Event name
|
||
* @param {*} data - Event data
|
||
*/
|
||
emitEvent(eventName, data) {
|
||
if (this.eventBus) {
|
||
this.eventBus.emit(eventName, data);
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Creates the Blacklist Panel content HTML structure
|
||
* Requirement 2.1: Display input field for brand names
|
||
* @returns {HTMLElement}
|
||
*/
|
||
createBlacklistContent() {
|
||
const container = document.createElement('div');
|
||
container.className = 'amazon-ext-blacklist-content';
|
||
|
||
container.innerHTML = `
|
||
<div class="blacklist-header">
|
||
<h2>Blacklist</h2>
|
||
<p class="blacklist-description">Markennamen hinzufügen, um Produkte zu markieren</p>
|
||
</div>
|
||
|
||
<div class="add-brand-form">
|
||
<input
|
||
type="text"
|
||
class="brand-input"
|
||
placeholder="Markenname eingeben (z.B. Nike, Adidas)..."
|
||
aria-label="Brand name input"
|
||
/>
|
||
<button class="add-brand-btn" type="button">Hinzufügen</button>
|
||
</div>
|
||
|
||
<div class="blacklist-message" style="display: none;"></div>
|
||
|
||
<div class="brand-list-container">
|
||
<div class="brand-list"></div>
|
||
</div>
|
||
`;
|
||
|
||
this.container = container;
|
||
this.attachEventListeners();
|
||
this.loadBrands();
|
||
|
||
return container;
|
||
}
|
||
|
||
/**
|
||
* Attaches event listeners to the Blacklist Panel elements
|
||
*/
|
||
attachEventListeners() {
|
||
if (!this.container) return;
|
||
|
||
const input = this.container.querySelector('.brand-input');
|
||
const addBtn = this.container.querySelector('.add-brand-btn');
|
||
|
||
if (addBtn) {
|
||
addBtn.addEventListener('click', () => this.handleAddBrand());
|
||
}
|
||
|
||
if (input) {
|
||
input.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
this.handleAddBrand();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Handles adding a new brand to the blacklist
|
||
* Requirements: 2.2, 2.4, 2.5, 2.6
|
||
*/
|
||
async handleAddBrand() {
|
||
if (!this.container) return;
|
||
|
||
const input = this.container.querySelector('.brand-input');
|
||
const brandName = input ? input.value.trim() : '';
|
||
|
||
// Clear previous messages
|
||
this.clearMessage();
|
||
|
||
if (!brandName) {
|
||
this.showMessage('Bitte einen Markennamen eingeben', 'error');
|
||
if (input) input.focus();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await this.blacklistStorage.addBrand(brandName);
|
||
|
||
// Requirement 2.4: Clear input field after saving
|
||
if (input) {
|
||
input.value = '';
|
||
}
|
||
|
||
this.showMessage(`"${brandName}" zur Blacklist hinzugefügt`, 'success');
|
||
this.loadBrands();
|
||
|
||
} catch (error) {
|
||
// Requirement 2.5: Display message for duplicate entry
|
||
if (error.message === 'Brand already exists') {
|
||
this.showMessage('Diese Marke ist bereits in der Blacklist', 'error');
|
||
} else if (error.message.includes('quota')) {
|
||
this.showMessage('Speicher voll. Bitte löschen Sie einige Marken.', 'error');
|
||
} else {
|
||
this.showMessage('Fehler beim Speichern', 'error');
|
||
}
|
||
|
||
if (input) input.focus();
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Loads and renders all blacklisted brands
|
||
* Requirement 3.1: Display all saved brand names in a list
|
||
*/
|
||
async loadBrands() {
|
||
if (!this.container) return;
|
||
|
||
try {
|
||
const brands = await this.blacklistStorage.getBrands();
|
||
this.renderBrandList(brands);
|
||
} catch (error) {
|
||
console.error('Error loading brands:', error);
|
||
this.showMessage('Fehler beim Laden der Marken', 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Renders the brand list in the Blacklist Panel
|
||
* Requirements: 3.1, 3.2, 3.3
|
||
* @param {Array} brands - Array of blacklisted brands
|
||
*/
|
||
renderBrandList(brands) {
|
||
if (!this.container) return;
|
||
|
||
const listContainer = this.container.querySelector('.brand-list');
|
||
if (!listContainer) return;
|
||
|
||
if (!brands || brands.length === 0) {
|
||
listContainer.innerHTML = '<p class="empty-message">Keine Marken in der Blacklist</p>';
|
||
return;
|
||
}
|
||
|
||
// Requirement 3.1: Display all saved brand names
|
||
// Requirement 3.2: Display each brand with its logo
|
||
// Requirement 3.3: Provide delete button for each brand
|
||
listContainer.innerHTML = brands.map(brand => `
|
||
<div class="brand-item" data-id="${brand.id}">
|
||
<div class="brand-logo">
|
||
${this.logoRegistry.getLogo(brand.name)}
|
||
</div>
|
||
<span class="brand-name">${this.escapeHtml(brand.name)}</span>
|
||
<button class="delete-brand-btn" data-id="${brand.id}" aria-label="Marke löschen">×</button>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Attach delete button event listeners
|
||
this.attachDeleteListeners(listContainer);
|
||
}
|
||
|
||
/**
|
||
* Attaches event listeners to delete buttons
|
||
* @param {HTMLElement} listContainer - Brand list container element
|
||
*/
|
||
attachDeleteListeners(listContainer) {
|
||
const deleteButtons = listContainer.querySelectorAll('.delete-brand-btn');
|
||
deleteButtons.forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const brandId = e.target.dataset.id;
|
||
if (brandId) {
|
||
this.handleDeleteBrand(brandId);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Handles deleting a brand from the blacklist
|
||
* Requirement 3.4: Remove brand from storage and update display
|
||
* @param {string} brandId - Brand ID to delete
|
||
*/
|
||
async handleDeleteBrand(brandId) {
|
||
try {
|
||
const brands = await this.blacklistStorage.getBrands();
|
||
const brand = brands.find(b => b.id === brandId);
|
||
const brandName = brand ? brand.name : '';
|
||
|
||
await this.blacklistStorage.deleteBrand(brandId);
|
||
|
||
if (brandName) {
|
||
this.showMessage(`"${brandName}" entfernt`, 'success');
|
||
}
|
||
|
||
this.loadBrands();
|
||
|
||
} catch (error) {
|
||
console.error('Error deleting brand:', error);
|
||
this.showMessage('Fehler beim Löschen der Marke', 'error');
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Shows a feedback message (success or error)
|
||
* @param {string} text - Message text
|
||
* @param {string} type - Message type ('success' or 'error')
|
||
*/
|
||
showMessage(text, type) {
|
||
if (!this.container) return;
|
||
|
||
const messageEl = this.container.querySelector('.blacklist-message');
|
||
if (!messageEl) return;
|
||
|
||
messageEl.textContent = text;
|
||
messageEl.className = `blacklist-message ${type}`;
|
||
messageEl.style.display = 'block';
|
||
|
||
// Auto-hide after 3 seconds
|
||
setTimeout(() => {
|
||
if (messageEl) {
|
||
messageEl.style.display = 'none';
|
||
}
|
||
}, 3000);
|
||
}
|
||
|
||
/**
|
||
* Clears the feedback message
|
||
*/
|
||
clearMessage() {
|
||
if (!this.container) return;
|
||
|
||
const messageEl = this.container.querySelector('.blacklist-message');
|
||
if (messageEl) {
|
||
messageEl.style.display = 'none';
|
||
messageEl.textContent = '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Shows the Blacklist Panel (called when menu item is clicked)
|
||
*/
|
||
showBlacklistPanel() {
|
||
this.loadBrands();
|
||
}
|
||
|
||
/**
|
||
* Hides the Blacklist Panel (called when menu is closed)
|
||
*/
|
||
hideBlacklistPanel() {
|
||
// Cleanup if needed
|
||
}
|
||
|
||
/**
|
||
* Escapes HTML special characters to prevent XSS
|
||
* @param {string} text - Text to escape
|
||
* @returns {string} Escaped text
|
||
*/
|
||
escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
}
|