Browse Source

feat: add site type detection to add-site modal

Add detectSite() and updateSiteSettings() to ApiService. Update the
add-site modal with Site Type dropdown, Platform dropdown, Sitemap URL
input with no-sitemap checkbox, and auto-detection on URL blur. Update
getSiteTypeInfo to show combined type/platform labels on site cards.
fszontagh 3 months ago
parent
commit
71f54322e2
4 changed files with 189 additions and 16 deletions
  1. 119 9
      web/src/components/pages/SitesPage.ts
  2. 36 5
      web/src/services/ApiService.ts
  3. 11 1
      web/src/types/index.ts
  4. 23 1
      web/src/utils/helpers.ts

+ 119 - 9
web/src/components/pages/SitesPage.ts

@@ -73,17 +73,66 @@ export class SitesPage {
                         <label for="site-url" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
                           Site URL
                         </label>
+                        <div class="relative">
+                          <input
+                            type="url"
+                            id="site-url"
+                            name="siteUrl"
+                            required
+                            class="input mt-1"
+                            placeholder="https://example.com"
+                          >
+                          <div class="absolute right-3 top-1/2 -translate-y-1/2 mt-0.5 hidden" id="detect-spinner">
+                            <div class="w-4 h-4 spinner"></div>
+                          </div>
+                        </div>
+                        <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
+                          Enter the main URL of the site you want to scrape
+                        </p>
+                        <p class="mt-1 text-sm text-primary-600 dark:text-primary-400 hidden" id="detect-result"></p>
+                      </div>
+                      <div>
+                        <label for="site-type-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                          Site Type
+                        </label>
+                        <select id="site-type-select" name="siteType" class="input mt-1">
+                          <option value="webshop">Webshop</option>
+                          <option value="website">Website</option>
+                        </select>
+                      </div>
+                      <div>
+                        <label for="site-platform-select" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                          Platform
+                        </label>
+                        <select id="site-platform-select" name="sitePlatform" class="input mt-1">
+                          <option value="generic">Generic</option>
+                          <option value="shoprenter">ShopRenter</option>
+                          <option value="woocommerce">WooCommerce</option>
+                          <option value="shopify">Shopify</option>
+                        </select>
+                      </div>
+                      <div>
+                        <label for="sitemap-url" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
+                          Sitemap URL
+                        </label>
                         <input
                           type="url"
-                          id="site-url"
-                          name="siteUrl"
-                          required
+                          id="sitemap-url"
+                          name="sitemapUrl"
                           class="input mt-1"
-                          placeholder="https://example.com"
+                          placeholder="https://example.com/sitemap.xml"
                         >
-                        <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
-                          Enter the main URL of the site you want to scrape
-                        </p>
+                        <div class="flex items-center mt-2">
+                          <input
+                            type="checkbox"
+                            id="no-sitemap"
+                            name="noSitemap"
+                            class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
+                          >
+                          <label for="no-sitemap" class="ml-2 text-sm text-gray-500 dark:text-gray-400">
+                            No sitemap available
+                          </label>
+                        </div>
                       </div>
                       <div>
                         <label for="custom-id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
@@ -171,6 +220,62 @@ export class SitesPage {
       urlInput?.focus();
     });
 
+    // URL blur: auto-detect site type
+    const urlInput = this.element?.querySelector('#site-url') as HTMLInputElement;
+    urlInput?.addEventListener('blur', async () => {
+      const url = urlInput.value.trim();
+      if (!url) return;
+      try {
+        new URL(url);
+      } catch {
+        return;
+      }
+
+      const detectSpinner = this.element?.querySelector('#detect-spinner');
+      const detectResult = this.element?.querySelector('#detect-result');
+      const siteTypeSelect = this.element?.querySelector('#site-type-select') as HTMLSelectElement;
+      const sitePlatformSelect = this.element?.querySelector('#site-platform-select') as HTMLSelectElement;
+      const sitemapUrlInput = this.element?.querySelector('#sitemap-url') as HTMLInputElement;
+
+      detectSpinner?.classList.remove('hidden');
+      if (detectResult) {
+        detectResult.classList.add('hidden');
+        detectResult.textContent = '';
+      }
+
+      try {
+        const response = await this.apiService.detectSite(url);
+        if (response.success && response.data) {
+          const { site_type, site_platform, sitemap_url } = response.data;
+          if (siteTypeSelect) siteTypeSelect.value = site_type;
+          if (sitePlatformSelect) sitePlatformSelect.value = site_platform;
+          if (sitemap_url && sitemapUrlInput) sitemapUrlInput.value = sitemap_url;
+          if (detectResult) {
+            const platformLabel = site_platform === 'generic' ? 'Generic' :
+              site_platform.charAt(0).toUpperCase() + site_platform.slice(1);
+            detectResult.textContent = `Detected: ${site_type} (${platformLabel})${sitemap_url ? ' - sitemap found' : ''}`;
+            detectResult.classList.remove('hidden');
+          }
+        }
+      } catch {
+        // Detection failed silently
+      } finally {
+        detectSpinner?.classList.add('hidden');
+      }
+    });
+
+    // No sitemap checkbox
+    const noSitemapCheckbox = this.element?.querySelector('#no-sitemap') as HTMLInputElement;
+    const sitemapInput = this.element?.querySelector('#sitemap-url') as HTMLInputElement;
+    noSitemapCheckbox?.addEventListener('change', () => {
+      if (sitemapInput) {
+        sitemapInput.disabled = noSitemapCheckbox.checked;
+        if (noSitemapCheckbox.checked) {
+          sitemapInput.value = '';
+        }
+      }
+    });
+
     // Hide modal
     const hideModal = () => {
       modal?.classList.add('hidden');
@@ -197,6 +302,11 @@ export class SitesPage {
       const formData = new FormData(form);
       const siteUrl = formData.get('siteUrl') as string;
       const customId = (formData.get('customId') as string)?.trim() || undefined;
+      const siteType = formData.get('siteType') as string || undefined;
+      const sitePlatform = formData.get('sitePlatform') as string || undefined;
+      const noSitemap = (this.element?.querySelector('#no-sitemap') as HTMLInputElement)?.checked;
+      const sitemapUrlValue = (formData.get('sitemapUrl') as string)?.trim() || undefined;
+      const customSitemapUrl = noSitemap ? undefined : sitemapUrlValue;
 
       if (!siteUrl) return;
 
@@ -215,7 +325,7 @@ export class SitesPage {
       submitSpinner?.classList.remove('hidden');
 
       try {
-        const response = await this.apiService.createJob(siteUrl, customId);
+        const response = await this.apiService.createJob(siteUrl, customId, siteType, sitePlatform, customSitemapUrl);
 
         if (response.success) {
           this.toastService.success('Site Added', 'Site scraping job has been started');
@@ -313,7 +423,7 @@ export class SitesPage {
 
   private renderSiteCard(site: SiteWithAnalytics): string {
     const domain = getDomainFromUrl(site.url);
-    const typeInfo = getSiteTypeInfo(site.site_type);
+    const typeInfo = getSiteTypeInfo(site.site_type, site.site_platform);
 
     return `
       <div

+ 36 - 5
web/src/services/ApiService.ts

@@ -2,6 +2,7 @@ import {
   Site,
   SiteWithAnalytics,
   SiteDetail,
+  DetectSiteResponse,
   Job,
   QueueStats,
   SiteContent,
@@ -76,11 +77,18 @@ export class ApiService {
     return this.request(`/api/jobs/${jobId}`);
   }
 
-  async createJob(siteUrl: string, customId?: string): Promise<ApiResponse<{ job_id: string; message: string }>> {
-    const body: { url: string; custom_id?: string } = { url: siteUrl };
-    if (customId) {
-      body.custom_id = customId;
-    }
+  async createJob(
+    siteUrl: string,
+    customId?: string,
+    siteType?: string,
+    sitePlatform?: string,
+    customSitemapUrl?: string
+  ): Promise<ApiResponse<{ job_id: string; message: string }>> {
+    const body: any = { url: siteUrl };
+    if (customId) body.custom_id = customId;
+    if (siteType) body.site_type = siteType;
+    if (sitePlatform) body.site_platform = sitePlatform;
+    if (customSitemapUrl) body.custom_sitemap_url = customSitemapUrl;
 
     return this.request('/api/jobs', {
       method: 'POST',
@@ -88,6 +96,29 @@ export class ApiService {
     });
   }
 
+  async detectSite(url: string): Promise<ApiResponse<DetectSiteResponse>> {
+    return this.request('/api/sites/detect', {
+      method: 'POST',
+      body: JSON.stringify({ url }),
+    });
+  }
+
+  async updateSiteSettings(
+    siteId: string,
+    settings: {
+      site_type?: string;
+      site_platform?: string;
+      custom_sitemap_url?: string | null;
+      max_crawl_depth?: number;
+      max_pages?: number;
+    }
+  ): Promise<ApiResponse<{ message: string }>> {
+    return this.request(`/api/sites/${siteId}/settings`, {
+      method: 'PATCH',
+      body: JSON.stringify(settings),
+    });
+  }
+
   // Sites
   async getSites(): Promise<ApiResponse<{ sites: SiteWithAnalytics[] }>> {
     const response = await this.request<{ sites: SiteWithAnalytics[] }>('/api/sites');

+ 11 - 1
web/src/types/index.ts

@@ -4,13 +4,23 @@ export interface Site {
   custom_id: string | null;
   url: string;
   sitemap_url?: string;
-  site_type: 'shopify' | 'woocommerce' | 'shoprenter' | string;
+  site_type: 'webshop' | 'website' | string;
+  site_platform?: 'shopify' | 'woocommerce' | 'shoprenter' | 'generic' | string;
+  custom_sitemap_url?: string | null;
+  max_crawl_depth?: number;
+  max_pages?: number;
   qdrant_enabled: boolean;
   deleted_at: string | null;
   created_at: string;
   updated_at: string;
 }
 
+export interface DetectSiteResponse {
+  site_type: string;
+  site_platform: string;
+  sitemap_url: string | null;
+}
+
 export interface SiteAnalytics {
   total_scrapes: number;
   last_scraped_at?: string;

+ 23 - 1
web/src/utils/helpers.ts

@@ -196,7 +196,15 @@ export function getContentTypeLabel(type: string): string {
 /**
  * Get site type label and logo
  */
-export function getSiteTypeInfo(type: string): { label: string; logo?: string } {
+export function getSiteTypeInfo(type: string, platform?: string): { label: string; logo?: string } {
+  // If platform is provided, show combined label
+  if (platform) {
+    const platformLabel = getPlatformLabel(platform);
+    const typeLabel = type === 'webshop' ? 'Webshop' : type === 'website' ? 'Website' : type;
+    return { label: platform === 'generic' ? typeLabel : `${typeLabel} (${platformLabel})` };
+  }
+
+  // Legacy fallback: type was used as platform name
   switch (type.toLowerCase()) {
     case 'shopify':
       return { label: 'Shopify' };
@@ -204,11 +212,25 @@ export function getSiteTypeInfo(type: string): { label: string; logo?: string }
       return { label: 'WooCommerce' };
     case 'shoprenter':
       return { label: 'ShopRenter' };
+    case 'webshop':
+      return { label: 'Webshop' };
+    case 'website':
+      return { label: 'Website' };
     default:
       return { label: type || 'Unknown' };
   }
 }
 
+function getPlatformLabel(platform: string): string {
+  switch (platform.toLowerCase()) {
+    case 'shopify': return 'Shopify';
+    case 'woocommerce': return 'WooCommerce';
+    case 'shoprenter': return 'ShopRenter';
+    case 'generic': return 'Generic';
+    default: return platform;
+  }
+}
+
 /**
  * Truncate text with ellipsis
  */