This commit is contained in:
David Arranz 2024-08-19 20:55:36 +02:00
parent f85f40311a
commit e2938594d2
8 changed files with 147 additions and 131 deletions

View File

@ -16,7 +16,8 @@ import {
} from "@/ui";
import { IListQuotes_Response_DTO } from "@shared/contexts";
import { t } from "i18next";
import { CopyIcon, MoreVerticalIcon, TruckIcon } from "lucide-react";
import { DownloadIcon, MoreVerticalIcon, PrinterIcon } from "lucide-react";
import printJS from "print-js";
import { useMemo } from "react";
import { Trans } from "react-i18next";
import { useNavigate } from "react-router-dom";
@ -54,17 +55,17 @@ export const QuotePDFPreview = ({
if (reportIsLoading || reportIsPending || reportIsFetching) {
return (
<Card className={cn("overflow-hidden", className)}>
<CardHeader className='flex flex-row items-start'>
<CardTitle className='flex items-center gap-2 text-lg group'>
<Skeleton className='w-[250px] h-8 bg-card' />
<Card className={cn("overflow-hidden flex flex-col", className)}>
<CardHeader>
<CardTitle>
<Skeleton className='w-full h-8' />
</CardTitle>
<CardDescription>
<Skeleton className='w-full h-8' />
</CardDescription>
</CardHeader>
<CardContent className='flex content-center px-4 py-6 text-center'>
<Skeleton className='object-contain w-full overflow-hidden bg-white border-2 border-dashed rounded-lg min-h-min' />
<CardContent className='py-4'>
<Skeleton className='w-full aspect-[3/4] relative bg-white shadow flex-1' />
</CardContent>
</Card>
);
@ -74,20 +75,33 @@ export const QuotePDFPreview = ({
<Card className={cn("overflow-hidden", className)}>
<CardHeader className='bg-muted/50'>
<CardTitle className='flex items-center gap-2 text-lg group'>
{t("quotes.list.quote")}
<Button
size='icon'
variant='outline'
className='w-6 h-6 transition-opacity opacity-0 group-hover:opacity-100'
>
<CopyIcon className='w-3 h-3' />
<span className='sr-only'>Copy Order ID</span>
</Button>
{t("quotes.list.preview.quote")}
<div className='flex items-center gap-1 ml-auto'>
<Button
size='sm'
variant='outline'
className='h-8 gap-1'
onClick={(e) => {
e.preventDefault();
printJS({
printable: file,
type: "pdf",
showModal: false,
modalMessage: "Cargando...",
});
}}
>
<PrinterIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
{t("common.print")}
</span>
</Button>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<TruckIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>Track Order</span>
<DownloadIcon className='h-3.5 w-3.5' />
<span className='lg:sr-only xl:not-sr-only xl:whitespace-nowrap'>
{t("quotes.list.preview.download_quote")}
</span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -108,11 +122,11 @@ export const QuotePDFPreview = ({
<Trans i18nKey={"common.edit"} />
</DropdownMenuItem>
<DropdownMenuItem>
<Trans i18nKey={"common.download"} />
<Trans i18nKey={"common.duplicate"} />
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Trans i18nKey={"common.remove"} />
<Trans i18nKey={"common.archive"} />
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -123,19 +137,8 @@ export const QuotePDFPreview = ({
{quote?.date.toString()}
</CardDescription>
</CardHeader>
<CardContent>
<PDFViewer
file={file}
className='object-contain my-6 overflow-hidden bg-white border-dashed rounded-none border-1'
/>
<button
onClick={(e) => {
e.preventDefault();
cancelQuery();
}}
>
Cancel
</button>
<CardContent className='py-4'>
<PDFViewer file={file} className='object-contain' />
</CardContent>
</Card>
);

View File

@ -169,7 +169,7 @@ export const QuotesDataTable = ({ status = "all" }: { status?: string }) => {
<DataTableToolbar table={table} />
</DataTable>
<QuotePDFPreview quote={focusedQuote} className='flex-1' />
<QuotePDFPreview quote={focusedQuote} className='flex-1 ' />
</div>
);
};

View File

@ -0,0 +1,18 @@
import { cn } from "@/lib/utils";
import { LoaderIcon } from "lucide-react";
import * as React from "react";
const spinnerVariants = "w-6 h-6 rounded-full animate-spin";
interface LoadingSpinnerProps extends React.HTMLAttributes<SVGSVGElement> {
className?: string;
}
const LoadingSpinner = React.forwardRef<SVGSVGElement, LoadingSpinnerProps>((props, ref) => {
const { className, ...rest } = props;
return <LoaderIcon ref={ref} className={cn(spinnerVariants, className)} {...rest} />;
});
LoadingSpinner.displayName = "LoadingSpinner";
export { LoadingSpinner };

View File

@ -0,0 +1 @@
export * from "./LoadingSpinner";

View File

@ -1,9 +1,12 @@
import { cn } from "@/lib/utils";
import { Button } from "@/ui";
import { useResizeObserver } from "@wojtekmaj/react-hooks";
import type { PDFDocumentProxy } from "pdfjs-dist";
import printJS from "print-js";
import { useCallback, useEffect, useRef, useState } from "react";
import { t } from "i18next";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import { LoadingSpinner } from "../LoadingSpinner";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";
@ -28,15 +31,9 @@ export interface PDFViewerProps {
}
export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => {
const [eventResize, setEventResize] = useState(false);
const [numPages, setNumPages] = useState(0);
const [pageNumber, setPageNumber] = useState(1);
const [renderedPageNumber, setRenderedPageNumber] = useState(null);
const [width, setWidth] = useState(undefined);
const parentRef = useRef(null);
const canvasRef = useRef(null);
const [numPages, setNumPages] = useState<number>(0);
const [pageNumber, setPageNumber] = useState<number>(1);
const [renderedPageNumber, setRenderedPageNumber] = useState<number | undefined>(undefined);
const [containerRef, setContainerRef] = useState<HTMLElement | null>(null);
const [containerWidth, setContainerWidth] = useState<number>();
@ -51,38 +48,15 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => {
useResizeObserver(containerRef, resizeObserverOptions, onResize);
const hidePageCanvas = useCallback(() => {
const canvas = containerRef?.current?.querySelector("canvas");
if (canvas) canvas.style.visibility = "hidden";
}, [containerRef]);
const showPageCanvas = useCallback(() => {
const canvas = containerRef?.current?.querySelector("canvas");
if (canvas) canvas.style.visibility = "visible";
}, [containerRef]);
const onPageLoadSuccess = useCallback(() => {
hidePageCanvas();
}, [hidePageCanvas]);
const onPageRenderSuccess = useCallback(() => {
showPageCanvas();
}, [showPageCanvas]);
setRenderedPageNumber(pageNumber);
}, [setRenderedPageNumber, pageNumber]);
const onPageRenderError = useCallback(() => {
showPageCanvas();
}, [showPageCanvas]);
function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
setPageNumber(1);
setNumPages(nextNumPages);
setNumPages(numPages);
}
/*function onDocumentLoadSuccess({ numPages: nextNumPages }: PDFDocumentProxy): void {
setPageNumber(1);
setNumPages(nextNumPages);
}*/
const changePage = (offset: number) =>
setPageNumber((prevPage) =>
offset > 0 ? Math.min(prevPage + offset, numPages) : Math.max(prevPage + offset, 1)
@ -93,77 +67,82 @@ export const PDFViewer = ({ file, className }: PDFViewerProps): JSX.Element => {
const goToFirstPage = () => setPageNumber(1);
const goToLastPage = () => setPageNumber(numPages);
useEffect(() => {
if (numPages > 0 || eventResize) {
const parentWidth = parentRef.current ? parentRef.current.offsetWidth : 0;
const canvasWidth = canvasRef.current ? canvasRef.current.width : 0;
console.log("Document => ", parentWidth);
console.log("Canvas => ", canvasWidth);
setWidth(parentWidth);
setEventResize(false);
}
}, [eventResize, numPages]);
//file={`data:application/pdf;base64,$(pdfBase64String)`}
const isLoading = renderedPageNumber !== pageNumber;
const isLoading = useMemo(
() => renderedPageNumber !== pageNumber,
[renderedPageNumber, pageNumber]
);
return (
<div className='flex flex-col' ref={setContainerRef}>
<div
className={cn("flex flex-col cursor-default text-center", className)}
ref={setContainerRef}
>
<Document
options={options}
renderMode='canvas'
file={file}
onLoadSuccess={onDocumentLoadSuccess}
className={className}
renderMode='canvas'
loading={<LoadingSpinner className='w-full mx-auto mt-32' />}
options={options}
className={`w-full aspect-[3/4] relative bg-white shadow w-[${
containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth
}]`}
>
{isLoading && renderedPageNumber ? (
<Page
key={`page_${renderedPageNumber}`}
className='prevPage'
pageNumber={renderedPageNumber}
width={width}
/>
) : null}
{/**
* IMPORTANT: Keys are necessary so that React will know which Page component
* instances to use.
* Without keys, on page number update, React would replace the page number
* in 1st and 2nd page components. This may cause previously rendered page
* to render again, thus causing a flash.
* With keys, React, will add prevPage className to previously rendered page,
* and mount new Page component instance for the new page.
*/}
<Page
className={isLoading && renderedPageNumber ? "visible" : "hidden"}
key={`page_${renderedPageNumber}`}
pageNumber={renderedPageNumber}
canvasBackground={"white"}
key={`page_${pageNumber}`}
width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
/>
<Page
className={cn(isLoading && renderedPageNumber ? "hidden" : "visible", "text-center")}
key={pageNumber}
pageNumber={pageNumber}
canvasRef={canvasRef}
width={width}
onLoadSuccess={onPageLoadSuccess}
canvasBackground={"white"}
onRenderSuccess={onPageRenderSuccess}
onRenderError={onPageRenderError}
width={containerWidth ? Math.min(containerWidth, maxWidth) : maxWidth}
/>
</Document>
<p className='text-sm font-medium text-center'>
Página {pageNumber} de {numPages}
</p>
<div className='flex justify-between'>
<Button size={"icon"} variant='link' onClick={goToPrevPage}>
Prev
</Button>
<Button size={"icon"} variant='link' onClick={goToNextPage}>
Next
<div className='flex flex-row justify-center w-full mt-4 space-x-4'>
<Button
type='button'
variant='outline'
className='w-8 h-8 p-0'
onClick={goToPrevPage}
disabled={isLoading}
>
<span className='sr-only'>{t("common.go_to_prev_page")}</span>
<ChevronLeftIcon className='w-4 h-4' />
</Button>
<Button
size={"icon"}
variant='link'
onClick={(e) => {
e.preventDefault();
printJS({
printable: file,
type: "pdf",
showModal: false,
modalMessage: "Cargando...",
});
}}
type='button'
variant='outline'
className='w-8 h-8 p-0'
onClick={goToNextPage}
disabled={isLoading}
>
Imprimir react
<span className='sr-only'>{t("common.go_to_next_page")}</span>
<ChevronRightIcon className='w-4 h-4' />
</Button>
</div>
<p className='mt-4 text-sm font-medium text-center'>
{t("common.num_page_of_total", {
count: pageNumber,
total: numPages,
})}
</p>
</div>
);
};

View File

@ -8,8 +8,8 @@ export * from "./EmptyState";
export * from "./ErrorOverlay";
export * from "./Forms";
export * from "./Layout";
export * from "./LoadingIndicator";
export * from "./LoadingOverlay";
export * from "./LoadingSpinner";
export * from "./PDFViewer";
export * from "./ProtectedRoute";
//export * from "./SorteableDataTable";

View File

@ -46,7 +46,11 @@
"pick_date": "Select a date",
"required_field": "This field is required",
"unsaved_changes_prompt": "There are unsaved changes. If you leave, you'll lose your changes.",
"edit": "Edit"
"edit": "Edit",
"remove": "Remove",
"archive": "Archive",
"duplicate": "Duplicate",
"print": "Print"
},
"main_menu": {
"home": "Home",
@ -111,7 +115,10 @@
"customer_information": "Customer",
"total_price": "Imp. total"
},
"quote": "Quote"
"preview": {
"quote": "Quote",
"download_quote": "Download quote"
}
},
"create": {
"title": "New quote",

View File

@ -46,7 +46,11 @@
"pick_date": "Elige una fecha",
"required_field": "Este campo es obligatorio",
"unsaved_changes_prompt": "Los últimos cambios no se han guardado. Si continúas, se perderán.",
"edit": "Editar"
"edit": "Editar",
"remove": "Eliminar",
"archive": "Archivar",
"duplicate": "Duplicar",
"print": "Imprimir"
},
"main_menu": {
"home": "Inicio",
@ -110,6 +114,10 @@
"status": "Estado",
"customer_information": "Cliente",
"total_price": "Imp. total"
},
"preview": {
"quote": "Cotización",
"download_quote": "Descargar"
}
},
"create": {