/** * Parts lookup utility for frontend * * Provides functions to fetch parts data by manual filename */ import { ebayClient } from './ebay-api' interface PartForPage { partNumber: string description: string ebayListings: Array<{ itemId: string title: string price: string currency: string imageUrl?: string viewItemUrl: string condition?: string shippingCost?: string affiliateLink: string }> } interface ManualPartsLookup { [manualFilename: string]: PartForPage[] } interface ManualPagesParts { [manualFilename: string]: { [pageNumber: string]: PartForPage[] } } // Cache for eBay search results const ebaySearchCache = new Map() const EBAY_CACHE_TTL = 15 * 60 * 1000 // 15 minutes let manualPartsCache: ManualPartsLookup | null = null let manualPagesPartsCache: ManualPagesParts | null = null /** * Load parts lookup data */ async function loadPartsData(): Promise<{ manualParts: ManualPartsLookup manualPagesParts: ManualPagesParts }> { if (manualPartsCache && manualPagesPartsCache) { return { manualParts: manualPartsCache, manualPagesParts: manualPagesPartsCache, } } try { // Try to load from public data directory (relative to public folder) const [manualPartsResponse, manualPagesResponse] = await Promise.all([ fetch('/manual_parts_lookup.json'), fetch('/manual_pages_parts.json'), ]) if (manualPartsResponse.ok && manualPagesResponse.ok) { const [manualParts, manualPagesParts] = await Promise.all([ manualPartsResponse.json(), manualPagesResponse.json(), ]) manualPartsCache = manualParts manualPagesPartsCache = manualPagesParts return { manualParts, manualPagesParts } } } catch (error) { console.warn('Could not load parts data:', error) } // Return empty data if loading fails return { manualParts: {}, manualPagesParts: {}, } } /** * Search eBay for parts with caching */ async function searchEBayForParts(partNumber: string, description?: string, manufacturer?: string): Promise { const cacheKey = `parts:${partNumber}:${description || ''}:${manufacturer || ''}` // Check cache const cached = ebaySearchCache.get(cacheKey) if (cached && Date.now() - cached.timestamp < EBAY_CACHE_TTL) { return cached.results } try { const results = await ebayClient.searchVendingParts(partNumber, description, manufacturer) ebaySearchCache.set(cacheKey, { results, timestamp: Date.now() }) return results } catch (error) { console.error(`Error searching eBay for part ${partNumber}:`, error) // Return empty array if API fails return [] } } /** * Enhance parts data with real-time eBay listings */ async function enhancePartsData(parts: PartForPage[]): Promise { if (!ebayClient.isConfigured()) { return parts } const enhancedParts = await Promise.all(parts.map(async (part) => { // Only search for parts without existing eBay listings if (part.ebayListings.length === 0) { const ebayResults = await searchEBayForParts(part.partNumber, part.description) part.ebayListings = ebayResults.map(result => ({ itemId: result.itemId, title: result.title, price: result.price, currency: result.currency, imageUrl: result.imageUrl, viewItemUrl: result.viewItemUrl, condition: result.condition, shippingCost: result.shippingCost, affiliateLink: result.affiliateLink, })) } return part })) return enhancedParts } /** * Get all parts for a manual with enhanced eBay data */ export async function getPartsForManual( manualFilename: string ): Promise { const { manualParts } = await loadPartsData() // Try exact match first if (manualParts[manualFilename]) { const parts = manualParts[manualFilename] return enhancePartsData(parts) } // Try case-insensitive match const lowerFilename = manualFilename.toLowerCase() for (const [filename, parts] of Object.entries(manualParts)) { if (filename.toLowerCase() === lowerFilename) { return enhancePartsData(parts) } } // Try partial match (filename without extension) const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, '') const lowerWithoutExt = filenameWithoutExt.toLowerCase() for (const [filename, parts] of Object.entries(manualParts)) { const otherWithoutExt = filename.replace(/\.pdf$/i, '').toLowerCase() if (otherWithoutExt === lowerWithoutExt || otherWithoutExt.includes(lowerWithoutExt) || lowerWithoutExt.includes(otherWithoutExt)) { return enhancePartsData(parts) } } return [] } /** * Get parts for a specific page of a manual with enhanced eBay data */ export async function getPartsForPage( manualFilename: string, pageNumber: number ): Promise { const { manualPagesParts } = await loadPartsData() // Try exact match first if (manualPagesParts[manualFilename] && manualPagesParts[manualFilename][pageNumber.toString()]) { const parts = manualPagesParts[manualFilename][pageNumber.toString()] return enhancePartsData(parts) } // Try case-insensitive match const lowerFilename = manualFilename.toLowerCase() for (const [filename, pages] of Object.entries(manualPagesParts)) { if (filename.toLowerCase() === lowerFilename) { if (pages[pageNumber.toString()]) { const parts = pages[pageNumber.toString()] return enhancePartsData(parts) } } } // Try partial match const filenameWithoutExt = manualFilename.replace(/\.pdf$/i, '') const lowerWithoutExt = filenameWithoutExt.toLowerCase() for (const [filename, pages] of Object.entries(manualPagesParts)) { const otherWithoutExt = filename.replace(/\.pdf$/i, '').toLowerCase() if (otherWithoutExt === lowerWithoutExt || otherWithoutExt.includes(lowerWithoutExt) || lowerWithoutExt.includes(otherWithoutExt)) { if (pages[pageNumber.toString()]) { const parts = pages[pageNumber.toString()] return enhancePartsData(parts) } } } return [] } /** * Get top N parts for a manual (most relevant) with enhanced eBay data */ export async function getTopPartsForManual( manualFilename: string, limit: number = 5 ): Promise { const parts = await getPartsForManual(manualFilename) // Sort by number of eBay listings (more listings = more relevant) const sorted = parts.sort((a, b) => b.ebayListings.length - a.ebayListings.length) return sorted.slice(0, limit) } /** * Check if a manual has parts available */ export async function hasPartsForManual(manualFilename: string): Promise { const parts = await getPartsForManual(manualFilename) return parts.length > 0 && parts.some(part => part.ebayListings.length > 0) } /** * Get a set of all manual filenames that have parts available * Useful for filtering */ export async function getManualsWithParts(): Promise> { const { manualParts } = await loadPartsData() const manualsWithParts = new Set() for (const [filename, parts] of Object.entries(manualParts)) { if (parts.length > 0 && parts.some(part => part.ebayListings.length > 0)) { manualsWithParts.add(filename) // Also add variations for matching manualsWithParts.add(filename.toLowerCase()) manualsWithParts.add(filename.replace(/\.pdf$/i, '')) manualsWithParts.add(filename.replace(/\.pdf$/i, '').toLowerCase()) } } return manualsWithParts } /** * Clear cache (useful for development) */ export function clearPartsCache(): void { manualPartsCache = null manualPagesPartsCache = null }