"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, } 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" 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.slice(0, 3).map((name, index) => ( {name} ))} {manual.commonNames.length > 3 && ( +{manual.commonNames.length - 3} more )}
)} {manual.searchTerms && manual.searchTerms.length > 0 && !manual.commonNames && (
{manual.searchTerms.slice(0, 4).map((term, index) => ( {term} ))} {manual.searchTerms.length > 4 && ( +{manual.searchTerms.length - 4} more )}
)}
{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 */}

Start With Search

Find the manual first, then narrow it down

Search by model, manufacturer, or category. Use filters if you already know the brand or want manuals with parts.

Showing {filteredManuals.length} of{" "} {manuals.length} manuals
{/* 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:
View the full library grouped by manufacturer or switch to list view for a faster scan.
{/* 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}

{Object.values(group.categories).flat().length} manuals available from this 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) => ( ))}
))}
)}
) })}
) : ( /* List View */
{filteredManuals.map((manual) => ( ))}
)} {/* PDF Viewer Modal */} {viewingManual && ( setViewingManual(null)} /> )}
) }