diff --git a/src/components/PackageBrowserV2.tsx b/src/components/PackageBrowserV2.tsx index e2da6a3..3703157 100644 --- a/src/components/PackageBrowserV2.tsx +++ b/src/components/PackageBrowserV2.tsx @@ -17,11 +17,11 @@ interface PackageBrowserProps { onFiltersChange: (filters: FilterOptions) => void } -export function PackageBrowserV2({ - selectedPlatform, - selectedPackages, +export function PackageBrowserV2({ + selectedPlatform, + selectedPackages, onPackageToggle, - onFiltersChange + onFiltersChange }: PackageBrowserProps) { const { t } = useLocale() const [packages, setPackages] = useState([]) @@ -42,6 +42,8 @@ export function PackageBrowserV2({ // Load packages when platform changes useEffect(() => { + const controller = new AbortController() + const loadPackages = async () => { if (!selectedPlatform) { setPackages([]) @@ -52,7 +54,7 @@ export function PackageBrowserV2({ setLoading(true) setPackages([]) setHasMore(true) - + try { console.log('🔍 Frontend: Fetching initial packages for', selectedPlatform.id) const params: any = { @@ -60,20 +62,20 @@ export function PackageBrowserV2({ limit: 50, offset: 0 } - + if (searchQuery && searchQuery.trim()) { params.search = searchQuery.trim() } - + if (isDebianUbuntu && typeFilter && typeFilter !== 'all') { params.type = typeFilter as 'gui' | 'cli' } if (isArch && repositoryFilter && repositoryFilter !== 'all') { params.repository = repositoryFilter as 'official' | 'aur' } - + console.log('🔍 Frontend: API params:', params) - const result = await apiClient.getPackages(params) + const result = await apiClient.getPackages(params, controller.signal) console.log('📦 Frontend: Received initial packages:', { total: result.total, packageCount: result.packages.length, @@ -83,47 +85,57 @@ export function PackageBrowserV2({ setTotalCount(result.total) setHasMore(result.packages.length < result.total) } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + console.log('Request aborted') + return + } console.error('Failed to load packages:', error) setPackages([]) } finally { - setLoading(false) + if (!controller.signal.aborted) { + setLoading(false) + } } } loadPackages() + + return () => { + controller.abort() + } }, [selectedPlatform, typeFilter, repositoryFilter, isDebianUbuntu, isArch]) // Debounced search to prevent focus loss useEffect(() => { if (!selectedPlatform) return - + const timeoutId = setTimeout(() => { const loadPackages = async () => { // DON'T set loading to true - it causes re-render and focus loss // setLoading(true) - + try { const params: any = { platform_id: selectedPlatform.id, limit: 50, offset: 0 } - + if (searchQuery && searchQuery.trim()) { params.search = searchQuery.trim() } - + if (isDebianUbuntu && typeFilter && typeFilter !== 'all') { params.type = typeFilter as 'gui' | 'cli' } if (isArch && repositoryFilter && repositoryFilter !== 'all') { params.repository = repositoryFilter as 'official' | 'aur' } - + console.log('🔍 Frontend: Debounced API params:', params) const result = await apiClient.getPackages(params) console.log('📦 Frontend: Debounced result:', result.packages.length) - + // Update packages without triggering loading state setPackages(result.packages) setTotalCount(result.total) @@ -143,7 +155,7 @@ export function PackageBrowserV2({ // Load more packages const loadMore = async () => { if (!selectedPlatform || loadingMore || !hasMore) return - + setLoadingMore(true) try { const params: any = { @@ -151,18 +163,18 @@ export function PackageBrowserV2({ limit: 50, offset: packages.length } - + if (searchQuery && searchQuery.trim()) { params.search = searchQuery.trim() } - + if (isDebianUbuntu && typeFilter && typeFilter !== 'all') { params.type = typeFilter as 'gui' | 'cli' } if (isArch && repositoryFilter && repositoryFilter !== 'all') { params.repository = repositoryFilter as 'official' | 'aur' } - + const result = await apiClient.getPackages(params) setPackages(prev => [...prev, ...result.packages]) setHasMore(packages.length + result.packages.length < result.total) @@ -256,30 +268,30 @@ export function PackageBrowserV2({ {/* Type Filter (Debian/Ubuntu only) */} - {isDebianUbuntu && ( - - )} - {isArch && ( - - )} + {isDebianUbuntu && ( + + )} + {isArch && ( + + )} {/* Package List */} @@ -303,11 +315,10 @@ export function PackageBrowserV2({ packages.map((pkg) => (
onPackageToggle(pkg)} > )) )} - + {/* Load More Button */} {hasMore && (
diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index f2493c1..6a65a06 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -6,7 +6,7 @@ import { const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL && - process.env.NEXT_PUBLIC_API_URL.trim() !== "" + process.env.NEXT_PUBLIC_API_URL.trim() !== "" ? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, "") : "/api"; @@ -47,7 +47,8 @@ class ApiClient { // Package operations async getPackages( - filters: FilterOptions = {} + filters: FilterOptions = {}, + signal?: AbortSignal ): Promise<{ packages: Package[]; total: number }> { const params = new URLSearchParams(); @@ -64,7 +65,8 @@ class ApiClient { const query = params.toString() ? `?${params.toString()}` : ""; return this.request<{ packages: Package[]; total: number }>( - `/packages${query}` + `/packages${query}`, + { signal } ); } diff --git a/src/services/archPackageFetcher.ts b/src/services/archPackageFetcher.ts index ee0028e..087d2ac 100644 --- a/src/services/archPackageFetcher.ts +++ b/src/services/archPackageFetcher.ts @@ -7,29 +7,11 @@ interface ArchPackage { lastUpdated: string } -// Simple HTML parser without external dependencies -function parseHTML(html: string) { - return { - querySelector: (selector: string) => { - const regex = new RegExp(`<${selector}[^>]*>([\\s\\S]*?)<\\/${selector}>`, 'gi') - const match = regex.exec(html) - return match ? { textContent: match[1].replace(/<[^>]*>/g, '').trim() } : null - }, - querySelectorAll: (selector: string) => { - const results: any[] = [] - const regex = new RegExp(`<${selector}[^>]*>([\\s\\S]*?)<\\/${selector}>`, 'gi') - let match - while ((match = regex.exec(html)) !== null) { - results.push({ innerHTML: match[1], textContent: match[1].replace(/<[^>]*>/g, '').trim() }) - } - return results - } - } -} + export class ArchPackageFetcher { private baseUrl = 'https://archlinux.org/packages' - + /** * Fetch all Arch Linux packages with progress callback */ @@ -37,86 +19,86 @@ export class ArchPackageFetcher { onProgress?: (current: number, total: number, packageName: string) => void ): Promise { console.log('🔍 Starting Arch Linux package fetch...') - + const allPackages: ArchPackage[] = [] let currentPage = 1 let totalPages = 1 - + try { // Fetch first page to get total pages const firstPageUrl = `${this.baseUrl}/?page=1` console.log(`📄 Fetching first page: ${firstPageUrl}`) - + const firstPageResponse = await fetch(firstPageUrl) if (!firstPageResponse.ok) { throw new Error(`HTTP error! status: ${firstPageResponse.status}`) } - + const firstPageHtml = await firstPageResponse.text() - + // Parse total packages and pages const statsMatch = firstPageHtml.match(/(\d+)\s+matching packages[\s\S]*?Page \d+ of (\d+)/) const totalPackages = statsMatch ? parseInt(statsMatch[1]) : 0 totalPages = statsMatch ? parseInt(statsMatch[2]) : 1 - + console.log(`📦 Total packages: ${totalPackages}`) console.log(`📄 Total pages: ${totalPages}`) - + // Parse first page packages const firstPagePackages = this.parsePackagesFromHtml(firstPageHtml) allPackages.push(...firstPagePackages) - + if (onProgress) { onProgress(allPackages.length, totalPackages, firstPagePackages[0]?.name || 'N/A') } - + // Fetch remaining pages for (currentPage = 2; currentPage <= totalPages; currentPage++) { const pageUrl = `${this.baseUrl}/?page=${currentPage}` console.log(`📄 Fetching page ${currentPage}/${totalPages}...`) - + try { const response = await fetch(pageUrl) if (!response.ok) { console.error(`❌ Failed to fetch page ${currentPage}: ${response.status}`) continue } - + const html = await response.text() const packages = this.parsePackagesFromHtml(html) - + allPackages.push(...packages) - + if (onProgress && packages.length > 0) { onProgress(allPackages.length, totalPackages, packages[0].name) } - + // Rate limiting: wait 100ms between requests await new Promise(resolve => setTimeout(resolve, 100)) - + } catch (error) { console.error(`❌ Error fetching page ${currentPage}:`, error) } } - + console.log(`✅ Fetch completed! Total packages: ${allPackages.length}`) return allPackages - + } catch (error) { console.error('❌ Error fetching Arch packages:', error) throw error } } - + /** * Parse packages from HTML using regex */ private parsePackagesFromHtml(html: string): ArchPackage[] { const packages: ArchPackage[] = [] - + // Match all table rows const rowRegex = /\s*([^<]+)<\/td>\s*([^<]+)<\/td>\s*]*>([^<]+)<\/a><\/td>\s*([^<]+)<\/td>\s*]*>([^<]*)<\/td>\s*([^<]*)<\/td>/gi - + let match while ((match = rowRegex.exec(html)) !== null) { try { @@ -126,7 +108,7 @@ export class ArchPackageFetcher { const version = match[4].trim() const description = match[5].trim() const lastUpdated = match[6].trim() - + if (name && version) { packages.push({ name, @@ -141,10 +123,10 @@ export class ArchPackageFetcher { console.error('Error parsing package row:', error) } } - + return packages } - + /** * Store packages in database */ @@ -153,17 +135,17 @@ export class ArchPackageFetcher { onProgress?: (current: number, total: number) => void ): Promise { const { query } = await import('@/lib/database/config') - + console.log(`💾 Storing ${packages.length} Arch packages in database...`) - + const batchSize = 100 let stored = 0 let updated = 0 let skipped = 0 - + for (let i = 0; i < packages.length; i += batchSize) { const batch = packages.slice(i, i + batchSize) - + for (const pkg of batch) { try { // Check if package already exists @@ -171,7 +153,7 @@ export class ArchPackageFetcher { 'SELECT id, version FROM packages WHERE name = $1 AND platform_id = $2', [pkg.name, 'arch'] ) - + if (existingResult.rows.length === 0) { // Insert new package (id will be auto-generated by SERIAL) await query( @@ -197,17 +179,17 @@ export class ArchPackageFetcher { ) skipped++ } - + if (onProgress && (stored + updated + skipped) % 100 === 0) { onProgress(stored + updated + skipped, packages.length) } - + } catch (error) { console.error(`Error storing package ${pkg.name}:`, error) } } } - + console.log(`✅ Stored ${stored} new packages, updated ${updated}, skipped ${skipped}`) } }