diff --git a/app/(main)/pdf-viewer/page.tsx b/app/(main)/pdf-viewer/page.tsx new file mode 100644 index 0000000..bff4ab3 --- /dev/null +++ b/app/(main)/pdf-viewer/page.tsx @@ -0,0 +1,5 @@ +import { PDFViewer } from '@/components/pdf-viewer'; + +export default function PDFViewerPage() { + return ; +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 4140892..9997c6f 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,6 +2,30 @@ @tailwind components; @tailwind utilities; +/* PDF Viewer specific styles */ +.react-pdf__Page { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.react-pdf__Page__canvas { + display: block; + margin: 0 auto; +} + +.react-pdf__Document { + display: flex; + flex-direction: column; + align-items: center; +} + +/* Line clamp utility for truncating text */ +.line-clamp-2 { + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + @layer base { :root { --background: 0 0% 100%; diff --git a/components/pdf-viewer.tsx b/components/pdf-viewer.tsx new file mode 100644 index 0000000..d9c7aa7 --- /dev/null +++ b/components/pdf-viewer.tsx @@ -0,0 +1,272 @@ +"use client"; + +import React, { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { ChevronLeft, ChevronRight, Upload, FileText } from 'lucide-react'; +import { extractPDFSections, highlightPDFSection, type PDFSection } from '@/lib/pdf-utils'; +import dynamic from 'next/dynamic'; + +// Dynamically import PDF components to avoid SSR issues +const Document = dynamic( + () => import('react-pdf').then((mod) => mod.Document), + { ssr: false } +); + +const Page = dynamic( + () => import('react-pdf').then((mod) => mod.Page), + { ssr: false } +); + +// Set up PDF.js worker on client side only +if (typeof window !== 'undefined') { + import('react-pdf').then((reactPdf) => { + reactPdf.pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${reactPdf.pdfjs.version}/build/pdf.worker.min.js`; + }); +} + +export function PDFViewer() { + const [file, setFile] = useState(null); + const [numPages, setNumPages] = useState(null); + const [pageNumber, setPageNumber] = useState(1); + const [selectedSection, setSelectedSection] = useState(null); + const [sections, setSections] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + // Load sections when component mounts + const loadedSections = extractPDFSections(); + setSections(loadedSections); + }, []); + + function onDocumentLoadSuccess({ numPages }: { numPages: number }) { + setNumPages(numPages); + setLoading(false); + setError(null); + } + + function onDocumentLoadError(error: Error) { + setError(`Failed to load PDF: ${error.message}`); + setLoading(false); + } + + function onFileChange(event: React.ChangeEvent) { + const selectedFile = event.target.files?.[0]; + if (selectedFile && selectedFile.type === 'application/pdf') { + setFile(selectedFile); + setPageNumber(1); + setSelectedSection(null); + setLoading(true); + setError(null); + } + } + + function goToPrevPage() { + setPageNumber(prev => Math.max(prev - 1, 1)); + } + + function goToNextPage() { + setPageNumber(prev => Math.min(prev + 1, numPages || 1)); + } + + function handleSectionClick(sectionId: string, page?: number) { + setSelectedSection(selectedSection === sectionId ? null : sectionId); + if (page && page !== pageNumber) { + setPageNumber(page); + } + // Highlight the section in the PDF (conceptual for now) + if (page) { + highlightPDFSection(sectionId, page); + } + } + + const selectedSectionData = sections.find(section => section.id === selectedSection); + + return ( +
+
+
+

PDF Document Viewer

+ + {!file && ( + + +
+ +
+

Upload PDF Document

+

+ Select a PDF file to view its content and navigate through sections +

+
+ + +
+
+
+
+
+ )} +
+ +
+ {/* Left Panel - PDF Preview */} +
+ + + + + PDF Preview + {file && ( + + ({file.name}) + + )} + + + + {file ? ( +
+ {error && ( +
+ {error} +
+ )} + {loading && ( +
+
Loading PDF...
+
+ )} + {!error && !loading && ( +
+ + + +
+ )} + + {numPages && ( +
+ + + + Page {pageNumber} of {numPages} + + + +
+ )} +
+ ) : ( +
+
+ +

No PDF selected

+

Upload a PDF to view it here

+
+
+ )} +
+
+
+ + {/* Right Panel - Sections List */} +
+ + + Document Sections + + + +
+ {sections.map((section, index) => ( +
handleSectionClick(section.id, section.page)} + > +

+ {section.title} +

+

+ {section.content} +

+ {section.page && ( +

+ Page {section.page} +

+ )} +
+ ))} +
+
+
+
+ + {/* Selected Section Details */} + {selectedSectionData && ( + + + + {selectedSectionData.title} + + + +
+

+ {selectedSectionData.content} +

+ {selectedSectionData.page && ( +
+ + Located on page {selectedSectionData.page} +
+ )} +
+
+
+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/sections/hero.tsx b/components/sections/hero.tsx index 8f763bc..fa900d9 100644 --- a/components/sections/hero.tsx +++ b/components/sections/hero.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { ArrowRight, Github } from "lucide-react"; +import { ArrowRight, Github, FileText } from "lucide-react"; export function Hero() { return ( @@ -24,6 +24,9 @@ export function Hero() { + ); } diff --git a/lib/pdf-utils.ts b/lib/pdf-utils.ts new file mode 100644 index 0000000..5aa36e0 --- /dev/null +++ b/lib/pdf-utils.ts @@ -0,0 +1,78 @@ +export interface PDFSection { + id: string; + title: string; + content: string; + page?: number; +} + +// This function would ideally extract sections from actual PDF text content +// For now, it returns sample sections that match the user's requirements +export function extractPDFSections(pdfText?: string): PDFSection[] { + // In a real implementation, this would parse the PDF text and extract sections + // based on patterns like "1. PROPERTY DESCRIPTION:", "2. LEGAL DESCRIPTION:", etc. + + const sampleSections: PDFSection[] = [ + { + id: '1', + title: '1. PROPERTY DESCRIPTION:', + content: 'This section contains detailed information about the property including location, size, and key characteristics. The property is located at 123 Main Street and encompasses approximately 2.5 acres of residential land with mature landscaping and established utilities.', + page: 1 + }, + { + id: '2', + title: '2. LEGAL DESCRIPTION:', + content: 'Legal boundaries and official property description as recorded in the county records. Lot 1, Block 2, Sample Subdivision, as recorded in Plat Book 15, Page 42, County Records. The property is legally described with precise metes and bounds measurements.', + page: 1 + }, + { + id: '3', + title: '3. ZONING INFORMATION:', + content: 'Current zoning classification and permitted uses. The property is zoned R-1 Single Family Residential, allowing for single-family homes and accessory structures. Building setbacks are 25 feet from front, 15 feet from rear, and 10 feet from side property lines.', + page: 1 + }, + { + id: '4', + title: '4. UTILITIES AND SERVICES:', + content: 'Available utilities including water, sewer, electricity, gas, and telecommunications. All major utilities are available at the street with established service connections. Municipal water and sewer services are provided by the city.', + page: 1 + }, + { + id: '5', + title: '5. ENVIRONMENTAL CONSIDERATIONS:', + content: 'Environmental assessments, flood zones, and any environmental restrictions or considerations. The property is located outside of designated flood zones with no known environmental hazards. Soil conditions are suitable for construction.', + page: 1 + }, + { + id: '6', + title: '6. ACCESS AND TRANSPORTATION:', + content: 'Property access, road conditions, and transportation infrastructure. The property has direct access to Main Street, a paved public road maintained by the city. Public transportation is available within 0.5 miles.', + page: 1 + }, + { + id: '7', + title: '7. NEIGHBORHOOD CHARACTERISTICS:', + content: 'Information about the surrounding area, nearby amenities, and community features. The property is located in a well-established residential neighborhood with parks, schools, and shopping within walking distance.', + page: 1 + } + ]; + + return sampleSections; +} + +// Function to highlight text within a PDF page (conceptual) +export function highlightPDFSection(sectionId: string, pageNumber: number): void { + // In a real implementation, this would interact with the PDF.js API + // to highlight specific text regions on the PDF page + console.log(`Highlighting section ${sectionId} on page ${pageNumber}`); +} + +// Function to extract text content from PDF (would use PDF.js in real implementation) +export async function extractPDFText(file: File): Promise { + // This is a placeholder - in a real implementation, you would use PDF.js + // to extract text content from the PDF file + return new Promise((resolve) => { + setTimeout(() => { + resolve("Sample PDF text content..."); + }, 1000); + }); +} \ No newline at end of file diff --git a/package.json b/package.json index 6e74d74..4c3d7b0 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,12 @@ "lucide-react": "^0.454.0", "next": "14.2.16", "next-themes": "latest", + "pdfjs-dist": "^5.4.54", "react": "^18", "react-day-picker": "latest", "react-dom": "^18", "react-hook-form": "latest", + "react-pdf": "^10.0.1", "react-resizable-panels": "latest", "recharts": "latest", "sonner": "latest", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 493f8a2..833fd3e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: next-themes: specifier: latest version: 0.4.6(react-dom@18.0.0(react@18.0.0))(react@18.0.0) + pdfjs-dist: + specifier: ^5.4.54 + version: 5.4.54 react: specifier: ^18 version: 18.0.0 @@ -122,6 +125,9 @@ importers: react-hook-form: specifier: latest version: 7.60.0(react@18.0.0) + react-pdf: + specifier: ^10.0.1 + version: 10.0.1(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0) react-resizable-panels: specifier: latest version: 3.0.3(react-dom@18.0.0(react@18.0.0))(react@18.0.0) @@ -261,6 +267,70 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@napi-rs/canvas-android-arm64@0.1.76': + resolution: {integrity: sha512-7EAfkLBQo2QoEzpHdInFbfEUYTXsiO2hvtFo1D9zfTzcQM8n5piZdOpJ3EIkmpe8yLoSV8HLyUQtq4bv11x6Tg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.76': + resolution: {integrity: sha512-Cs8WRMzaWSJWeWY8tvnCe+TuduHUbB0xFhZ0FmOrNy2prPxT4A6aU3FQu8hR9XJw8kKZ7v902wzaDmy9SdhG8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.76': + resolution: {integrity: sha512-ya+T6gV9XAq7YAnMa2fKhWXAuRR5cpRny2IoHacoMxgtOARnUkJO/k3hIb52FtMoq7UxLi5+IFGVHU6ZiMu4Ag==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.76': + resolution: {integrity: sha512-fgnPb+FKVuixACvkHGldJqYXExORBwvqGgL0K80uE6SGH2t0UKD2auHw2CtBy14DUzfg82PkupO2ix2w7kB+Xw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.76': + resolution: {integrity: sha512-r8OxIenvBPOa4I014k1ZWTCz2dB0ZTsxMP7+ovMOKO7jkl1Z+YZo2OTAqxArpMhN0wdEeI3Lw9zUcn2HgwEgDA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.76': + resolution: {integrity: sha512-smxwzKfHYaOYG7QXUuDPrFEC7WqjL3Lx4AM6mk8/FxDAS+8o0eoZJwSu+zXsaBLimEQUozEYgEGtJ2JJ0RdL4A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.76': + resolution: {integrity: sha512-G2PsFwsP+r4syEoNLStV3n1wtNAClwf8s/qB57bexG08R4f4WaiBd+x+d4iYS0Y5o90YIEm8/ewZn4bLIa0wNQ==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.76': + resolution: {integrity: sha512-SNK+vgge4DnuONYdYE3Y09LivGgUiUPQDU+PdGNZJIzIi0hRDLcA59eag8LGeQfPmJW84c1aZD04voihybKFog==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.76': + resolution: {integrity: sha512-tWHLBI9iVoR1NsfpHz1MGERTkqcca8akbH/CzX6JQUNC+lJOeYYXeRuK8hKqMIg1LI+4QOMAtHNVeZu8NvjEug==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-x64-msvc@0.1.76': + resolution: {integrity: sha512-ifM5HOGw2hP5QLQzCB41Riw3Pq5yKAAjZpn+lJC0sYBmyS2s/Kq6KpTOKxf0CuptkI1wMcRcYQfhLRdeWiYvIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.76': + resolution: {integrity: sha512-YIk5okeNN53GzjvWmAyCQFE9xrLeQXzYpudX4TiLvqaz9SqXgIgxIuKPe4DKyB5nccsQMIev7JGKTzZaN5rFdw==} + engines: {node: '>= 10'} + '@next/env@14.2.16': resolution: {integrity: sha512-fLrX5TfJzHCbnZ9YUSnGW63tMV3L4nSfhgOQ0iCcX21Pt+VSTDuaLsSuL8J/2XAiVA5AnzvXDpf6pMs60QxOag==} @@ -1372,6 +1442,20 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + make-cancellable-promise@2.0.0: + resolution: {integrity: sha512-3SEQqTpV9oqVsIWqAcmDuaNeo7yBO3tqPtqGRcKkEo0lrzD3wqbKG9mkxO65KoOgXqj+zH2phJ2LiAsdzlogSw==} + + make-event-props@2.0.0: + resolution: {integrity: sha512-G/hncXrl4Qt7mauJEXSg3AcdYzmpkIITTNl5I+rH9sog5Yw0kK6vseJjCaPfOXqOqQuPUP89Rkhfz5kPS8ijtw==} + + merge-refs@2.0.0: + resolution: {integrity: sha512-3+B21mYK2IqUWnd2EivABLT7ueDhb0b8/dGK8LoFQPrU61YITeCMn14F7y7qZafWNZhUEKb24cJdiT5Wxs3prg==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1456,6 +1540,14 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + pdfjs-dist@5.3.31: + resolution: {integrity: sha512-EhPdIjNX0fcdwYQO+e3BAAJPXt+XI29TZWC7COhIXs/K0JHcUt1Gdz1ITpebTwVMFiLsukdUZ3u0oTO7jij+VA==} + engines: {node: '>=20.16.0 || >=22.3.0'} + + pdfjs-dist@5.4.54: + resolution: {integrity: sha512-TBAiTfQw89gU/Z4LW98Vahzd2/LoCFprVGvGbTgFt+QCB1F+woyOPmNNVgLa6djX9Z9GGTnj7qE1UzpOVJiINw==} + engines: {node: '>=20.16.0 || >=22.3.0'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1545,6 +1637,16 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-pdf@10.0.1: + resolution: {integrity: sha512-dblLZ5BqubeNFZwTzHkRi7Rexev2+Xb9z7sEvNYT+IOxmih1KgrByeqD4Hm0RnwR9nTWludXbiHQMe9d47DK2w==} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-redux@9.2.0: resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: @@ -1795,6 +1897,9 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1997,6 +2102,50 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} + '@napi-rs/canvas-android-arm64@0.1.76': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.76': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.76': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.76': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.76': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.76': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.76': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.76': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.76': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.76': + optional: true + + '@napi-rs/canvas@0.1.76': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.76 + '@napi-rs/canvas-darwin-arm64': 0.1.76 + '@napi-rs/canvas-darwin-x64': 0.1.76 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.76 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.76 + '@napi-rs/canvas-linux-arm64-musl': 0.1.76 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.76 + '@napi-rs/canvas-linux-x64-gnu': 0.1.76 + '@napi-rs/canvas-linux-x64-musl': 0.1.76 + '@napi-rs/canvas-win32-x64-msvc': 0.1.76 + optional: true + '@next/env@14.2.16': {} '@next/swc-darwin-arm64@14.2.16': @@ -3091,6 +3240,14 @@ snapshots: lz-string@1.5.0: {} + make-cancellable-promise@2.0.0: {} + + make-event-props@2.0.0: {} + + merge-refs@2.0.0(@types/react@18.0.0): + optionalDependencies: + '@types/react': 18.0.0 + merge2@1.4.1: {} micromatch@4.0.8: @@ -3165,6 +3322,14 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + pdfjs-dist@5.3.31: + optionalDependencies: + '@napi-rs/canvas': 0.1.76 + + pdfjs-dist@5.4.54: + optionalDependencies: + '@napi-rs/canvas': 0.1.76 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3243,6 +3408,21 @@ snapshots: react-is@18.3.1: {} + react-pdf@10.0.1(@types/react@18.0.0)(react-dom@18.0.0(react@18.0.0))(react@18.0.0): + dependencies: + clsx: 2.1.1 + dequal: 2.0.3 + make-cancellable-promise: 2.0.0 + make-event-props: 2.0.0 + merge-refs: 2.0.0(@types/react@18.0.0) + pdfjs-dist: 5.3.31 + react: 18.0.0 + react-dom: 18.0.0(react@18.0.0) + tiny-invariant: 1.3.3 + warning: 4.0.3 + optionalDependencies: + '@types/react': 18.0.0 + react-redux@9.2.0(@types/react@18.0.0)(react@18.0.0)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 @@ -3511,6 +3691,10 @@ snapshots: w3c-keyname@2.2.8: {} + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/public/sample-document.pdf b/public/sample-document.pdf new file mode 100644 index 0000000..92d2b9e Binary files /dev/null and b/public/sample-document.pdf differ