From 3515eeb1b78e4856b24c23afad844b399cd6293d Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Fri, 27 Sep 2024 13:46:36 -0300 Subject: [PATCH 1/8] feat: create CropOverlay component --- components/ds/CropOverlayComponent.tsx | 36 ++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 components/ds/CropOverlayComponent.tsx diff --git a/components/ds/CropOverlayComponent.tsx b/components/ds/CropOverlayComponent.tsx new file mode 100644 index 0000000..57c5ba9 --- /dev/null +++ b/components/ds/CropOverlayComponent.tsx @@ -0,0 +1,36 @@ +"use client"; +import { FollowingTooltipComponent } from "./FollowingTooltipComponent"; + +interface CropOverlayProps { + isCropping: boolean; + cropRect: { x: number; y: number; width: number; height: number } | null; + mousePosition: { x: number; y: number }; +} + +const CropOverlayComponent: React.FC = ({ + isCropping, + cropRect, + mousePosition, +}) => { + if (!isCropping || !cropRect) return null; + + const left = Math.min(cropRect.x, cropRect.x + cropRect.width); + const top = Math.min(cropRect.y, cropRect.y + cropRect.height); + const width = Math.abs(cropRect.width); + const height = Math.abs(cropRect.height); + + return ( + <> +
+ + + ); +}; + +export { CropOverlayComponent }; From c8e9924432416a75fbb5c52c25766d17d654d17f Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Fri, 27 Sep 2024 13:51:00 -0300 Subject: [PATCH 2/8] refactor: make divider component generic with margin props --- components/ds/DividerComponent.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 components/ds/DividerComponent.tsx diff --git a/components/ds/DividerComponent.tsx b/components/ds/DividerComponent.tsx new file mode 100644 index 0000000..35a4bde --- /dev/null +++ b/components/ds/DividerComponent.tsx @@ -0,0 +1,16 @@ +"use client"; +interface DividerProps { + margin?: 'small' | 'medium' | 'large'; +} + +const DividerComponent: React.FC = ({ margin = 'small' }) => { + const marginClasses = { + small: 'my-2', + medium: 'my-6', + large: 'my-8', + }; + + return
; +}; + +export { DividerComponent }; From 91f0ad0889c654ed16a5ee308dae9997b8062127 Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Fri, 27 Sep 2024 13:51:43 -0300 Subject: [PATCH 3/8] feat: create followingTooltip component --- components/ds/FollowingTooltipComponent.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 components/ds/FollowingTooltipComponent.tsx diff --git a/components/ds/FollowingTooltipComponent.tsx b/components/ds/FollowingTooltipComponent.tsx new file mode 100644 index 0000000..f11cad4 --- /dev/null +++ b/components/ds/FollowingTooltipComponent.tsx @@ -0,0 +1,21 @@ +"use client"; +interface TooltipProps { + message: string; + position: { x: number; y: number }; +} + +const FollowingTooltipComponent: React.FC = ({ + message, + position, +}) => { + return ( +
+ {message} +
+ ); +}; + +export { FollowingTooltipComponent }; From 0601494e487410c50ce51f0fcc7e2b6f52a526c7 Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Fri, 27 Sep 2024 13:52:21 -0300 Subject: [PATCH 4/8] test: add missing tests --- components/utils/resize-image.utils.test.ts | 77 +++++++++++++++++++-- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/components/utils/resize-image.utils.test.ts b/components/utils/resize-image.utils.test.ts index b200d2f..6bfde58 100644 --- a/components/utils/resize-image.utils.test.ts +++ b/components/utils/resize-image.utils.test.ts @@ -1,5 +1,7 @@ import { + calculateCropDimensions, handleResizeImage, + isPointInCropRect, processImageFile, resizeImage, updateHeight, @@ -97,7 +99,7 @@ describe("Image Processing Functions", () => { const setOutput = jest.fn(); processImageFile({ - file: mockFile, + source: mockFile, format: "jpeg", preserveAspectRatio: true, quality: 0.8, @@ -121,7 +123,7 @@ describe("Image Processing Functions", () => { }); const setWidth = jest.fn(); - updateWidth({ file: mockFile, height: 200, setWidth }); + updateWidth({ source: mockFile, height: 200, setWidth }); setTimeout(() => { expect(setWidth).toHaveBeenCalledWith(400); @@ -135,7 +137,7 @@ describe("Image Processing Functions", () => { }); const setHeight = jest.fn(); - updateHeight({ file: mockFile, width: 300, setHeight }); + updateHeight({ source: mockFile, width: 300, setHeight }); setTimeout(() => { expect(setHeight).toHaveBeenCalledWith(150); @@ -150,7 +152,7 @@ describe("Image Processing Functions", () => { const setOutput = jest.fn(); handleResizeImage({ - file: mockFile, + source: mockFile, format: "jpeg", height: 400, width: 600, @@ -166,4 +168,71 @@ describe("Image Processing Functions", () => { done(); }, 0); }); + + it("should calculate the crop dimensions correctly", () => { + const imgMock = { + width: 1000, + height: 500, + } as HTMLImageElement; + + const currentImageRefMock = { + clientWidth: 500, + clientHeight: 250, + } as HTMLImageElement; + + const cropRect = { x: 50, y: 50, width: 100, height: 50 }; + + const result = calculateCropDimensions(imgMock, currentImageRefMock, cropRect); + + expect(result).toEqual({ + x: 100, + y: 100, + width: 200, + height: 100, + }); + }); + + it("should handle negative width and height values in cropRect", () => { + const imgMock = { + width: 1000, + height: 500, + } as HTMLImageElement; + + const currentImageRefMock = { + clientWidth: 500, + clientHeight: 250, + } as HTMLImageElement; + + const cropRect = { x: 150, y: 150, width: -100, height: -50 }; + + const result = calculateCropDimensions(imgMock, currentImageRefMock, cropRect); + + expect(result).toEqual({ + x: 100, + y: 200, + width: 200, + height: 100, + }); + }); + + const cropRect = { x: 50, y: 50, width: 100, height: 50 }; + + it("should return true for a point inside the crop rectangle", () => { + const result = isPointInCropRect(75, 75, cropRect); + expect(result).toBe(true); + }); + + it("should return false for a point outside the crop rectangle", () => { + const result = isPointInCropRect(200, 200, cropRect); + expect(result).toBe(false); + }); + + it("should handle negative width and height in crop rectangle", () => { + const cropRectNegative = { x: 150, y: 150, width: -100, height: -50 }; + const result = isPointInCropRect(75, 75, cropRectNegative); + expect(result).toBe(false); + + const resultInside = isPointInCropRect(125, 125, cropRectNegative); + expect(resultInside).toBe(true); + }); }); From b61fd976ab212e8e1c89b87977e574247ca5c5d4 Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Fri, 27 Sep 2024 13:52:45 -0300 Subject: [PATCH 5/8] feat: add functions to calculate crop dimensions --- components/utils/resize-image.utils.ts | 207 +++++++++++++++++-------- 1 file changed, 143 insertions(+), 64 deletions(-) diff --git a/components/utils/resize-image.utils.ts b/components/utils/resize-image.utils.ts index d7809eb..b333d2d 100644 --- a/components/utils/resize-image.utils.ts +++ b/components/utils/resize-image.utils.ts @@ -72,7 +72,7 @@ export function resizeImage({ } interface ProcessImageFileOptions { - file: File; + source: File; setWidth: (width: number) => void; setHeight: (height: number) => void; setOutput: (output: string) => void; @@ -83,7 +83,7 @@ interface ProcessImageFileOptions { } export const processImageFile = ({ - file, + source, format, preserveAspectRatio, quality, @@ -92,73 +92,104 @@ export const processImageFile = ({ setWidth, done, }: ProcessImageFileOptions) => { - const reader = new FileReader(); - reader.onload = (e) => { - const img = new Image(); - img.src = e.target?.result as string; - img.onload = () => { - setWidth(img.width); - setHeight(img.height); - resizeImage({ - img, - width: img.width, - height: img.height, - format, - quality, - preserveAspectRatio, - }) - .then(setOutput) - .catch((error) => console.error(error)) - .finally(() => { - if (done) { - done(); - } - }); - }; + const img = new Image(); + const handleLoad = () => { + setWidth(img.width); + setHeight(img.height); + resizeImage({ + img, + width: img.width, + height: img.height, + format, + quality, + preserveAspectRatio, + }) + .then(setOutput) + .catch((error) => console.error(error)) + .finally(() => { + if (done) { + done(); + } + }); }; - reader.readAsDataURL(file); + + if (typeof source === "string") { + img.src = source; + img.onload = handleLoad; + } else { + const reader = new FileReader(); + reader.onload = (e) => { + img.src = e.target?.result as string; + img.onload = handleLoad; + }; + reader.readAsDataURL(source); + } }; interface UpdateWidthOptions { height: number; - file: File; + source: File | string; setWidth: (width: number) => void; } -export const updateWidth = ({ file, height, setWidth }: UpdateWidthOptions) => { +export const updateWidth = ({ + source, + height, + setWidth, +}: UpdateWidthOptions) => { const img = new Image(); - const reader = new FileReader(); - reader.onload = (e) => { - img.src = e.target?.result as string; - img.onload = () => { - const newWidth = Math.round(height * (img.width / img.height)); - setWidth(newWidth); - }; + + const handleLoad = () => { + const newWidth = Math.round(height * (img.width / img.height)); + setWidth(newWidth); }; - reader.readAsDataURL(file); + + if (typeof source === "string") { + img.src = source; + img.onload = handleLoad; + } else { + const reader = new FileReader(); + reader.onload = (e) => { + img.src = e.target?.result as string; + img.onload = handleLoad; + }; + reader.readAsDataURL(source); + } }; -interface UpdateWidthOption { +interface UpdateHeightOptions { width: number; - file: File; + source: File | string; setHeight: (height: number) => void; } -export const updateHeight = ({ file, setHeight, width }: UpdateWidthOption) => { +export const updateHeight = ({ + source, + setHeight, + width, +}: UpdateHeightOptions) => { const img = new Image(); - const reader = new FileReader(); - reader.onload = (e) => { - img.src = e.target?.result as string; - img.onload = () => { - const newHeight = Math.round(width / (img.width / img.height)); - setHeight(newHeight); - }; + + const handleLoad = () => { + const newHeight = Math.round(width / (img.width / img.height)); + setHeight(newHeight); }; - reader.readAsDataURL(file); + + if (typeof source === "string") { + img.src = source; + img.onload = handleLoad; + } else { + const reader = new FileReader(); + reader.onload = (e) => { + img.src = e.target?.result as string; + img.onload = handleLoad; + }; + reader.readAsDataURL(source); + } }; interface HandleResizeImage { - file: File; + source: File | string; width: number | undefined; height: number | undefined; format: Format; @@ -168,7 +199,7 @@ interface HandleResizeImage { } export const handleResizeImage = ({ - file, + source, format, height, preserveAspectRatio, @@ -176,20 +207,68 @@ export const handleResizeImage = ({ setOutput, width, }: HandleResizeImage) => { - const reader = new FileReader(); - reader.onload = (e) => { - const img = new Image(); - img.src = e.target?.result as string; - img.onload = () => { - resizeImage({ - img, - width, - height, - format, - quality, - preserveAspectRatio, - }).then(setOutput); - }; + const img = new Image(); + const handleLoad = () => { + resizeImage({ + img, + width, + height, + format, + quality, + preserveAspectRatio, + }).then(setOutput); }; - reader.readAsDataURL(file); + + if (typeof source === "string") { + img.src = source; + img.onload = handleLoad; + } else { + const reader = new FileReader(); + reader.onload = (e) => { + img.src = e.target?.result as string; + img.onload = handleLoad; + }; + reader.readAsDataURL(source); + } }; + +interface CropDimensions { + x: number; + y: number; + width: number; + height: number; +} + +export function calculateCropDimensions( + img: HTMLImageElement, + currentImageRef: HTMLImageElement, + cropRect: { x: number; y: number; width: number; height: number } +): CropDimensions { + const scaleX = img.width / currentImageRef.clientWidth; + const scaleY = img.height / currentImageRef.clientHeight; + + const x = Math.min(cropRect.x, cropRect.x + cropRect.width) * scaleX; + const y = Math.min(cropRect.y, cropRect.y + cropRect.height) * scaleY; + const width = Math.abs(cropRect.width) * scaleX; + const height = Math.abs(cropRect.height) * scaleY; + + return { x, y, width, height }; +} +interface CropRect { + x: number; + y: number; + width: number; + height: number; +} + +export function isPointInCropRect( + x: number, + y: number, + cropRect: CropRect +): boolean { + const rectLeft = Math.min(cropRect.x, cropRect.x + cropRect.width); + const rectTop = Math.min(cropRect.y, cropRect.y + cropRect.height); + const rectRight = rectLeft + Math.abs(cropRect.width); + const rectBottom = rectTop + Math.abs(cropRect.height); + return x >= rectLeft && x <= rectRight && y >= rectTop && y <= rectBottom; +} From 872167249439033400c8b31308a7b7b7fd087f5d Mon Sep 17 00:00:00 2001 From: Eduardo Giacometti De Patta Date: Fri, 27 Sep 2024 13:54:15 -0300 Subject: [PATCH 6/8] refactor: update components to use generic divider component --- pages/utilities/hex-to-rgb.tsx | 9 +++------ pages/utilities/image-to-base64.tsx | 10 ++++------ pages/utilities/jwt-parser.tsx | 11 ++++------- pages/utilities/regex-tester.tsx | 7 ++----- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/pages/utilities/hex-to-rgb.tsx b/pages/utilities/hex-to-rgb.tsx index 4b998f2..3e1d8fc 100644 --- a/pages/utilities/hex-to-rgb.tsx +++ b/pages/utilities/hex-to-rgb.tsx @@ -21,6 +21,7 @@ import CallToActionGrid from "@/components/CallToActionGrid"; import Meta from "@/components/Meta"; import { cn } from "@/lib/utils"; import RgbToHexSEO from "@/components/seo/RgbToHexSEO"; +import { DividerComponent } from "../../components/ds/DividerComponent"; const DEFAULT_RGB: RGBValues = { r: "0", g: "0", b: "0" }; @@ -123,7 +124,7 @@ export default function HEXtoRGB(props: HEXtoRGBProps) { - +
{(["r", "g", "b"] as (keyof RGBValues)[]).map((colorKey) => { @@ -154,7 +155,7 @@ export default function HEXtoRGB(props: HEXtoRGBProps) {
- + ); } - -const Divider = () => { - return
; -}; diff --git a/pages/utilities/image-to-base64.tsx b/pages/utilities/image-to-base64.tsx index 12f25f1..67b65e6 100644 --- a/pages/utilities/image-to-base64.tsx +++ b/pages/utilities/image-to-base64.tsx @@ -11,6 +11,7 @@ import CallToActionGrid from "@/components/CallToActionGrid"; import Meta from "@/components/Meta"; import { ImageUploadComponent } from "@/components/ds/ImageUploadComponent"; import ImageToBase64SEO from "@/components/seo/ImageToBase64SEO"; +import { DividerComponent } from "../../components/ds/DividerComponent"; export default function ImageToBase64() { const [base64, setBase64] = useState(""); @@ -63,7 +64,8 @@ export default function ImageToBase64() { - + +