diff --git a/package.json b/package.json index c118f91..b4834e1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,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 cc3bd3f..c59cf2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,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 @@ -1914,6 +1917,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'} @@ -4920,6 +4926,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/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 e4321cb..e1e5e8e 100644 --- a/src/components/product/previsualization/index.tsx +++ b/src/components/product/previsualization/index.tsx @@ -1,13 +1,19 @@ +import React from "react"; +import type { Video } from "@/types/video"; + 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"; -import type { Video } from "@/types/video"; + +const ELEMENT_ID = "image-to-download"; type ProductPrevisualizationProps = { className?: string; @@ -16,8 +22,8 @@ type ProductPrevisualizationProps = { }; 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 = ({ @@ -25,73 +31,68 @@ const ProductPrevisualization: React.FC = ({ video = defaultVideo, 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; 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";