deploy: fix manuals eBay live search
This commit is contained in:
parent
60b70e46ab
commit
1948fd564e
3 changed files with 335 additions and 85 deletions
|
|
@ -26,6 +26,10 @@ interface eBaySearchResult {
|
||||||
|
|
||||||
type MaybeArray<T> = T | T[]
|
type MaybeArray<T> = T | T[]
|
||||||
|
|
||||||
|
const SEARCH_CACHE_TTL = 15 * 60 * 1000 // 15 minutes
|
||||||
|
const searchResponseCache = new Map<string, { results: eBaySearchResult[]; timestamp: number }>()
|
||||||
|
const inFlightSearchResponses = new Map<string, Promise<eBaySearchResult[]>>()
|
||||||
|
|
||||||
// Affiliate campaign ID for generating links
|
// Affiliate campaign ID for generating links
|
||||||
const AFFILIATE_CAMPAIGN_ID = process.env.EBAY_AFFILIATE_CAMPAIGN_ID?.trim() || ''
|
const AFFILIATE_CAMPAIGN_ID = process.env.EBAY_AFFILIATE_CAMPAIGN_ID?.trim() || ''
|
||||||
|
|
||||||
|
|
@ -78,6 +82,64 @@ function normalizeItem(item: any): eBaySearchResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function readEbayErrorMessage(response: Response) {
|
||||||
|
const text = await response.text().catch(() => '')
|
||||||
|
if (!text.trim()) {
|
||||||
|
return `eBay API error: ${response.status}`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text) as any
|
||||||
|
const messages = parsed?.errorMessage?.[0]?.error?.[0]
|
||||||
|
const message = Array.isArray(messages?.message) ? messages.message[0] : messages?.message
|
||||||
|
|
||||||
|
if (typeof message === 'string' && message.trim()) {
|
||||||
|
const errorId = Array.isArray(messages?.errorId) ? messages.errorId[0] : messages?.errorId
|
||||||
|
return errorId ? `eBay API error ${errorId}: ${message}` : `eBay API error: ${message}`
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall through to returning the raw text below.
|
||||||
|
}
|
||||||
|
|
||||||
|
return text.trim() || `eBay API error: ${response.status}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCacheKey(
|
||||||
|
keywords: string,
|
||||||
|
categoryId: string | undefined,
|
||||||
|
sortOrder: string,
|
||||||
|
maxResults: number,
|
||||||
|
): string {
|
||||||
|
return [
|
||||||
|
keywords.trim().toLowerCase(),
|
||||||
|
categoryId || '',
|
||||||
|
sortOrder || 'BestMatch',
|
||||||
|
String(maxResults),
|
||||||
|
].join('|')
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedSearchResults(cacheKey: string): eBaySearchResult[] | null {
|
||||||
|
const cached = searchResponseCache.get(cacheKey)
|
||||||
|
|
||||||
|
if (!cached) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - cached.timestamp > SEARCH_CACHE_TTL) {
|
||||||
|
searchResponseCache.delete(cacheKey)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached.results
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCachedSearchResults(cacheKey: string, results: eBaySearchResult[]) {
|
||||||
|
searchResponseCache.set(cacheKey, {
|
||||||
|
results,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url)
|
const { searchParams } = new URL(request.url)
|
||||||
|
|
||||||
|
|
@ -85,6 +147,7 @@ export async function GET(request: NextRequest) {
|
||||||
const categoryId = searchParams.get('categoryId') || undefined
|
const categoryId = searchParams.get('categoryId') || undefined
|
||||||
const sortOrder = searchParams.get('sortOrder') || 'BestMatch'
|
const sortOrder = searchParams.get('sortOrder') || 'BestMatch'
|
||||||
const maxResults = parseInt(searchParams.get('maxResults') || '6', 10)
|
const maxResults = parseInt(searchParams.get('maxResults') || '6', 10)
|
||||||
|
const cacheKey = buildCacheKey(keywords || '', categoryId, sortOrder, maxResults)
|
||||||
|
|
||||||
if (!keywords) {
|
if (!keywords) {
|
||||||
return NextResponse.json({ error: 'Keywords parameter is required' }, { status: 400 })
|
return NextResponse.json({ error: 'Keywords parameter is required' }, { status: 400 })
|
||||||
|
|
@ -100,6 +163,24 @@ export async function GET(request: NextRequest) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cachedResults = getCachedSearchResults(cacheKey)
|
||||||
|
if (cachedResults) {
|
||||||
|
return NextResponse.json(cachedResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inFlight = inFlightSearchResponses.get(cacheKey)
|
||||||
|
if (inFlight) {
|
||||||
|
try {
|
||||||
|
const results = await inFlight
|
||||||
|
return NextResponse.json(results)
|
||||||
|
} catch (error) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: error instanceof Error ? error.message : 'Failed to fetch products from eBay' },
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build eBay Finding API URL
|
// Build eBay Finding API URL
|
||||||
const baseUrl = 'https://svcs.ebay.com/services/search/FindingService/v1'
|
const baseUrl = 'https://svcs.ebay.com/services/search/FindingService/v1'
|
||||||
const url = new URL(baseUrl)
|
const url = new URL(baseUrl)
|
||||||
|
|
@ -118,6 +199,7 @@ export async function GET(request: NextRequest) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const request = (async () => {
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -126,12 +208,8 @@ export async function GET(request: NextRequest) {
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text()
|
const errorMessage = await readEbayErrorMessage(response)
|
||||||
console.error('eBay API error:', response.status, errorText)
|
throw new Error(errorMessage)
|
||||||
return NextResponse.json(
|
|
||||||
{ error: `eBay API error: ${response.status}` },
|
|
||||||
{ status: response.status }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
@ -139,25 +217,32 @@ export async function GET(request: NextRequest) {
|
||||||
// Parse eBay API response
|
// Parse eBay API response
|
||||||
const findItemsAdvancedResponse = data.findItemsAdvancedResponse?.[0]
|
const findItemsAdvancedResponse = data.findItemsAdvancedResponse?.[0]
|
||||||
if (!findItemsAdvancedResponse) {
|
if (!findItemsAdvancedResponse) {
|
||||||
return NextResponse.json([])
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchResult = findItemsAdvancedResponse.searchResult?.[0]
|
const searchResult = findItemsAdvancedResponse.searchResult?.[0]
|
||||||
if (!searchResult || !searchResult.item || searchResult.item.length === 0) {
|
if (!searchResult || !searchResult.item || searchResult.item.length === 0) {
|
||||||
return NextResponse.json([])
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = Array.isArray(searchResult.item) ? searchResult.item : [searchResult.item]
|
const items = Array.isArray(searchResult.item) ? searchResult.item : [searchResult.item]
|
||||||
|
|
||||||
const results: eBaySearchResult[] = items.map((item: any) => normalizeItem(item))
|
return items.map((item: any) => normalizeItem(item))
|
||||||
|
})()
|
||||||
|
|
||||||
|
inFlightSearchResponses.set(cacheKey, request)
|
||||||
|
|
||||||
|
const results = await request
|
||||||
|
setCachedSearchResults(cacheKey, results)
|
||||||
return NextResponse.json(results)
|
return NextResponse.json(results)
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching from eBay API:', error)
|
console.error('Error fetching from eBay API:', error)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to fetch products from eBay' },
|
{ error: error instanceof Error ? error.message : 'Failed to fetch products from eBay' },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
)
|
)
|
||||||
|
} finally {
|
||||||
|
inFlightSearchResponses.delete(cacheKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,15 +88,20 @@ export function PartsPanel({ manualFilename, className = '' }: PartsPanelProps)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error && !hasListings) {
|
if (error && !hasListings) {
|
||||||
|
const loweredError = error.toLowerCase()
|
||||||
|
const statusMessage = error.includes('eBay API not configured')
|
||||||
|
? 'Set EBAY_APP_ID in the app environment so live listings can load.'
|
||||||
|
: loweredError.includes('rate limit') || loweredError.includes('exceeded')
|
||||||
|
? 'eBay is temporarily rate-limited. Try again in a minute.'
|
||||||
|
: error
|
||||||
|
|
||||||
return renderStatusCard(
|
return renderStatusCard(
|
||||||
'eBay unavailable',
|
'eBay unavailable',
|
||||||
error.includes('eBay API not configured')
|
statusMessage,
|
||||||
? 'Set EBAY_APP_ID in the app environment so live listings can load.'
|
|
||||||
: error,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length === 0 || !hasListings) {
|
if (parts.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col h-full ${className}`}>
|
<div className={`flex flex-col h-full ${className}`}>
|
||||||
<div className="px-3 py-2 border-b border-yellow-300/20 flex-shrink-0 bg-yellow-100/50 dark:bg-yellow-900/30">
|
<div className="px-3 py-2 border-b border-yellow-300/20 flex-shrink-0 bg-yellow-100/50 dark:bg-yellow-900/30">
|
||||||
|
|
@ -109,7 +114,26 @@ export function PartsPanel({ manualFilename, className = '' }: PartsPanelProps)
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 py-3 text-xs text-yellow-900/70 dark:text-yellow-100/70 flex items-center justify-center">
|
<div className="px-3 py-3 text-xs text-yellow-900/70 dark:text-yellow-100/70 flex items-center justify-center">
|
||||||
<AlertCircle className="h-4 w-4 mr-2 text-yellow-700 dark:text-yellow-300" />
|
<AlertCircle className="h-4 w-4 mr-2 text-yellow-700 dark:text-yellow-300" />
|
||||||
No parts available
|
No parts data extracted for this manual yet
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasListings) {
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col h-full ${className}`}>
|
||||||
|
<div className="px-3 py-2 border-b border-yellow-300/20 flex-shrink-0 bg-yellow-100/50 dark:bg-yellow-900/30">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<ShoppingCart className="h-3.5 w-3.5 text-yellow-900 dark:text-yellow-100" />
|
||||||
|
<span className="text-xs font-semibold text-yellow-900 dark:text-yellow-100">
|
||||||
|
Parts
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-3 text-xs text-yellow-900/70 dark:text-yellow-100/70 flex items-center justify-center">
|
||||||
|
<AlertCircle className="h-4 w-4 mr-2 text-yellow-700 dark:text-yellow-300" />
|
||||||
|
No live eBay matches found for these parts yet
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -138,6 +162,8 @@ export function PartsPanel({ manualFilename, className = '' }: PartsPanelProps)
|
||||||
<p className="mt-0.5 text-yellow-900/70 dark:text-yellow-100/70">
|
<p className="mt-0.5 text-yellow-900/70 dark:text-yellow-100/70">
|
||||||
{error.includes('eBay API not configured')
|
{error.includes('eBay API not configured')
|
||||||
? 'Set EBAY_APP_ID in the app environment, then reload the panel.'
|
? 'Set EBAY_APP_ID in the app environment, then reload the panel.'
|
||||||
|
: error.toLowerCase().includes('rate limit') || error.toLowerCase().includes('exceeded')
|
||||||
|
? 'eBay is temporarily rate-limited. Reload after a short wait.'
|
||||||
: error}
|
: error}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,35 @@ interface EbaySearchResponse {
|
||||||
|
|
||||||
// Cache for eBay search results
|
// Cache for eBay search results
|
||||||
const ebaySearchCache = new Map<string, { results: EbaySearchResult[]; timestamp: number }>()
|
const ebaySearchCache = new Map<string, { results: EbaySearchResult[]; timestamp: number }>()
|
||||||
|
const inFlightEbaySearches = new Map<string, Promise<EbaySearchResponse>>()
|
||||||
const EBAY_CACHE_TTL = 15 * 60 * 1000 // 15 minutes
|
const EBAY_CACHE_TTL = 15 * 60 * 1000 // 15 minutes
|
||||||
|
|
||||||
|
const GENERIC_PART_TERMS = new Set([
|
||||||
|
'and',
|
||||||
|
'the',
|
||||||
|
'for',
|
||||||
|
'with',
|
||||||
|
'from',
|
||||||
|
'page',
|
||||||
|
'part',
|
||||||
|
'parts',
|
||||||
|
'number',
|
||||||
|
'numbers',
|
||||||
|
'read',
|
||||||
|
'across',
|
||||||
|
'refer',
|
||||||
|
'reference',
|
||||||
|
'shown',
|
||||||
|
'figure',
|
||||||
|
'fig',
|
||||||
|
'rev',
|
||||||
|
'revision',
|
||||||
|
'item',
|
||||||
|
'items',
|
||||||
|
'assembly',
|
||||||
|
'assy',
|
||||||
|
])
|
||||||
|
|
||||||
let manualPartsCache: ManualPartsLookup | null = null
|
let manualPartsCache: ManualPartsLookup | null = null
|
||||||
let manualPagesPartsCache: ManualPagesParts | null = null
|
let manualPagesPartsCache: ManualPagesParts | null = null
|
||||||
|
|
||||||
|
|
@ -140,6 +167,13 @@ async function searchEBayForParts(
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchViaApi = async (categoryId?: string): Promise<EbaySearchResponse> => {
|
const searchViaApi = async (categoryId?: string): Promise<EbaySearchResponse> => {
|
||||||
|
const requestKey = `${cacheKey}:${categoryId || 'general'}`
|
||||||
|
|
||||||
|
const inFlight = inFlightEbaySearches.get(requestKey)
|
||||||
|
if (inFlight) {
|
||||||
|
return inFlight
|
||||||
|
}
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
keywords: buildQuery(),
|
keywords: buildQuery(),
|
||||||
maxResults: '3',
|
maxResults: '3',
|
||||||
|
|
@ -150,6 +184,7 @@ async function searchEBayForParts(
|
||||||
params.set('categoryId', categoryId)
|
params.set('categoryId', categoryId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const request = (async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/ebay/search?${params.toString()}`)
|
const response = await fetch(`/api/ebay/search?${params.toString()}`)
|
||||||
const body = await response.json().catch(() => null)
|
const body = await response.json().catch(() => null)
|
||||||
|
|
@ -171,6 +206,15 @@ async function searchEBayForParts(
|
||||||
error: error instanceof Error ? error.message : 'Failed to search eBay',
|
error: error instanceof Error ? error.message : 'Failed to search eBay',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
inFlightEbaySearches.set(requestKey, request)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await request
|
||||||
|
} finally {
|
||||||
|
inFlightEbaySearches.delete(requestKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const categorySearch = await searchViaApi('11700')
|
const categorySearch = await searchViaApi('11700')
|
||||||
|
|
@ -197,22 +241,105 @@ async function searchEBayForParts(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePartText(value: string): string {
|
||||||
|
return value.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSyntheticEbayListing(listing: PartForPage['ebayListings'][number]): boolean {
|
||||||
|
const itemId = listing.itemId?.trim() || ''
|
||||||
|
const viewItemUrl = listing.viewItemUrl?.trim() || ''
|
||||||
|
const imageUrl = listing.imageUrl?.trim() || ''
|
||||||
|
|
||||||
|
return (
|
||||||
|
imageUrl.includes('images.unsplash.com') ||
|
||||||
|
viewItemUrl.includes('123456789') ||
|
||||||
|
itemId.startsWith('123456789')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasLiveEbayListings(listings: PartForPage['ebayListings']): boolean {
|
||||||
|
return listings.some((listing) => !isSyntheticEbayListing(listing))
|
||||||
|
}
|
||||||
|
|
||||||
|
function scorePartForLiveSearch(part: PartForPage, index: number): number {
|
||||||
|
const partNumber = part.partNumber.trim()
|
||||||
|
const description = part.description.trim()
|
||||||
|
const lowerPartNumber = normalizePartText(partNumber)
|
||||||
|
const lowerDescription = normalizePartText(description)
|
||||||
|
|
||||||
|
if (!partNumber) {
|
||||||
|
return -1000
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GENERIC_PART_TERMS.has(lowerPartNumber)) {
|
||||||
|
return -500
|
||||||
|
}
|
||||||
|
|
||||||
|
let score = 0
|
||||||
|
|
||||||
|
// Prefer actual part numbers, model numbers, and revision codes over prose.
|
||||||
|
if (/\d/.test(partNumber)) {
|
||||||
|
score += 30
|
||||||
|
}
|
||||||
|
if (/[a-z]/i.test(partNumber) && /\d/.test(partNumber)) {
|
||||||
|
score += 20
|
||||||
|
}
|
||||||
|
if (/^[a-z0-9][a-z0-9\-._/ ]{2,24}$/i.test(partNumber)) {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
if (partNumber.length >= 4 && partNumber.length <= 20) {
|
||||||
|
score += 8
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description && description.length <= 80) {
|
||||||
|
score += 6
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description && !GENERIC_PART_TERMS.has(lowerDescription)) {
|
||||||
|
score += 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowerPartNumber.includes('rev') || lowerPartNumber.includes('figure')) {
|
||||||
|
score -= 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer earlier entries when scores are similar.
|
||||||
|
score -= index * 0.01
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectPartsForLiveSearch(parts: PartForPage[], limit: number): PartForPage[] {
|
||||||
|
return parts
|
||||||
|
.map((part, index) => ({
|
||||||
|
part,
|
||||||
|
score: scorePartForLiveSearch(part, index),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => b.score - a.score)
|
||||||
|
.slice(0, limit)
|
||||||
|
.map(({ part }) => part)
|
||||||
|
}
|
||||||
|
|
||||||
async function enhancePartsData(parts: PartForPage[]): Promise<{
|
async function enhancePartsData(parts: PartForPage[]): Promise<{
|
||||||
parts: PartForPage[]
|
parts: PartForPage[]
|
||||||
error?: string
|
error?: string
|
||||||
}> {
|
}> {
|
||||||
let firstError: string | undefined
|
let firstError: string | undefined
|
||||||
|
|
||||||
const enhancedParts = await Promise.all(parts.map(async (part) => {
|
const enhancedParts: PartForPage[] = []
|
||||||
// Only search for parts without existing eBay listings
|
|
||||||
if (part.ebayListings.length === 0) {
|
for (const part of parts) {
|
||||||
|
const shouldRefreshListings =
|
||||||
|
part.ebayListings.length === 0 || !hasLiveEbayListings(part.ebayListings)
|
||||||
|
|
||||||
|
if (shouldRefreshListings) {
|
||||||
const ebayResults = await searchEBayForParts(part.partNumber, part.description)
|
const ebayResults = await searchEBayForParts(part.partNumber, part.description)
|
||||||
|
|
||||||
if (ebayResults.error && !firstError) {
|
if (ebayResults.error && !firstError) {
|
||||||
firstError = ebayResults.error
|
firstError = ebayResults.error
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
enhancedParts.push({
|
||||||
...part,
|
...part,
|
||||||
ebayListings: ebayResults.results.map((result) => ({
|
ebayListings: ebayResults.results.map((result) => ({
|
||||||
itemId: result.itemId,
|
itemId: result.itemId,
|
||||||
|
|
@ -225,11 +352,12 @@ async function enhancePartsData(parts: PartForPage[]): Promise<{
|
||||||
shippingCost: result.shippingCost,
|
shippingCost: result.shippingCost,
|
||||||
affiliateLink: result.affiliateLink,
|
affiliateLink: result.affiliateLink,
|
||||||
})),
|
})),
|
||||||
}
|
})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return part
|
enhancedParts.push(part)
|
||||||
}))
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
parts: enhancedParts,
|
parts: enhancedParts,
|
||||||
|
|
@ -316,11 +444,7 @@ async function getPartsForManualWithStatus(
|
||||||
const { manualParts } = await loadPartsData()
|
const { manualParts } = await loadPartsData()
|
||||||
const parts = findManualParts(manualFilename, manualParts)
|
const parts = findManualParts(manualFilename, manualParts)
|
||||||
|
|
||||||
if (parts.length === 0) {
|
return { parts }
|
||||||
return { parts: [] }
|
|
||||||
}
|
|
||||||
|
|
||||||
return enhancePartsData(parts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -361,10 +485,25 @@ export async function getTopPartsForManual(
|
||||||
parts: PartForPage[]
|
parts: PartForPage[]
|
||||||
error?: string
|
error?: string
|
||||||
}> {
|
}> {
|
||||||
const { parts, error } = await getPartsForManualWithStatus(manualFilename)
|
const { parts } = await getPartsForManualWithStatus(manualFilename)
|
||||||
|
|
||||||
// Sort by number of eBay listings (more listings = more relevant)
|
if (parts.length === 0) {
|
||||||
const sorted = parts.sort((a, b) => b.ebayListings.length - a.ebayListings.length)
|
return { parts: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const liveSearchCandidates = selectPartsForLiveSearch(parts, Math.max(limit * 2, limit))
|
||||||
|
const { parts: enrichedParts, error } = await enhancePartsData(liveSearchCandidates)
|
||||||
|
|
||||||
|
const sorted = enrichedParts.sort((a, b) => {
|
||||||
|
const aHasLiveListings = hasLiveEbayListings(a.ebayListings) ? 1 : 0
|
||||||
|
const bHasLiveListings = hasLiveEbayListings(b.ebayListings) ? 1 : 0
|
||||||
|
|
||||||
|
if (aHasLiveListings !== bHasLiveListings) {
|
||||||
|
return bHasLiveListings - aHasLiveListings
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.ebayListings.length - a.ebayListings.length
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
parts: sorted.slice(0, limit),
|
parts: sorted.slice(0, limit),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue