Sfoglia il codice sorgente

fix: modal null safety and expand content detection keywords

- Fixed modal error: Cannot read properties of undefined (reading 'substring')
- Added null checks for content property in modal rendering and copy functionality
- Fixed Copy button showing plain JavaScript text instead of content
- Significantly expanded keyword lists in SitemapParser for better WooCommerce content detection:
  - Added comprehensive English and Hungarian shipping terms
  - Expanded contact/about page detection keywords
  - Enhanced terms & conditions/privacy policy keywords
  - Improved FAQ/help/support page detection
- Better support for Hungarian e-commerce sites like woo.aicaller.hu

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 8 mesi fa
parent
commit
4886ff439c

BIN
data/shops.db


+ 89 - 4
src/scraper/SitemapParser.ts

@@ -98,10 +98,95 @@ export class SitemapParser {
     terms: SitemapUrl[];
     faq: SitemapUrl[];
   } {
-    const shippingKeywords = ['shipping', 'delivery', 'szallitas', 'kiszallitas'];
-    const contactKeywords = ['contact', 'kapcsolat', 'elerheto'];
-    const termsKeywords = ['terms', 'conditions', 'aszf', 'feltetel', 'privacy', 'policy'];
-    const faqKeywords = ['faq', 'gyakori', 'kerdes', 'questions'];
+    const shippingKeywords = [
+      // English terms
+      'shipping', 'delivery', 'ship', 'deliver', 'postage', 'courier',
+      'logistics', 'transport', 'freight', 'dispatch', 'fulfillment',
+      'shipping-info', 'delivery-info', 'shipping-policy', 'delivery-policy',
+      'how-we-ship', 'shipping-rates', 'delivery-rates', 'shipping-cost',
+      'delivery-cost', 'shipping-method', 'delivery-method', 'shipping-options',
+      'delivery-options', 'express-delivery', 'overnight-shipping', 'fast-shipping',
+      'international-shipping', 'domestic-shipping', 'free-shipping', 'next-day',
+
+      // Hungarian terms
+      'szallitas', 'kiszallitas', 'szallitasi', 'kiszallitasi', 'fuvar', 'fuvarozas',
+      'szallitasi-informacio', 'szallitasi-feltetelek', 'szallitasi-mod', 'szallitasi-dij',
+      'szallitasi-dijak', 'szallitasi-modok', 'szallitasi-ido', 'szallitasi-hatarido',
+      'futarszolgalat', 'futarszallitas', 'hazhozszallitas', 'ingyenes-szallitas',
+      'expressz-szallitas', 'gyors-szallitas', 'nemzetkozi-szallitas', 'hazai-szallitas',
+      'postai-szallitas', 'csomagpont', 'csomagautomata', 'pickup-pont'
+    ];
+
+    const contactKeywords = [
+      // English terms
+      'contact', 'contact-us', 'contact-info', 'contact-page', 'get-in-touch',
+      'reach-us', 'find-us', 'locate-us', 'visit-us', 'where-to-find-us',
+      'about', 'about-us', 'about-company', 'who-we-are', 'our-story',
+      'company-info', 'business-info', 'team', 'our-team', 'meet-the-team',
+      'email', 'phone', 'telephone', 'address', 'location', 'directions',
+      'office', 'store-location', 'headquarters', 'branch', 'outlet',
+      'customer-service', 'customer-support', 'support', 'help-center',
+      'impressum', 'imprint', 'legal-notice', 'company-details',
+
+      // Hungarian terms
+      'kapcsolat', 'kapcsolatfelvetel', 'elerhetoseg', 'elerheto', 'kontakt',
+      'kapcsolatok', 'kapcsolat-felvetel', 'elerhetosegek', 'megkozelites',
+      'rolunk', 'magunkrol', 'cegunkrol', 'tortenelem', 'cegtortenet',
+      'bemutatkozas', 'csapat', 'csapatunk', 'munkatarsak', 'kollektiva',
+      'telefon', 'telefonszam', 'email', 'e-mail', 'cim', 'lakcim', 'szekhely',
+      'iroda', 'uzlet', 'bolt', 'telephelyunk', 'fiokunk', 'kepviselet',
+      'ugyfelszolgalat', 'vevoszolgalat', 'segitseg', 'tamogatas', 'support',
+      'impresszum', 'jogi-nyilatkozat', 'cegadatok', 'vallalkozasi-adatok'
+    ];
+
+    const termsKeywords = [
+      // English terms
+      'terms', 'conditions', 'terms-and-conditions', 'terms-of-service', 'terms-of-use',
+      'tos', 'toc', 'tc', 'user-agreement', 'user-terms', 'service-terms',
+      'privacy', 'privacy-policy', 'privacy-notice', 'privacy-statement',
+      'data-protection', 'data-policy', 'cookie-policy', 'cookies',
+      'legal', 'legal-notice', 'legal-info', 'legal-disclaimer', 'disclaimer',
+      'refund', 'refund-policy', 'return-policy', 'returns', 'exchange-policy',
+      'warranty', 'guarantee', 'liability', 'limitation-of-liability',
+      'copyright', 'intellectual-property', 'trademark', 'rights',
+      'gdpr', 'ccpa', 'compliance', 'regulations', 'rules',
+
+      // Hungarian terms
+      'aszf', 'altalanos-szerzodesi-feltetelek', 'feltetel', 'feltetelek',
+      'felhasznalasi-feltetelek', 'szolgaltatas-feltetel', 'hasznalati-feltetelek',
+      'vasarlasi-feltetelek', 'szerzodesi-feltetelek', 'szabalyzat',
+      'adatvedelm', 'adatkezeles', 'adatvedelmi-tajekoztato', 'adatkezelesi-tajekoztato',
+      'adatkezeles-szabalyzat', 'adatbiztonsag', 'cookie-szabalyzat', 'sutik',
+      'jogi-nyilatkozat', 'jogtud', 'jogi-informacio', 'jogi-tudnivalo',
+      'felelosseg', 'felelosseg-korlatozas', 'garancia', 'jotallas',
+      'visszaterites', 'visszavételar', 'cserepolitika', 'reklamacio',
+      'szerzoi-jog', 'vedjegy', 'jogok', 'impresszum', 'impressum',
+      'gdpr', 'adatvedelmi-rendelet', 'megfeleloseg', 'jogszabaly'
+    ];
+
+    const faqKeywords = [
+      // English terms
+      'faq', 'frequently-asked-questions', 'frequently-asked', 'f-a-q',
+      'questions', 'common-questions', 'popular-questions', 'most-asked',
+      'help', 'help-center', 'help-desk', 'helpdesk', 'help-section',
+      'support', 'customer-support', 'tech-support', 'user-support',
+      'knowledge', 'knowledge-base', 'knowledgebase', 'kb', 'wiki',
+      'guide', 'guides', 'tutorial', 'tutorials', 'how-to', 'manual',
+      'documentation', 'docs', 'user-guide', 'user-manual',
+      'troubleshooting', 'problem', 'issue', 'solution', 'answers',
+      'community', 'forum', 'discussion', 'q-and-a', 'q&a',
+
+      // Hungarian terms
+      'gyik', 'gyakori-kerdesek', 'gyakori', 'kerdesek', 'kerdes',
+      'gyakran-felmerulo', 'leggyakoribb-kerdesek', 'nepszeru-kerdesek',
+      'kerdezz-felelek', 'kerdes-valasz', 'q-a', 'qa',
+      'segitseg', 'segitsegkeres', 'segito-kozpont', 'tamogatas',
+      'ugyfelszolgalat', 'vevoszolgalat', 'technikai-tamogatas',
+      'tudnivalok', 'hasznos-informaciok', 'hasznos-tudnivalok', 'tudastAr',
+      'utmutato', 'utmutatók', 'kezikonyv', 'manual', 'dokumentacio',
+      'felhasznaloi-utmutato', 'hogyan', 'hibaelharitas', 'problema',
+      'megoldas', 'valaszok', 'kozosseg', 'forum', 'megbeszeles'
+    ];
 
     const containsKeyword = (url: string, keywords: string[]): boolean => {
       const lowerUrl = url.toLowerCase();

+ 57 - 23
web/src/components/pages/ContentPage.ts

@@ -15,6 +15,7 @@ export class ContentPage {
   private totalPages: number = 1;
   private totalItems: number = 0;
   private eventsbound: boolean = false;
+  private currentModalItem: ShopContent | null = null;
 
   constructor(
     private apiService: ApiService,
@@ -244,7 +245,8 @@ export class ContentPage {
     copyContentBtn?.addEventListener('click', async () => {
       if (this.currentModalItem) {
         try {
-          await navigator.clipboard.writeText(this.currentModalItem.content);
+          const content = this.currentModalItem.content || '';
+          await navigator.clipboard.writeText(content);
           this.toastService.success('Copied!', 'Content copied to clipboard');
         } catch (error) {
           this.toastService.error('Copy Failed', 'Could not copy to clipboard');
@@ -523,22 +525,42 @@ export class ContentPage {
   }
 
   private bindContentEvents(): void {
-    // Bind "View Full" button events
-    this.element?.querySelectorAll('.view-full-btn').forEach(btn => {
-      btn.addEventListener('click', (e) => {
-        const contentId = (e.target as HTMLElement).closest('.view-full-btn')?.getAttribute('data-content-id');
-        if (contentId) {
-          this.showFullContent(contentId);
-        }
-      });
-    });
+    // Use event delegation to handle dynamically created buttons
+    // Remove any existing delegated listener first
+    this.element?.removeEventListener('click', this.handleContentClick);
+
+    // Add delegated event listener
+    this.element?.addEventListener('click', this.handleContentClick);
   }
 
+  private handleContentClick = (e: Event) => {
+    const target = e.target as HTMLElement;
+
+    // Check if clicked element is a view-full-btn or is inside one
+    const viewBtn = target.closest('.view-full-btn') as HTMLElement;
+    if (viewBtn) {
+      e.preventDefault();
+      e.stopPropagation();
+
+      const contentId = viewBtn.getAttribute('data-content-id');
+      if (contentId) {
+        this.showFullContent(contentId);
+      }
+    }
+  };
+
   private renderContentItem(item: ShopContent): string {
     const domain = getDomainFromUrl(item.url);
     const hasChanged = item.changed;
-    const preview = item.content.substring(0, 300) + (item.content.length > 300 ? '...' : '');
-    const isLongContent = item.content.length > 300;
+
+    // Handle case where content might be undefined or null
+    const content = item.content || '';
+    const preview = content.substring(0, 300) + (content.length > 300 ? '...' : '');
+    const isLongContent = content.length > 300;
+
+    // Generate a unique ID for the item since backend might not provide one
+    // Use content hash as the unique identifier, or fall back to URL + content_type
+    const contentId = item.id || item.content_hash || `${item.url}_${item.content_type}`;
 
     return `
       <div class="p-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
@@ -566,7 +588,7 @@ export class ContentPage {
             <div class="text-sm text-gray-500 dark:text-gray-400 mb-3">
               <div class="whitespace-pre-wrap">${preview.replace(/[#*]/g, '')}</div>
               ${isLongContent ? `
-                <button class="view-full-btn text-primary-600 dark:text-primary-400 hover:underline text-xs mt-1" data-content-id="${item.id}">
+                <button class="view-full-btn text-primary-600 dark:text-primary-400 hover:underline text-xs mt-1" data-content-id="${contentId}">
                   View full content
                 </button>
               ` : ''}
@@ -577,7 +599,7 @@ export class ContentPage {
           </div>
           <div class="ml-4 flex-shrink-0 space-y-2">
             ${isLongContent ? `
-              <button class="view-full-btn btn btn-secondary btn-sm w-full" data-content-id="${item.id}">
+              <button class="view-full-btn btn btn-secondary btn-sm w-full" data-content-id="${contentId}">
                 <div class="w-4 h-4 mr-1">${getIcon('eye')}</div>
                 View Full
               </button>
@@ -601,15 +623,25 @@ export class ContentPage {
 
   private showFullContent(contentId: string): void {
     // Find the content item by ID
-    if (!this.contentData) return;
+    if (!this.contentData) {
+      return;
+    }
 
     let foundItem: ShopContent | null = null;
-    for (const [_, items] of Object.entries(this.contentData.results)) {
-      foundItem = items.find(item => item.id === contentId) || null;
-      if (foundItem) break;
+    for (const [category, items] of Object.entries(this.contentData.results)) {
+      foundItem = items.find(item => {
+        // Use the same ID generation logic as in renderContentItem
+        const itemId = item.id || item.content_hash || `${item.url}_${item.content_type}`;
+        return itemId === contentId;
+      }) || null;
+      if (foundItem) {
+        break;
+      }
     }
 
-    if (!foundItem) return;
+    if (!foundItem) {
+      return;
+    }
 
     // Populate modal
     const modal = this.element?.querySelector('#content-modal');
@@ -622,9 +654,11 @@ export class ContentPage {
 
     if (modalTitle) modalTitle.textContent = `${getContentTypeLabel(foundItem.content_type)} Content`;
     if (modalUrl) modalUrl.textContent = foundItem.url;
-    if (modalContent) modalContent.textContent = foundItem.content;
+    if (modalContent) modalContent.textContent = foundItem.content || '';
     if (modalMetadata) {
-      modalMetadata.textContent = `Last updated: ${formatRelativeTime(foundItem.updated_at)} • ${foundItem.content.length} characters • Hash: ${foundItem.content_hash.substring(0, 8)}`;
+      const content = foundItem.content || '';
+      const contentHash = foundItem.content_hash || '';
+      modalMetadata.textContent = `Last updated: ${formatRelativeTime(foundItem.updated_at)} • ${content.length} characters • Hash: ${contentHash.substring(0, 8)}`;
     }
 
     // Update badges
@@ -650,9 +684,9 @@ export class ContentPage {
     modal?.classList.remove('hidden');
   }
 
-  private currentModalItem: ShopContent | null = null;
-
   destroy(): void {
     this.eventsbound = false;
+    // Clean up event listeners
+    this.element?.removeEventListener('click', this.handleContentClick);
   }
 }

+ 30 - 0
web/src/utils/helpers.ts

@@ -2,7 +2,17 @@
  * Format relative time (e.g., "2 hours ago")
  */
 export function formatRelativeTime(dateString: string): string {
+  if (!dateString) {
+    return 'unknown';
+  }
+
   const date = new Date(dateString);
+
+  // Check if the date is valid
+  if (isNaN(date.getTime())) {
+    return 'unknown';
+  }
+
   const now = new Date();
   const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
 
@@ -38,7 +48,17 @@ export function formatRelativeTime(dateString: string): string {
  * Format time for scheduled events - handles both future and past times appropriately
  */
 export function formatScheduleTime(dateString: string): string {
+  if (!dateString) {
+    return 'unknown';
+  }
+
   const date = new Date(dateString);
+
+  // Check if the date is valid
+  if (isNaN(date.getTime())) {
+    return 'unknown';
+  }
+
   const now = new Date();
   const diffInSeconds = Math.floor((date.getTime() - now.getTime()) / 1000);
 
@@ -75,7 +95,17 @@ export function formatScheduleTime(dateString: string): string {
  * Format absolute time (e.g., "Dec 15, 2023 at 2:30 PM")
  */
 export function formatAbsoluteTime(dateString: string): string {
+  if (!dateString) {
+    return 'unknown';
+  }
+
   const date = new Date(dateString);
+
+  // Check if the date is valid
+  if (isNaN(date.getTime())) {
+    return 'unknown';
+  }
+
   return date.toLocaleString('en-US', {
     month: 'short',
     day: 'numeric',