330 lines
12 KiB
TypeScript
330 lines
12 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import {
|
|
X,
|
|
Download,
|
|
ExternalLink,
|
|
AlertCircle,
|
|
ShoppingCart,
|
|
} from "lucide-react"
|
|
import { Button } from "@/components/ui/button"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogDescription,
|
|
DialogTitle,
|
|
DialogClose,
|
|
} from "@/components/ui/dialog"
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from "@/components/ui/sheet"
|
|
import { PartsPanel } from "@/components/parts-panel"
|
|
import { useIsMobile } from "@/hooks/use-mobile"
|
|
|
|
interface ManualViewerProps {
|
|
manualUrl: string
|
|
filename: string
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
export function ManualViewer({
|
|
manualUrl,
|
|
filename,
|
|
isOpen,
|
|
onClose,
|
|
}: ManualViewerProps) {
|
|
const [pdfError, setPdfError] = useState(false)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [showPartsPanel, setShowPartsPanel] = useState(true)
|
|
const [partsPanelLoading, setPartsPanelLoading] = useState(true)
|
|
const [partsPanelVisible, setPartsPanelVisible] = useState(true)
|
|
const isMobile = useIsMobile()
|
|
|
|
// Reset error state when manual URL changes
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setPdfError(false)
|
|
setIsLoading(true)
|
|
setPartsPanelLoading(true)
|
|
setPartsPanelVisible(true)
|
|
}
|
|
}, [manualUrl, isOpen])
|
|
|
|
// The URL is already properly encoded in getManualUrl function
|
|
// Just use it as-is for the iframe src
|
|
const encodedUrl = manualUrl
|
|
|
|
const handleIframeLoad = () => {
|
|
setIsLoading(false)
|
|
}
|
|
|
|
const handleIframeError = () => {
|
|
setIsLoading(false)
|
|
setPdfError(true)
|
|
}
|
|
|
|
const showPartsPanelWithData =
|
|
showPartsPanel && (partsPanelLoading || partsPanelVisible)
|
|
const canToggleParts = partsPanelLoading || partsPanelVisible
|
|
const partsToggleLabel = partsPanelLoading
|
|
? "Checking Parts..."
|
|
: partsPanelVisible
|
|
? showPartsPanel
|
|
? "Hide Parts"
|
|
: "Show Parts"
|
|
: "Parts Unavailable"
|
|
|
|
// Mobile layout - use Sheet
|
|
if (isMobile) {
|
|
return (
|
|
<Sheet open={isOpen} onOpenChange={onClose}>
|
|
<SheetContent
|
|
side="bottom"
|
|
className="h-screen max-h-screen flex flex-col p-0 gap-0 [&>button]:hidden"
|
|
>
|
|
<SheetHeader className="px-4 pt-4 pb-3 border-b flex-shrink-0">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<SheetTitle className="text-base font-semibold line-clamp-1 flex-1 min-w-0">
|
|
{filename.replace(/\.pdf$/i, "")}
|
|
</SheetTitle>
|
|
<div className="flex items-center gap-1 flex-shrink-0">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const link = document.createElement("a")
|
|
link.href = encodedUrl
|
|
link.download = filename
|
|
link.click()
|
|
}}
|
|
className="h-8 px-2"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={onClose}
|
|
className="h-8 px-2"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</SheetHeader>
|
|
<div className="flex-1 overflow-hidden min-h-0 relative bg-muted/50">
|
|
{isLoading && !pdfError && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
|
|
<p className="text-sm text-muted-foreground">
|
|
Loading PDF...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{pdfError ? (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-background z-10 p-4">
|
|
<div className="text-center max-w-md">
|
|
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">
|
|
Unable to Load PDF
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
The PDF file could not be loaded. This may be due to file
|
|
corruption or an encoding issue.
|
|
</p>
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => window.open(encodedUrl, "_blank")}
|
|
className="w-full"
|
|
>
|
|
<ExternalLink className="h-4 w-4 mr-2" />
|
|
Try Opening in New Tab
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
const link = document.createElement("a")
|
|
link.href = encodedUrl
|
|
link.download = filename
|
|
link.click()
|
|
}}
|
|
className="w-full"
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Download Instead
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<iframe
|
|
src={`${encodedUrl}#toolbar=1&navpanes=0&scrollbar=1`}
|
|
className="w-full h-full border-0 min-h-full block"
|
|
title={filename}
|
|
onLoad={handleIframeLoad}
|
|
onError={handleIframeError}
|
|
/>
|
|
)}
|
|
</div>
|
|
</SheetContent>
|
|
</Sheet>
|
|
)
|
|
}
|
|
|
|
// Desktop layout - use Dialog
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent
|
|
className="!max-w-none !w-screen !h-screen !max-h-screen p-0 flex flex-col gap-0 !m-0 !rounded-none !border-0 !translate-x-0 !translate-y-0 !top-0 !left-0 !right-0 !bottom-0 !inset-0"
|
|
showCloseButton={false}
|
|
style={{
|
|
position: "fixed",
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
width: "100vw",
|
|
height: "100vh",
|
|
maxWidth: "100vw",
|
|
maxHeight: "100vh",
|
|
transform: "none",
|
|
margin: 0,
|
|
borderRadius: 0,
|
|
border: "none",
|
|
}}
|
|
>
|
|
<DialogHeader className="px-4 sm:px-6 pt-4 sm:pt-6 pb-3 sm:pb-4 border-b flex-shrink-0 bg-background">
|
|
<DialogDescription className="sr-only">
|
|
PDF viewer for {filename.replace(/\.pdf$/i, "")}. Use the actions to
|
|
open the manual in a new tab, download it, or browse available
|
|
parts.
|
|
</DialogDescription>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<DialogTitle className="text-base sm:text-lg font-semibold line-clamp-1 flex-1 min-w-0">
|
|
{filename.replace(/\.pdf$/i, "")}
|
|
</DialogTitle>
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setShowPartsPanel(!showPartsPanel)}
|
|
disabled={!canToggleParts}
|
|
>
|
|
<ShoppingCart className="h-4 w-4 mr-1" />
|
|
{partsToggleLabel}
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => window.open(encodedUrl, "_blank")}
|
|
>
|
|
<ExternalLink className="h-4 w-4 mr-1" />
|
|
Open in New Tab
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => {
|
|
const link = document.createElement("a")
|
|
link.href = encodedUrl
|
|
link.download = filename
|
|
link.click()
|
|
}}
|
|
>
|
|
<Download className="h-4 w-4 mr-1" />
|
|
Download
|
|
</Button>
|
|
<DialogClose asChild>
|
|
<Button variant="ghost" size="sm" className="flex-shrink-0">
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</DialogClose>
|
|
</div>
|
|
</div>
|
|
</DialogHeader>
|
|
<div className="flex-1 overflow-hidden min-h-0 relative bg-muted/50 flex flex-row w-full">
|
|
{/* PDF Viewer - responsive width based on parts panel */}
|
|
<div
|
|
className={`overflow-hidden min-h-0 relative transition-all duration-300 h-full ${
|
|
showPartsPanelWithData ? "w-[75%] lg:w-[80%]" : "w-full"
|
|
}`}
|
|
>
|
|
{isLoading && !pdfError && (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-background/80 z-10">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-2"></div>
|
|
<p className="text-sm text-muted-foreground">
|
|
Loading PDF...
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{pdfError ? (
|
|
<div className="absolute inset-0 flex items-center justify-center bg-background z-10">
|
|
<div className="text-center p-6 max-w-md">
|
|
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
|
|
<h3 className="text-lg font-semibold mb-2">
|
|
Unable to Load PDF
|
|
</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
The PDF file could not be loaded. This may be due to file
|
|
corruption or an encoding issue.
|
|
</p>
|
|
<div className="flex gap-2 justify-center">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => window.open(encodedUrl, "_blank")}
|
|
>
|
|
<ExternalLink className="h-4 w-4 mr-2" />
|
|
Try Opening in New Tab
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
const link = document.createElement("a")
|
|
link.href = encodedUrl
|
|
link.download = filename
|
|
link.click()
|
|
}}
|
|
>
|
|
<Download className="h-4 w-4 mr-2" />
|
|
Download Instead
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<iframe
|
|
src={`${encodedUrl}#toolbar=1&navpanes=0&scrollbar=1`}
|
|
className="w-full h-full border-0 min-h-full block"
|
|
title={filename}
|
|
onLoad={handleIframeLoad}
|
|
onError={handleIframeError}
|
|
/>
|
|
)}
|
|
</div>
|
|
{/* Parts Panel - right side, responsive width */}
|
|
{showPartsPanelWithData && (
|
|
<PartsPanel
|
|
manualFilename={filename}
|
|
className={`border-l border-yellow-300/20 bg-yellow-50 dark:bg-yellow-950/90 overflow-y-auto h-full ${"w-[25%] lg:w-[20%]"}`}
|
|
onStateChange={(state) => {
|
|
setPartsPanelLoading(state.isLoading)
|
|
setPartsPanelVisible(state.isVisible)
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|