'use client' import { useState, useMemo, useEffect, useCallback } from 'react' import Image from 'next/image' import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { PublicInset, PublicSurface } from '@/components/public-surface' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Download, Search, Filter, X, Coffee, Utensils, Package, Droplet, Eye, ShoppingCart, LayoutGrid, List, ExternalLink, Loader2, AlertCircle } from 'lucide-react' import type { Manual, ManualGroup } from '@/lib/manuals-types' import { getManualUrl, getThumbnailUrl } from '@/lib/manuals-types' import { getMachineTypeInfo, getAllMachineTypeNames } from '@/lib/manuals-config' import { ManualViewer } from '@/components/manual-viewer' import { getManualsWithParts } from '@/lib/parts-lookup' import { ebayClient } from '@/lib/ebay-api' // Product Suggestion Component interface ProductSuggestion { itemId: string title: string price: string currency: string imageUrl?: string viewItemUrl: string affiliateLink: string condition?: string } interface ProductSuggestionsProps { manual: Manual className?: string } function ProductSuggestions({ manual, className = '' }: ProductSuggestionsProps) { const [suggestions, setSuggestions] = useState([]) const [isLoading, setIsLoading] = useState(ebayClient.isConfigured()) const [error, setError] = useState(null) useEffect(() => { if (!ebayClient.isConfigured()) { setIsLoading(false) return } async function loadSuggestions() { setIsLoading(true) setError(null) try { // Generate search query from manual content const query = `${manual.manufacturer} ${manual.category} vending machine` const results = await ebayClient.searchItems({ keywords: query, maxResults: 6, sortOrder: 'BestMatch', }) setSuggestions(results) } catch (err) { console.error('Error loading product suggestions:', err) setError('Could not load product suggestions') } finally { setIsLoading(false) } } if (manual) { loadSuggestions() } }, [manual]) if (!ebayClient.isConfigured()) { return null } if (isLoading) { return (
) } if (error) { return (
{error}
) } if (suggestions.length === 0) { return (
No products found in sandbox environment
) } return (

Related Products

{suggestions.map((product) => (
{/* Image */} {product.imageUrl && (
{product.title} { e.currentTarget.src = `https://via.placeholder.com/120x80/fbbf24/1f2937?text=${encodeURIComponent(product.title)}` }} />
)} {!product.imageUrl && (
No Image
)} {/* Product Details */}
{product.title}
{product.price}
{product.condition && (
{product.condition}
)}
))}
) } interface ManualsPageClientProps { manuals: Manual[] groupedManuals: ManualGroup[] manufacturers: string[] categories: string[] } interface ManualCardProps { manual: Manual onView: (manual: { url: string; filename: string }) => void manualHasParts: (manual: Manual) => boolean getCategoryBadgeClass: (category: string) => string formatFileSize: (bytes?: number) => string showManufacturer?: boolean } function ManualCard({ manual, onView, manualHasParts, getCategoryBadgeClass, formatFileSize, showManufacturer = false, }: ManualCardProps) { const thumbnailUrl = getThumbnailUrl(manual) return ( {thumbnailUrl && (
{manual.filename.replace(/\.pdf$/i,
)} {manual.filename.replace(/\.pdf$/i, '')} {manual.commonNames && manual.commonNames.length > 0 && (
{manual.commonNames.map((name, index) => ( {name} ))}
)} {manual.searchTerms && manual.searchTerms.length > 0 && !manual.commonNames && (
{manual.searchTerms.map((term, index) => ( {term} ))}
)}
{manual.category} {manualHasParts(manual) && ( Has Parts )}
{showManufacturer && (

Manufacturer: {manual.manufacturer}

)} {manual.size && (

Size: {formatFileSize(manual.size)}

)}
) } export function ManualsPageClient({ manuals, groupedManuals, manufacturers, categories, }: ManualsPageClientProps) { const [searchTerm, setSearchTerm] = useState('') const [selectedManufacturer, setSelectedManufacturer] = useState('') const [selectedCategory, setSelectedCategory] = useState('') const [hasPartsOnly, setHasPartsOnly] = useState(false) const [viewMode, setViewMode] = useState<'grouped' | 'list'>('grouped') const [viewingManual, setViewingManual] = useState<{ url: string; filename: string } | null>(null) const [manualsWithParts, setManualsWithParts] = useState>(new Set()) const [partsLoading, setPartsLoading] = useState(true) // Load manuals with parts on mount useEffect(() => { getManualsWithParts().then(set => { setManualsWithParts(set) setPartsLoading(false) }).catch(() => { setPartsLoading(false) }) }, []) // Helper function to check if a manual has parts const manualHasParts = useCallback((manual: Manual): boolean => { if (manualsWithParts.size === 0) return false const filename = manual.filename return manualsWithParts.has(filename) || manualsWithParts.has(filename.toLowerCase()) || manualsWithParts.has(filename.replace(/\.pdf$/i, '')) || manualsWithParts.has(filename.replace(/\.pdf$/i, '').toLowerCase()) }, [manualsWithParts]) // Filter manuals based on search and filters const filteredManuals = useMemo(() => { let filtered = manuals if (selectedManufacturer) { filtered = filtered.filter((m) => m.manufacturer === selectedManufacturer) } if (selectedCategory) { filtered = filtered.filter((m) => m.category === selectedCategory) } if (hasPartsOnly) { filtered = filtered.filter((m) => manualHasParts(m)) } if (searchTerm) { const search = searchTerm.toLowerCase() filtered = filtered.filter( (m) => m.filename.toLowerCase().includes(search) || m.manufacturer.toLowerCase().includes(search) || m.category.toLowerCase().includes(search) || (m.commonNames && m.commonNames.some(name => name.toLowerCase().includes(search))) || (m.searchTerms && m.searchTerms.some(term => term.includes(search))) ) } return filtered }, [manuals, searchTerm, selectedManufacturer, selectedCategory, hasPartsOnly, manualHasParts]) // Get filtered categories based on selected manufacturer const filteredCategories = useMemo(() => { if (!selectedManufacturer) { return categories } const manufacturerManuals = manuals.filter(m => m.manufacturer === selectedManufacturer) const uniqueCategories = new Set(manufacturerManuals.map(m => m.category)) return Array.from(uniqueCategories) }, [manuals, selectedManufacturer, categories]) // Filter grouped manuals const filteredGroupedManuals = useMemo(() => { if (!selectedManufacturer && !selectedCategory && !searchTerm && !hasPartsOnly) { return groupedManuals } return groupedManuals .filter((group) => { if (selectedManufacturer && group.manufacturer !== selectedManufacturer) { return false } return true }) .map((group) => { const filteredCategories: { [key: string]: Manual[] } = {} for (const [category, categoryManuals] of Object.entries(group.categories)) { if (selectedCategory && category !== selectedCategory) { continue } const filtered = categoryManuals.filter((manual) => { if (hasPartsOnly && !manualHasParts(manual)) { return false } if (searchTerm) { const search = searchTerm.toLowerCase() return ( manual.filename.toLowerCase().includes(search) || manual.manufacturer.toLowerCase().includes(search) || manual.category.toLowerCase().includes(search) || (manual.commonNames && manual.commonNames.some(name => name.toLowerCase().includes(search))) || (manual.searchTerms && manual.searchTerms.some(term => term.includes(search))) ) } return true }) if (filtered.length > 0) { filteredCategories[category] = filtered } } return { ...group, categories: filteredCategories, } }) .filter((group) => Object.keys(group.categories).length > 0) }, [groupedManuals, selectedManufacturer, selectedCategory, searchTerm, hasPartsOnly, manualHasParts]) const hasActiveFilters = selectedManufacturer || selectedCategory || searchTerm || hasPartsOnly const clearFilters = () => { setSearchTerm('') setSelectedManufacturer('') setSelectedCategory('') setHasPartsOnly(false) } const formatFileSize = (bytes?: number): string => { if (!bytes) return 'Unknown size' if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } // Organize categories by machine type first, then others const organizeCategories = (categories: { [key: string]: Manual[] }) => { const machineTypes = getAllMachineTypeNames() const machineTypeCategories: { [key: string]: Manual[] } = {} const otherCategories: { [key: string]: Manual[] } = {} for (const [category, manuals] of Object.entries(categories)) { if (machineTypes.includes(category)) { machineTypeCategories[category] = manuals } else { otherCategories[category] = manuals } } // Sort machine types by priority order const sortedMachineTypes = machineTypes .filter((type) => machineTypeCategories[type]) .map((type) => [type, machineTypeCategories[type]] as [string, Manual[]]) // Sort other categories alphabetically const sortedOthers = Object.entries(otherCategories).sort(([a], [b]) => a.localeCompare(b) ) return { machineTypes: sortedMachineTypes, others: sortedOthers } } // Get icon for machine type const getCategoryIcon = (category: string) => { const machineType = getMachineTypeInfo(category) if (!machineType) return null switch (category) { case 'Coffee': return case 'Food': return case 'Beverage': return case 'Snack': return default: return null } } // Get badge color for category const getCategoryBadgeClass = (category: string): string => { const machineType = getMachineTypeInfo(category) const categoryLower = category.toLowerCase() // Check for common category patterns if (categoryLower.includes('snack')) { return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200' } if (categoryLower.includes('beverage') || categoryLower.includes('drink')) { return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' } if (categoryLower.includes('frozen')) { return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900 dark:text-indigo-200' } if (categoryLower.includes('combo')) { return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' } if (categoryLower.includes('coffee') || categoryLower.includes('hot')) { return 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200' } if (categoryLower.includes('food')) { return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' } if (categoryLower.includes('bulk')) { return 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200' } if (categoryLower.includes('ice') || categoryLower.includes('cream')) { return 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900 dark:text-cyan-200' } if (categoryLower.includes('service') || categoryLower.includes('repair')) { return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' } if (categoryLower.includes('parts')) { return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' } if (categoryLower.includes('operator') || categoryLower.includes('user')) { return 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200' } if (categoryLower.includes('installation') || categoryLower.includes('setup')) { return 'bg-lime-100 text-lime-800 dark:bg-lime-900 dark:text-lime-200' } // Fallback for machine types if (machineType) { switch (category) { case 'Snack': return 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200' case 'Beverage': return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' case 'Combo': return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200' case 'Coffee': return 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200' case 'Food': return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' case 'Bulk': return 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200' case 'Ice Cream': return 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900 dark:text-cyan-200' default: return 'bg-muted text-muted-foreground' } } return 'bg-primary/[0.08] text-foreground' } return (
{/* Search and Filter Controls */}
{/* Search Bar */}
setSearchTerm(e.target.value)} className="h-12 rounded-xl border-border/70 bg-white pl-10 shadow-sm" />
{/* Filters Row */}
Filters:
{selectedManufacturer && ( Applied )}
{selectedCategory && ( Applied )}
setHasPartsOnly(checked === true)} /> {hasPartsOnly && ( Applied )} {hasActiveFilters && ( )}
{/* View Mode Toggle and Results Count */}
View:
Showing {filteredManuals.length} of {manuals.length}{' '} manuals
{/* Manuals Display */} {filteredManuals.length === 0 ? (

No manuals found

Try adjusting your search or filters to find what you're looking for.

) : viewMode === 'grouped' ? ( /* Grouped View */
{filteredGroupedManuals.map((group) => { const organized = organizeCategories(group.categories) return (

{group.manufacturer}

{/* Machine Type Categories First */} {organized.machineTypes.length > 0 && (
{organized.machineTypes.map(([category, categoryManuals]) => { const machineTypeInfo = getMachineTypeInfo(category) const icon = getCategoryIcon(category) return (
{icon && {icon}}

{category}

{categoryManuals.length} {categoryManuals.length === 1 ? 'manual' : 'manuals'} {machineTypeInfo && ( {machineTypeInfo.description} )}
{categoryManuals.map((manual) => ( ))}
) })}
)} {/* Other Categories (Model Numbers, Document Types, etc.) */} {organized.others.length > 0 && (
{organized.machineTypes.length > 0 && (

Models & Other Documents

)} {organized.others.map(([category, categoryManuals]) => (

{category}

({categoryManuals.length} {categoryManuals.length === 1 ? 'manual' : 'manuals'})
{categoryManuals.map((manual) => ( ))}
))}
)} {/* Product Suggestions Section */} {filteredManuals.length > 0 && (

Related Products

{filteredManuals.slice(0, 3).map((manual) => ( ))}
)}
) })}
) : ( /* List View */
{filteredManuals.map((manual) => ( ))}
)} {/* PDF Viewer Modal */} {viewingManual && ( setViewingManual(null)} /> )}
) }