From 5f6074cedcf113cd7e457167569647d4d7645ab7 Mon Sep 17 00:00:00 2001 From: ImJustLucas Date: Mon, 20 May 2024 23:42:14 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20add=20html-to-image=20package?= =?UTF-8?q?=20and=20plugin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 8 ++++++++ src/plugins/html-to-image.ts | 39 ++++++++++++++++++++++++++++++++++++ src/types/image.ts | 1 + 4 files changed, 49 insertions(+) create mode 100644 src/plugins/html-to-image.ts create mode 100644 src/types/image.ts diff --git a/package.json b/package.json index f4b7753..ef4be49 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "axios": "^1.6.8", "dotenv-cli": "^7.3.0", "framer-motion": "10.6.1", + "html-to-image": "^1.11.11", "micro": "^10.0.1", "micro-cors": "^0.1.1", "next": "^14.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 372d888..da83118 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: framer-motion: specifier: 10.6.1 version: 10.6.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + html-to-image: + specifier: ^1.11.11 + version: 1.11.11 micro: specifier: ^10.0.1 version: 10.0.1 @@ -1868,6 +1871,9 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-to-image@1.11.11: + resolution: {integrity: sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==} + http-errors@1.7.3: resolution: {integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==} engines: {node: '>= 0.6'} @@ -4753,6 +4759,8 @@ snapshots: hookable@5.5.3: {} + html-to-image@1.11.11: {} + http-errors@1.7.3: dependencies: depd: 1.1.2 diff --git a/src/plugins/html-to-image.ts b/src/plugins/html-to-image.ts new file mode 100644 index 0000000..2f05abf --- /dev/null +++ b/src/plugins/html-to-image.ts @@ -0,0 +1,39 @@ +import type { ImageExtension } from "@/types/image"; +import { toPng, toJpeg, toBlob } from "html-to-image"; + +export const convertHtmlToImage = async ( + elementId: string, + format: ImageExtension +) => { + const element = document.getElementById(elementId); + if (!element) { + throw new Error("Element not found"); + } + + try { + let imageUrl = ""; + + switch (format) { + case "jpg": + imageUrl = await toJpeg(element, { quality: 0.95 }); + break; + case "webp": { + const blob = await toBlob(element, { type: "image/webp" }); + if (blob) { + imageUrl = URL.createObjectURL(blob); + } else { + throw new Error("Failed to create blob"); + } + break; + } + default: + imageUrl = await toPng(element); + break; + } + + return imageUrl; + } catch (error) { + console.error("Error generating image:", error); + throw error; + } +}; diff --git a/src/types/image.ts b/src/types/image.ts new file mode 100644 index 0000000..77a4ee1 --- /dev/null +++ b/src/types/image.ts @@ -0,0 +1 @@ +export type ImageExtension = "png" | "jpg" | "webp"; From fa7a7443f69cda1dbbc7af3621ebc17a16e193cd Mon Sep 17 00:00:00 2001 From: ImJustLucas Date: Mon, 20 May 2024 23:45:25 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20add=20handleDownload=20and=20ha?= =?UTF-8?q?ndleCopy=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../product/previsualization/card/index.tsx | 309 ++++++++--------- .../product/previsualization/index.tsx | 316 +++++++++--------- 2 files changed, 315 insertions(+), 310 deletions(-) diff --git a/src/components/product/previsualization/card/index.tsx b/src/components/product/previsualization/card/index.tsx index 041836f..24b3ee1 100644 --- a/src/components/product/previsualization/card/index.tsx +++ b/src/components/product/previsualization/card/index.tsx @@ -8,175 +8,176 @@ import { AnimatePresence, motion } from "framer-motion"; import { useMeasure } from "@/hooks/use-measure"; const roboto = Roboto({ - subsets: ["latin"], - weight: ["400", "500", "700"], - variable: "--font-roboto", + subsets: ["latin"], + weight: ["400", "500", "700"], + variable: "--font-roboto", }); type PrevisualizationCardProps = { - video: Video; - options: { - theme: "light" | "dark"; - activeWidgets: Widget[]; - display: Display; - textSize: number; - cornerRadius: number; - spacing: number; - }; + video: Video; + options: { + theme: "light" | "dark"; + activeWidgets: Widget[]; + display: Display; + textSize: number; + cornerRadius: number; + spacing: number; + }; }; const PrevisualizationCard = React.forwardRef< - HTMLDivElement, - PrevisualizationCardProps + HTMLDivElement, + PrevisualizationCardProps >(({ video, options }: PrevisualizationCardProps) => { - const buildFooter = () => { - const viewsCount = - isActiveWidgets(options.activeWidgets, "videoViews") && video.viewsCount; - const publishedAt = - isActiveWidgets(options.activeWidgets, "videoPublishedAt") && - video.publishedAt; - const hasSeparator = viewsCount && publishedAt; + const buildFooter = () => { + const viewsCount = + isActiveWidgets(options.activeWidgets, "videoViews") && video.viewsCount; + const publishedAt = + isActiveWidgets(options.activeWidgets, "videoPublishedAt") && + video.publishedAt; + const hasSeparator = viewsCount && publishedAt; - return `${viewsCount ? `${viewsCount} vues` : ""}${ - hasSeparator ? " • " : "" - }${publishedAt ? "Il y a 16 heures" : ""}`; - }; + return `${viewsCount ? `${viewsCount} vues` : ""}${ + hasSeparator ? " • " : "" + }${publishedAt ? "Il y a 16 heures" : ""}`; + }; - return ( - -
- {"Miniature + return ( + +
+ {"Miniature - {isActiveWidgets(options.activeWidgets, "videoDuration") && ( -
- ??:?? -
- )} + {isActiveWidgets(options.activeWidgets, "videoDuration") && ( +
+ ??:?? +
+ )} - {isActiveWidgets(options.activeWidgets, "videoProgressBar") && ( - - )} -
+ {isActiveWidgets(options.activeWidgets, "videoProgressBar") && ( + + )} +
-
- {isActiveWidgets(options.activeWidgets, "channelLogo") && ( - {`Logo - )} +
+ {isActiveWidgets(options.activeWidgets, "channelLogo") && ( + {`Logo + )} -
-
- {video.title} -
+
+
+ {video.title} +
- {isActiveWidgets(options.activeWidgets, "channelName") && ( -
{video.channelName}
- )} + {isActiveWidgets(options.activeWidgets, "channelName") && ( +
{video.channelName}
+ )} -
{buildFooter()}
-
-
- - ); +
{buildFooter()}
+
+
+
+ ); }); export default PrevisualizationCard; diff --git a/src/components/product/previsualization/index.tsx b/src/components/product/previsualization/index.tsx index 4df3777..f34cd65 100644 --- a/src/components/product/previsualization/index.tsx +++ b/src/components/product/previsualization/index.tsx @@ -1,174 +1,178 @@ +import React from "react"; + import Button from "@/components/ui/button"; import Icon from "@/components/ui/icon"; +import Select from "@/components/ui/select"; +import { defaultOptions, defaultVideo } from "@/constants/default-video"; import { css, cx } from "@styled-system/css"; +import type { ImageExtension } from "@/types/image"; +import { convertHtmlToImage } from "@/plugins/html-to-image"; + import PrevisualizationCard from "./card"; -import { defaultOptions, defaultVideo } from "@/constants/default-video"; import type { Widget } from ".."; -import Select from "@/components/ui/select"; import { useForm } from "react-hook-form"; -import React from "react"; + +const ELEMENT_ID = "image-to-download"; type ProductPrevisualizationProps = { - className?: string; - activeWidgets: Widget[]; + className?: string; + activeWidgets: Widget[]; }; type FormData = { - format: "png" | "jpg" | "webp"; - size: "0.5" | "0.75" | "1" | "1.5" | "2" | "3"; + format: ImageExtension; + size: "0.5" | "0.75" | "1" | "1.5" | "2" | "3"; }; const ProductPrevisualization: React.FC = ({ - className, - activeWidgets, + className, + activeWidgets, }: ProductPrevisualizationProps) => { - const ref = React.useRef(null); - - const { register, handleSubmit } = useForm({ - defaultValues: { - format: "png", - size: "1", - }, - }); - - // Un truc dans l'idée tu capte jsp j'pense bref azy - const onSubmit = handleSubmit(({ format, size }: FormData) => { - console.log("Data", format, size); - - //TODO: html to image and return image - - return "caca"; - }); - - const handleDownload = () => { - const image = onSubmit(); - console.log("Download image", image); - if (ref.current) { - } - }; - - const handleCopy = () => { - if (ref.current) { - const image = onSubmit(); - console.log("Copy image", image); - } - }; - - return ( -
-
-

- Prévisualisation -

- -
- -
- -
- -
- - - - - - - -
-
- ); + const ref = React.useRef(null); + + const { register, handleSubmit } = useForm({ + defaultValues: { + format: "png", + size: "1", + }, + }); + + const handleDownload = handleSubmit(async ({ format, size }: FormData) => { + const imageBase64 = await convertHtmlToImage(ELEMENT_ID, format); + + const link = document.createElement("a"); + link.download = `my-image-name.${format}`; + link.href = imageBase64; + link.click(); + }); + + const handleCopy = handleSubmit(async ({ format, size }: FormData) => { + const imageBase64 = await convertHtmlToImage(ELEMENT_ID, format); + + const blob = await fetch(imageBase64).then((res) => res.blob()); + + const clipboardItemInput = new ClipboardItem({ + [blob.type]: blob, + }); + + await navigator.clipboard.write([clipboardItemInput]); + }); + + return ( +
+
+

+ Prévisualisation +

+ +
+ +
+ +
+ +
+ + + + + + + +
+
+ ); }; export default ProductPrevisualization;