From 3076613ea5efd9cc2435d8cef36f7ece360b79b8 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Thu, 30 Jan 2025 11:37:21 +0100 Subject: [PATCH 01/15] feat(bulk-download-qr): create dialog for bulk qr download --- app/atoms/bulk-update-dialog.ts | 3 +- .../assets/bulk-actions-dropdown.tsx | 10 +++ .../assets/bulk-download-qr-dialog.tsx | 63 +++++++++++++++++++ .../bulk-update-dialog/bulk-update-dialog.tsx | 3 +- app/components/icons/library.tsx | 2 +- app/components/shared/icons-map.tsx | 4 +- 6 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 app/components/assets/bulk-download-qr-dialog.tsx diff --git a/app/atoms/bulk-update-dialog.ts b/app/atoms/bulk-update-dialog.ts index 39c00b1d5..f877a1fff 100644 --- a/app/atoms/bulk-update-dialog.ts +++ b/app/atoms/bulk-update-dialog.ts @@ -5,7 +5,7 @@ import type { BulkDialogType } from "~/components/bulk-update-dialog/bulk-update * This atom is responsible for holding the open state for dialogs * Open Dialog must be open at a time */ -const DEFAULT_STATE = { +const DEFAULT_STATE: Record = { location: false, category: false, "assign-custody": false, @@ -21,6 +21,7 @@ const DEFAULT_STATE = { unavailable: false, bookings: false, "booking-exist": false, + "download-qr": false, }; export const bulkDialogAtom = diff --git a/app/components/assets/bulk-actions-dropdown.tsx b/app/components/assets/bulk-actions-dropdown.tsx index da966f98a..f79c2b5ef 100644 --- a/app/components/assets/bulk-actions-dropdown.tsx +++ b/app/components/assets/bulk-actions-dropdown.tsx @@ -32,6 +32,7 @@ import { } from "../shared/dropdown"; import When from "../when/when"; import BookSelectedAssetsDropdown from "./assets-index/book-selected-assets-dropdown"; +import BulkDownloadQrDialog from "./bulk-download-qr-dialog"; export default function BulkActionsDropdown() { const isHydrated = useHydrated(); @@ -127,6 +128,7 @@ function ConditionalDropdown() { + + + + + + {({ disabled, handleCloseDialog, fetcherError }) => ( +
+ {zo.errors.assetIds()?.message ? ( +

+ {zo.errors.assetIds()?.message} +

+ ) : null} + + {fetcherError ? ( +

{fetcherError}

+ ) : null} + +
+ + +
+
+ )} + + ); +} diff --git a/app/components/bulk-update-dialog/bulk-update-dialog.tsx b/app/components/bulk-update-dialog/bulk-update-dialog.tsx index b39ddc5a3..7ba4d7443 100644 --- a/app/components/bulk-update-dialog/bulk-update-dialog.tsx +++ b/app/components/bulk-update-dialog/bulk-update-dialog.tsx @@ -44,7 +44,8 @@ type BulkDialogType = | "available" | "unavailable" | "bookings" - | "booking-exist"; + | "booking-exist" + | "download-qr"; type CommonBulkDialogProps = { type: BulkDialogType; diff --git a/app/components/icons/library.tsx b/app/components/icons/library.tsx index 4e9d80fba..203ddb7af 100644 --- a/app/components/icons/library.tsx +++ b/app/components/icons/library.tsx @@ -1273,7 +1273,7 @@ export const DownloadIcon = (props: SVGProps) => ( > , change: , "booking-exist": , + "download-qr": , }; export default iconsMap; From c3eec614e8037076ad482fa51c638d185c0fb693 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 31 Jan 2025 11:46:49 +0100 Subject: [PATCH 02/15] feat(bulk-download-qr): create function to handle downloadng bulk qr codes --- .../assets/bulk-actions-dropdown.tsx | 7 +- .../assets/bulk-download-qr-dialog.tsx | 159 ++++++++++++------ app/components/qr/qr-preview.tsx | 18 +- .../assets.get-assets-for-bulk-qr-download.ts | 79 +++++++++ 4 files changed, 196 insertions(+), 67 deletions(-) create mode 100644 app/routes/api+/assets.get-assets-for-bulk-qr-download.ts diff --git a/app/components/assets/bulk-actions-dropdown.tsx b/app/components/assets/bulk-actions-dropdown.tsx index f79c2b5ef..40a24a233 100644 --- a/app/components/assets/bulk-actions-dropdown.tsx +++ b/app/components/assets/bulk-actions-dropdown.tsx @@ -128,7 +128,6 @@ function ConditionalDropdown() { -
- + { + query.append("assetIds", asset.id); + }); + + setIsGeneratingQrCodes(true); - const assetsSelected = useAtomValue(selectedBulkItemsAtom); - const isAllSelect = isSelectingAllItems(assetsSelected); + try { + /* Getting all validated assets with qr object */ + const response = await fetch( + `/api/assets/get-assets-for-bulk-qr-download?${query}&${searchParams}` + ).then((response) => response.json()); + + const assets = response.assets as Array<{ + id: string; + title: string; + createdAt: string; + qr: QrDef; + }>; + + const zip = new JSZip(); + const qrFolder = zip.folder("qr-codes"); + + for (const asset of assets) { + const filename = `${asset.id}.jpg`; + + /* Converting our React compoentn to html so that we can later convert it into an image */ + const qrCodeContent = renderToStaticMarkup( +
+ +
+ ); + + /* Creating div element to convert it into image because domtoimage expects an Html node */ + const div = document.createElement("div"); + div.innerHTML = qrCodeContent; + + /* Converting html to image */ + const qrBlob = await domtoimage.toBlob(div, { + height: 600, + width: 600, + bgcolor: "white", + style: { + display: "flex", + alignItems: "center", + justifyContent: "center", + transform: "scale(2)", + transformOrigin: "center", + }, + }); + + const qrImageFile = new File([qrBlob], filename); + + /* Appending qr code image to zip file */ + if (qrFolder) { + qrFolder.file(filename, qrImageFile); + } else { + zip.file(filename, qrImageFile); + } + } + + const zipBlob = await zip.generateAsync({ type: "blob" }); + const downloadLink = document.createElement("a"); + + downloadLink.href = URL.createObjectURL(zipBlob); + downloadLink.download = "qr-codes.zip"; + + downloadLink.click(); + + setTimeout(() => { + URL.revokeObjectURL(downloadLink.href); + }, 4e4); + } catch (error) { + setError( + error instanceof Error ? error.message : "Something went wrong." + ); + } finally { + setIsGeneratingQrCodes(false); + } + } return ( - - {({ disabled, handleCloseDialog, fetcherError }) => ( -
- {zo.errors.assetIds()?.message ? ( -

- {zo.errors.assetIds()?.message} -

- ) : null} - - {fetcherError ? ( -

{fetcherError}

- ) : null} - -
- - -
-
- )} -
+ + Download QR Codes + + ); } diff --git a/app/components/qr/qr-preview.tsx b/app/components/qr/qr-preview.tsx index 1f7c04d7f..3c5a5bf14 100644 --- a/app/components/qr/qr-preview.tsx +++ b/app/components/qr/qr-preview.tsx @@ -104,20 +104,22 @@ export const QrPreview = ({ className, qrObj, item }: ObjectType) => { ); }; +export type QrDef = { + id: string; + size: SizeKeys; + src: string; +}; + interface QrLabelProps { - data?: { - qr?: { - id: string; - size: SizeKeys; - src: string; - }; - }; + className?: string; + data?: { qr?: QrDef }; title: string; } -const QrLabel = React.forwardRef( +export const QrLabel = React.forwardRef( function QrLabel(props, ref) { const { data, title } = props ?? {}; + return (
Date: Fri, 31 Jan 2025 13:19:27 +0100 Subject: [PATCH 03/15] feat(bulk-download-qr): create dialog for bulk download qr codes --- .../assets/bulk-actions-dropdown.tsx | 28 ++++- .../assets/bulk-download-qr-dialog.tsx | 119 ++++++++++++++---- 2 files changed, 122 insertions(+), 25 deletions(-) diff --git a/app/components/assets/bulk-actions-dropdown.tsx b/app/components/assets/bulk-actions-dropdown.tsx index 40a24a233..ec9c8ca78 100644 --- a/app/components/assets/bulk-actions-dropdown.tsx +++ b/app/components/assets/bulk-actions-dropdown.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useNavigation } from "@remix-run/react"; import { useAtomValue } from "jotai"; import { useHydrated } from "remix-utils/use-hydrated"; @@ -33,6 +34,7 @@ import { import When from "../when/when"; import BookSelectedAssetsDropdown from "./assets-index/book-selected-assets-dropdown"; import BulkDownloadQrDialog from "./bulk-download-qr-dialog"; +import Icon from "../icons/icon"; export default function BulkActionsDropdown() { const isHydrated = useHydrated(); @@ -58,6 +60,7 @@ export default function BulkActionsDropdown() { function ConditionalDropdown() { const navigation = useNavigation(); const isLoading = isFormProcessing(navigation.state); + const [isBulkDownloadQrOpen, setIsBulkDownloadQrOpen] = useState(false); const { ref: dropdownRef, @@ -130,6 +133,13 @@ function ConditionalDropdown() { + { + setIsBulkDownloadQrOpen(false); + }} + /> + - - + { + closeMenu(); + setIsBulkDownloadQrOpen(true); + }} + className="py-1 lg:p-0" + > + void; +}; + +type DownloadState = + | { status: "idle" } + | { status: "loading" } + | { status: "success" } + | { status: "error"; error: string }; + +export default function BulkDownloadQrDialog({ + className, + isDialogOpen, + onClose, +}: BulkDownloadQrDialogProps) { + const [downloadState, setDownloadState] = useState({ + status: "idle", + }); const [searchParams] = useSearchParams(); const selectedAssets = useAtomValue(selectedBulkItemsAtom); + const allAssetsSelected = isSelectingAllItems(selectedAssets); + + const disabled = + selectedAssets.length === 0 || downloadState.status === "loading"; + + function handleClose() { + setDownloadState({ status: "idle" }); + onClose(); + } async function handleBulkDownloadQr() { const query = new URLSearchParams(); @@ -24,7 +55,7 @@ export default function BulkDownloadQrDialog() { query.append("assetIds", asset.id); }); - setIsGeneratingQrCodes(true); + setDownloadState({ status: "loading" }); try { /* Getting all validated assets with qr object */ @@ -47,7 +78,7 @@ export default function BulkDownloadQrDialog() { /* Converting our React compoentn to html so that we can later convert it into an image */ const qrCodeContent = renderToStaticMarkup( -
+
); @@ -65,6 +96,7 @@ export default function BulkDownloadQrDialog() { display: "flex", alignItems: "center", justifyContent: "center", + textAlign: "center", transform: "scale(2)", transformOrigin: "center", }, @@ -91,26 +123,67 @@ export default function BulkDownloadQrDialog() { setTimeout(() => { URL.revokeObjectURL(downloadLink.href); }, 4e4); + + setDownloadState({ status: "success" }); } catch (error) { - setError( - error instanceof Error ? error.message : "Something went wrong." - ); - } finally { - setIsGeneratingQrCodes(false); + setDownloadState({ + status: "error", + error: error instanceof Error ? error.message : "Something went wrong.", + }); } } return ( - + + + +
+ } + > +
+

+ Download qr codes for{" "} + {allAssetsSelected ? "all" : selectedAssets.length} asset(s). +

+

+ {allAssetsSelected ? "All" : selectedAssets.length} qr code(s) will + be downloaded in a zip file. +

+ +

+ Successfully downloaded qr codes. +

+
+ + {downloadState.status === "error" ? ( +

{downloadState.error}

+ ) : null} + +
+ + + +
+
+ + ); } From c15a92856f085da11fad1a1da798bd995465bd01 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Wed, 5 Feb 2025 11:04:32 +0100 Subject: [PATCH 04/15] feat(bulk-download-qr): update style, add feedback for loading, update button position --- .../assets/bulk-actions-dropdown.tsx | 36 ++--- .../assets/bulk-download-qr-dialog.tsx | 131 +++++++++++------- app/components/qr/qr-preview.tsx | 84 ++++++----- 3 files changed, 149 insertions(+), 102 deletions(-) diff --git a/app/components/assets/bulk-actions-dropdown.tsx b/app/components/assets/bulk-actions-dropdown.tsx index ec9c8ca78..c352c2ef2 100644 --- a/app/components/assets/bulk-actions-dropdown.tsx +++ b/app/components/assets/bulk-actions-dropdown.tsx @@ -215,6 +215,24 @@ function ConditionalDropdown() { ref={dropdownRef} >
+ { + closeMenu(); + setIsBulkDownloadQrOpen(true); + }} + className="border-b py-1 lg:p-0" + > + + + - { - closeMenu(); - setIsBulkDownloadQrOpen(true); - }} - className="py-1 lg:p-0" - > - - - (); + const [downloadState, setDownloadState] = useState({ status: "idle", }); @@ -43,6 +46,8 @@ export default function BulkDownloadQrDialog({ const disabled = selectedAssets.length === 0 || downloadState.status === "loading"; + const selectedCount = allAssetsSelected ? totalItems : selectedAssets.length; + function handleClose() { setDownloadState({ status: "idle" }); onClose(); @@ -55,7 +60,7 @@ export default function BulkDownloadQrDialog({ query.append("assetIds", asset.id); }); - setDownloadState({ status: "loading" }); + setDownloadState({ status: "loading", generatedQrCount: 0 }); try { /* Getting all validated assets with qr object */ @@ -74,13 +79,20 @@ export default function BulkDownloadQrDialog({ const qrFolder = zip.folder("qr-codes"); for (const asset of assets) { - const filename = `${asset.id}.jpg`; + const filename = `${asset.title}_${asset.qr.id}.jpg`; /* Converting our React compoentn to html so that we can later convert it into an image */ const qrCodeContent = renderToStaticMarkup( -
- -
+ ); /* Creating div element to convert it into image because domtoimage expects an Html node */ @@ -89,8 +101,8 @@ export default function BulkDownloadQrDialog({ /* Converting html to image */ const qrBlob = await domtoimage.toBlob(div, { - height: 600, - width: 600, + height: 700, + width: 700, bgcolor: "white", style: { display: "flex", @@ -110,6 +122,17 @@ export default function BulkDownloadQrDialog({ } else { zip.file(filename, qrImageFile); } + + setDownloadState((prev) => { + if (prev.status !== "loading") { + return prev; + } + + return { + status: "loading", + generatedQrCount: prev.generatedQrCount + 1, + }; + }); } const zipBlob = await zip.generateAsync({ type: "blob" }); @@ -146,42 +169,54 @@ export default function BulkDownloadQrDialog({ } >
-

- Download qr codes for{" "} - {allAssetsSelected ? "all" : selectedAssets.length} asset(s). -

-

- {allAssetsSelected ? "All" : selectedAssets.length} qr code(s) will - be downloaded in a zip file. -

- -

- Successfully downloaded qr codes. -

-
- - {downloadState.status === "error" ? ( -

{downloadState.error}

- ) : null} - -
- - - -
+ {downloadState.status === "loading" ? ( +
+ +

+ Generating Zip file [{downloadState.generatedQrCount}/ + {selectedCount}] +

+
+ ) : ( + <> +

+ Download qr codes for{" "} + {allAssetsSelected ? "all" : selectedAssets.length} asset(s). +

+

+ {allAssetsSelected ? "All" : selectedAssets.length} qr code(s) + will be downloaded in a zip file. +

+ +

+ Successfully downloaded qr codes. +

+
+ + {downloadState.status === "error" ? ( +

{downloadState.error}

+ ) : null} + +
+ + + +
+ + )}
diff --git a/app/components/qr/qr-preview.tsx b/app/components/qr/qr-preview.tsx index 3c5a5bf14..03ef0dba4 100644 --- a/app/components/qr/qr-preview.tsx +++ b/app/components/qr/qr-preview.tsx @@ -5,11 +5,14 @@ import { useReactToPrint } from "react-to-print"; import { Button } from "~/components/shared/button"; import { slugify } from "~/utils/slugify"; import { tw } from "~/utils/tw"; +import When from "../when/when"; type SizeKeys = "cable" | "small" | "medium" | "large"; interface ObjectType { className?: string; + style?: React.CSSProperties; + hideButton?: boolean; item: { name: string; type: "asset" | "kit"; @@ -23,7 +26,13 @@ interface ObjectType { }; } -export const QrPreview = ({ className, qrObj, item }: ObjectType) => { +export const QrPreview = ({ + className, + style, + qrObj, + item, + hideButton = false, +}: ObjectType) => { const captureDivRef = useRef(null); const downloadQrBtnRef = useRef(null); @@ -66,40 +75,40 @@ export const QrPreview = ({ className, qrObj, item }: ObjectType) => { } } - const printQr = useReactToPrint({ - content: () => captureDivRef.current, - }); + const printQr = useReactToPrint({ content: () => captureDivRef.current }); + return (
-
- - -
+ + +
+ + +
+
); }; @@ -134,14 +143,17 @@ export const QrLabel = React.forwardRef( alt={`${data?.qr?.size}-shelf-qr-code.png`} /> -
- - {data?.qr?.id} - - +
+
{data?.qr?.id}
+
Powered by{" "} - shelf.nu - + + shelf.nu + +
); From ca63a5ac48a0dd72676d0d9f2caf6c64b1c4ee9c Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Wed, 5 Feb 2025 12:24:11 +0100 Subject: [PATCH 05/15] feat(bulk-download-qr): optimize qr code generation --- .../assets/bulk-download-qr-dialog.tsx | 83 ++++++++----------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/app/components/assets/bulk-download-qr-dialog.tsx b/app/components/assets/bulk-download-qr-dialog.tsx index 3278293bb..09a8fe3eb 100644 --- a/app/components/assets/bulk-download-qr-dialog.tsx +++ b/app/components/assets/bulk-download-qr-dialog.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { useLoaderData } from "@remix-run/react"; import domtoimage from "dom-to-image"; import { useAtomValue } from "jotai"; import JSZip from "jszip"; @@ -7,7 +6,6 @@ import { DownloadIcon } from "lucide-react"; import { renderToStaticMarkup } from "react-dom/server"; import { selectedBulkItemsAtom } from "~/atoms/list"; import { useSearchParams } from "~/hooks/search-params"; -import { type loader } from "~/routes/_layout+/assets._index"; import { isSelectingAllItems } from "~/utils/list"; import { Dialog, DialogPortal } from "../layout/dialog"; import type { QrDef } from "../qr/qr-preview"; @@ -24,7 +22,7 @@ type BulkDownloadQrDialogProps = { type DownloadState = | { status: "idle" } - | { status: "loading"; generatedQrCount: number } + | { status: "loading" } | { status: "success" } | { status: "error"; error: string }; @@ -33,8 +31,6 @@ export default function BulkDownloadQrDialog({ isDialogOpen, onClose, }: BulkDownloadQrDialogProps) { - const { totalItems } = useLoaderData(); - const [downloadState, setDownloadState] = useState({ status: "idle", }); @@ -46,8 +42,6 @@ export default function BulkDownloadQrDialog({ const disabled = selectedAssets.length === 0 || downloadState.status === "loading"; - const selectedCount = allAssetsSelected ? totalItems : selectedAssets.length; - function handleClose() { setDownloadState({ status: "idle" }); onClose(); @@ -60,7 +54,7 @@ export default function BulkDownloadQrDialog({ query.append("assetIds", asset.id); }); - setDownloadState({ status: "loading", generatedQrCount: 0 }); + setDownloadState({ status: "loading" }); try { /* Getting all validated assets with qr object */ @@ -78,10 +72,8 @@ export default function BulkDownloadQrDialog({ const zip = new JSZip(); const qrFolder = zip.folder("qr-codes"); - for (const asset of assets) { - const filename = `${asset.title}_${asset.qr.id}.jpg`; - - /* Converting our React compoentn to html so that we can later convert it into an image */ + /* Converting our React compoentn to html so that we can later convert it into an image */ + const qrNodes = assets.map((asset) => { const qrCodeContent = renderToStaticMarkup( + /* Converting html to image */ + domtoimage.toBlob(qrNode, { + height: 700, + width: 700, + bgcolor: "white", + style: { + display: "flex", + alignItems: "center", + justifyContent: "center", + textAlign: "center", + transform: "scale(2)", + transformOrigin: "center", + }, + }) + ) + ); + + /* Appending qr code image to zip file */ + qrImages.forEach((qrImage, index) => { + const asset = assets[index]; + const filename = `${asset.title}_${asset.qr.id}.jpg`; if (qrFolder) { - qrFolder.file(filename, qrImageFile); + qrFolder.file(filename, qrImage); } else { - zip.file(filename, qrImageFile); + zip.file(filename, qrImage); } - - setDownloadState((prev) => { - if (prev.status !== "loading") { - return prev; - } - - return { - status: "loading", - generatedQrCount: prev.generatedQrCount + 1, - }; - }); - } + }); const zipBlob = await zip.generateAsync({ type: "blob" }); const downloadLink = document.createElement("a"); @@ -172,10 +162,7 @@ export default function BulkDownloadQrDialog({ {downloadState.status === "loading" ? (
-

- Generating Zip file [{downloadState.generatedQrCount}/ - {selectedCount}] -

+

Generating Zip file ...

) : ( <> From e05e61a84a1a3a59b87818954fa6e39c03898926 Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 7 Feb 2025 12:23:40 +0100 Subject: [PATCH 06/15] feat(bulk-download-qr): change dom-to-image to html-to-image --- .../assets/bulk-download-qr-dialog.tsx | 79 ++++++++++--------- app/components/qr/qr-preview.tsx | 5 +- app/utils/component-to-html.ts | 22 ++++++ package-lock.json | 7 ++ package.json | 1 + 5 files changed, 77 insertions(+), 37 deletions(-) create mode 100644 app/utils/component-to-html.ts diff --git a/app/components/assets/bulk-download-qr-dialog.tsx b/app/components/assets/bulk-download-qr-dialog.tsx index 09a8fe3eb..55d69f2e8 100644 --- a/app/components/assets/bulk-download-qr-dialog.tsx +++ b/app/components/assets/bulk-download-qr-dialog.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; -import domtoimage from "dom-to-image"; +import { toBlob } from "html-to-image"; import { useAtomValue } from "jotai"; import JSZip from "jszip"; import { DownloadIcon } from "lucide-react"; -import { renderToStaticMarkup } from "react-dom/server"; import { selectedBulkItemsAtom } from "~/atoms/list"; import { useSearchParams } from "~/hooks/search-params"; +import { generateHtmlFromComponent } from "~/utils/component-to-html"; import { isSelectingAllItems } from "~/utils/list"; import { Dialog, DialogPortal } from "../layout/dialog"; import type { QrDef } from "../qr/qr-preview"; @@ -72,12 +72,12 @@ export default function BulkDownloadQrDialog({ const zip = new JSZip(); const qrFolder = zip.folder("qr-codes"); - /* Converting our React compoentn to html so that we can later convert it into an image */ - const qrNodes = assets.map((asset) => { - const qrCodeContent = renderToStaticMarkup( + /* Converting our React component to html so that we can later convert it into an image */ + const qrNodes = assets.map((asset) => + generateHtmlFromComponent( - ); - - /* Creating div element to convert it into image because domtoimage expects an Html node */ - const div = document.createElement("div"); - div.innerHTML = qrCodeContent; + ) + ); - return div; - }); + const toBlobOptions = { + height: 700, + width: 700, + backgroundColor: "white", + style: { + display: "flex", + alignItems: "center", + justifyContent: "center", + textAlign: "center", + transform: "scale(2)", + transformOrigin: "center", + }, + }; + + /** + * We are converting first qr to image separately because toBlob will cache the font + * and will not make further network requests for other qr codes. + */ + const firstQrImage = await toBlob(qrNodes[0], toBlobOptions); /* Converting all qr nodes into images */ const qrImages = await Promise.all( - qrNodes.map(async (qrNode) => - /* Converting html to image */ - domtoimage.toBlob(qrNode, { - height: 700, - width: 700, - bgcolor: "white", - style: { - display: "flex", - alignItems: "center", - justifyContent: "center", - textAlign: "center", - transform: "scale(2)", - transformOrigin: "center", - }, - }) - ) + qrNodes.slice(1).map((qrNode) => toBlob(qrNode, toBlobOptions)) ); + qrImages.push(firstQrImage); + /* Appending qr code image to zip file */ qrImages.forEach((qrImage, index) => { const asset = assets[index]; const filename = `${asset.title}_${asset.qr.id}.jpg`; + if (!qrImage) { + return; + } + if (qrFolder) { qrFolder.file(filename, qrImage); } else { @@ -194,13 +199,15 @@ export default function BulkDownloadQrDialog({ Close - + + +
)} diff --git a/app/components/qr/qr-preview.tsx b/app/components/qr/qr-preview.tsx index 03ef0dba4..9d23bed92 100644 --- a/app/components/qr/qr-preview.tsx +++ b/app/components/qr/qr-preview.tsx @@ -134,7 +134,10 @@ export const QrLabel = React.forwardRef( className="flex aspect-square w-[300px] flex-col justify-center gap-3 rounded border-[5px] border-[#E3E4E8] bg-white px-6 py-[17px]" ref={ref} > -
+
{title}
diff --git a/app/utils/component-to-html.ts b/app/utils/component-to-html.ts new file mode 100644 index 000000000..cb44ae31d --- /dev/null +++ b/app/utils/component-to-html.ts @@ -0,0 +1,22 @@ +import { renderToStaticMarkup } from "react-dom/server"; + +/** + * This function generates the html node from a react component with font family + */ +export function generateHtmlFromComponent(component: React.ReactElement) { + const componentMarkup = renderToStaticMarkup(component); + + const htmlElement = document.createElement("html"); + htmlElement.innerHTML = ` + + +${componentMarkup} + +`; + + return htmlElement; +} diff --git a/package-lock.json b/package-lock.json index 37f2d2bf4..f17c6e37c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "detectrtc": "^1.4.1", "dom-to-image": "^2.6.0", "framer-motion": "^11.0.5", + "html-to-image": "^1.11.11", "iconv-lite": "^0.6.3", "intl-parse-accept-language": "^1.0.0", "isbot": "^4.4.0", @@ -12938,6 +12939,12 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-to-image": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", + "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==", + "license": "MIT" + }, "node_modules/html-to-text": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", diff --git a/package.json b/package.json index 49a14329c..3194721f8 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "detectrtc": "^1.4.1", "dom-to-image": "^2.6.0", "framer-motion": "^11.0.5", + "html-to-image": "^1.11.11", "iconv-lite": "^0.6.3", "intl-parse-accept-language": "^1.0.0", "isbot": "^4.4.0", From eddf093f91b1d467264046e9f780eead09a330ca Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 7 Feb 2025 12:33:27 +0100 Subject: [PATCH 07/15] feat(bulk-download-qr): remove dom-to-image --- app/components/qr/qr-preview.tsx | 23 +++++++++++------------ package-lock.json | 13 ------------- package.json | 2 -- 3 files changed, 11 insertions(+), 27 deletions(-) diff --git a/app/components/qr/qr-preview.tsx b/app/components/qr/qr-preview.tsx index 9d23bed92..dfe68054e 100644 --- a/app/components/qr/qr-preview.tsx +++ b/app/components/qr/qr-preview.tsx @@ -1,6 +1,6 @@ import React, { useRef, useMemo } from "react"; import { changeDpiDataUrl } from "changedpi"; -import domtoimage from "dom-to-image"; +import { toPng } from "html-to-image"; import { useReactToPrint } from "react-to-print"; import { Button } from "~/components/shared/button"; import { slugify } from "~/utils/slugify"; @@ -49,17 +49,16 @@ export const QrPreview = ({ // making sure that the captureDiv and downloadBtn exists in DOM if (captureDiv && downloadBtn) { e.preventDefault(); - domtoimage - .toPng(captureDiv, { - height: captureDiv.offsetHeight * 2, - width: captureDiv.offsetWidth * 2, - style: { - transform: `scale(${2})`, - transformOrigin: "top left", - width: `${captureDiv.offsetWidth}px`, - height: `${captureDiv.offsetHeight}px`, - }, - }) + toPng(captureDiv, { + height: captureDiv.offsetHeight * 2, + width: captureDiv.offsetWidth * 2, + style: { + transform: `scale(${2})`, + transformOrigin: "top left", + width: `${captureDiv.offsetWidth}px`, + height: `${captureDiv.offsetHeight}px`, + }, + }) .then((dataUrl: string) => { const downloadLink = document.createElement("a"); downloadLink.href = changeDpiDataUrl(dataUrl, 300); diff --git a/package-lock.json b/package-lock.json index f17c6e37c..b50318a10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,6 @@ "date-fns": "^3.3.1", "date-fns-tz": "^3.2.0", "detectrtc": "^1.4.1", - "dom-to-image": "^2.6.0", "framer-motion": "^11.0.5", "html-to-image": "^1.11.11", "iconv-lite": "^0.6.3", @@ -101,7 +100,6 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", - "@types/dom-to-image": "^2.6.7", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.17.9", @@ -7598,12 +7596,6 @@ "@types/ms": "*" } }, - "node_modules/@types/dom-to-image": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.7.tgz", - "integrity": "sha512-me5VbCv+fcXozblWwG13krNBvuEOm6kA5xoa4RrjDJCNFOZSWR3/QLtOXimBHk1Fisq69Gx3JtOoXtg1N1tijg==", - "dev": true - }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -10338,11 +10330,6 @@ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "node_modules/dom-to-image": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz", - "integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==" - }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", diff --git a/package.json b/package.json index 3194721f8..53e3dadf8 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,6 @@ "date-fns": "^3.3.1", "date-fns-tz": "^3.2.0", "detectrtc": "^1.4.1", - "dom-to-image": "^2.6.0", "framer-motion": "^11.0.5", "html-to-image": "^1.11.11", "iconv-lite": "^0.6.3", @@ -135,7 +134,6 @@ "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^14.2.1", "@testing-library/user-event": "^14.5.2", - "@types/dom-to-image": "^2.6.7", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.5", "@types/lodash": "^4.17.9", From a2ad053e436ac3f5a1fd435f3657af5e38e5255d Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Fri, 7 Feb 2025 15:20:44 +0100 Subject: [PATCH 08/15] feat(bulk-download-qr): remove padding from qr image --- app/components/assets/bulk-download-qr-dialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/assets/bulk-download-qr-dialog.tsx b/app/components/assets/bulk-download-qr-dialog.tsx index 55d69f2e8..8af15f5d9 100644 --- a/app/components/assets/bulk-download-qr-dialog.tsx +++ b/app/components/assets/bulk-download-qr-dialog.tsx @@ -89,8 +89,8 @@ export default function BulkDownloadQrDialog({ ); const toBlobOptions = { - height: 700, - width: 700, + width: 470, + height: 472, backgroundColor: "white", style: { display: "flex", From 451b17dc3703d9939e7d7656df20dcdc7c6dfdeb Mon Sep 17 00:00:00 2001 From: Rohit Kumar Saini Date: Tue, 11 Feb 2025 11:43:46 +0100 Subject: [PATCH 09/15] feat(bulk-download-qr): fix sizing of downloaded qr --- .../assets/bulk-download-qr-dialog.tsx | 15 ++------- app/components/qr/qr-preview.tsx | 32 +++++++++++++------ app/utils/component-to-html.ts | 4 +++ 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app/components/assets/bulk-download-qr-dialog.tsx b/app/components/assets/bulk-download-qr-dialog.tsx index 8af15f5d9..028830cc5 100644 --- a/app/components/assets/bulk-download-qr-dialog.tsx +++ b/app/components/assets/bulk-download-qr-dialog.tsx @@ -76,11 +76,6 @@ export default function BulkDownloadQrDialog({ const qrNodes = assets.map((asset) => generateHtmlFromComponent( toBlob(qrNode, toBlobOptions)) ); - qrImages.push(firstQrImage); - /* Appending qr code image to zip file */ - qrImages.forEach((qrImage, index) => { + [firstQrImage, ...qrImages].forEach((qrImage, index) => { const asset = assets[index]; const filename = `${asset.title}_${asset.qr.id}.jpg`; if (!qrImage) { diff --git a/app/components/qr/qr-preview.tsx b/app/components/qr/qr-preview.tsx index dfe68054e..df8e8a9cd 100644 --- a/app/components/qr/qr-preview.tsx +++ b/app/components/qr/qr-preview.tsx @@ -130,12 +130,31 @@ export const QrLabel = React.forwardRef( return (
{title}
@@ -149,12 +168,7 @@ export const QrLabel = React.forwardRef(
{data?.qr?.id}
Powered by{" "} - - shelf.nu - + shelf.nu
diff --git a/app/utils/component-to-html.ts b/app/utils/component-to-html.ts index cb44ae31d..08b14b1bb 100644 --- a/app/utils/component-to-html.ts +++ b/app/utils/component-to-html.ts @@ -10,6 +10,10 @@ export function generateHtmlFromComponent(component: React.ReactElement) { htmlElement.innerHTML = `