npm-pkg-hook 1.11.3 → 1.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +5 -1
- package/package.json +1 -1
- package/src/config/content/en.json +5 -0
- package/src/config/content/es.json +5 -0
- package/src/config/content/index.js +16 -0
- package/src/hooks/index.js +6 -0
- package/src/hooks/newStoreOrderSubscription/index.js +1 -12
- package/src/hooks/updateExtProductFoodsOptional/index.js +2 -2
- package/src/hooks/useCatWithProduct/queries.js +50 -24
- package/src/hooks/useCategoriesProduct/index.js +1 -0
- package/src/hooks/useCategoriesProduct/queries.js +2 -2
- package/src/hooks/useCategoryInStore/queries.js +2 -2
- package/src/hooks/useClients/queries.js +72 -23
- package/src/hooks/useCreateProduct/helpers/index.js +2 -2
- package/src/hooks/useCreateProduct/index.js +45 -46
- package/src/hooks/useDessert/index.js +20 -3
- package/src/hooks/useDessertWithPrice/helpers/index.js +2 -2
- package/src/hooks/useDessertWithPrice/index.js +70 -60
- package/src/hooks/useDessertWithPrice/queries.js +2 -2
- package/src/hooks/useDevices/queries.js +16 -7
- package/src/hooks/useDevices/useGetDevices.js +12 -19
- package/src/hooks/useDevices/useRegisterDevices.js +3 -3
- package/src/hooks/useDownloadReports/helpers/downloadFileFromResponse.ts +21 -0
- package/src/hooks/useDownloadReports/index.ts +2 -0
- package/src/hooks/useDownloadReports/useDownloadReportByDay/index.ts +103 -0
- package/src/hooks/useDownloadReports/useGetReportByDateRange/index.ts +115 -0
- package/src/hooks/useEditCategory/index.js +10 -10
- package/src/hooks/useFormTools/index.js +2 -1
- package/src/hooks/useFormatDate/index.js +56 -3
- package/src/hooks/useGetStoreCookie/index.js +1 -1
- package/src/hooks/useImageUploaderProduct/helper/canvasUtils.ts +130 -0
- package/src/hooks/useImageUploaderProduct/helper/getOrientation.ts +53 -0
- package/src/hooks/useImageUploaderProduct/helper/index.ts +5 -0
- package/src/hooks/useImageUploaderProduct/index.ts +292 -0
- package/src/hooks/useImagesStore/index.js +100 -58
- package/src/hooks/useImagesStore/queries.js +2 -2
- package/src/hooks/useImagesStore/utils/index.js +4 -0
- package/src/hooks/useInventory/queries.js +1 -1
- package/src/hooks/useLocationManager/index.js +3 -1
- package/src/hooks/useLogout/helpers/fetchData.js +1 -1
- package/src/hooks/useLogout/helpers/logger.js +8 -8
- package/src/hooks/useLogout/index.js +6 -4
- package/src/hooks/useManageNewOrder/index.js +3 -3
- package/src/hooks/useManageQueryParams/index.js +28 -28
- package/src/hooks/useMobile/index.js +38 -8
- package/src/hooks/useOrderStatusTypes/index.ts +2 -0
- package/src/hooks/useOrderStatusTypes/useOrderStatusTypes/index.ts +52 -0
- package/src/hooks/useOrderStatusTypes/useUpdateOrderStatusPriorities/index.ts +97 -0
- package/src/hooks/useOrders/index.js +2 -74
- package/src/hooks/useOrders/queries.js +31 -195
- package/src/hooks/useOrders/useChangeOrderState/index.ts +125 -0
- package/src/hooks/useOrders/useOrdersFromStore/index.ts +83 -0
- package/src/hooks/usePortFetcher/index.ts +33 -0
- package/src/hooks/useProductsFood/index.js +3 -3
- package/src/hooks/useProductsFood/queriesStore.js +120 -64
- package/src/hooks/useProductsFood/useEditProduct.js +42 -2
- package/src/hooks/useProductsFood/usetagsProducts.js +47 -43
- package/src/hooks/useRemoveExtraProductFoodsOptional/queries.js +4 -4
- package/src/hooks/useSales/index.js +29 -19
- package/src/hooks/useSales/queries.js +140 -162
- package/src/hooks/useSales/useGetSale.js +3 -3
- package/src/hooks/useSaveAvailableProduct/index.js +1 -0
- package/src/hooks/useSetImageProducts/index.js +42 -13
- package/src/hooks/useSetImageProducts/queries.js +5 -0
- package/src/hooks/useStore/index.js +5 -1
- package/src/hooks/useStore/queries.js +71 -72
- package/src/hooks/useTagProducts/index.ts +3 -0
- package/src/hooks/useTagProducts/useDeleteOneTag.ts +106 -0
- package/src/hooks/useTagProducts/useGetAllTags.ts +68 -0
- package/src/hooks/useTagProducts/useRegisterMultipleTags.ts +156 -0
- package/src/hooks/useUpdateDashboardComponent/index.ts +91 -0
- package/src/hooks/useUpdateExistingOrders/index.js +3 -9
- package/src/hooks/useUpdateExtProductFoodsSubOptional/index.js +25 -3
- package/src/hooks/useUpdateMultipleExtProduct/index.js +33 -0
- package/src/hooks/useUpdateMultipleExtProduct/queries.js +33 -0
- package/src/hooks/useUploadProducts/index.js +0 -1
- package/src/hooks/useUser/queries.js +0 -1
- package/src/utils/index.js +97 -5
- package/tsconfig.json +5 -0
- package/src/hooks/useUpdateMultipleExtProductFoods/index.js +0 -21
- package/src/hooks/useUpdateMultipleExtProductFoods/queries.js +0 -19
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export const useFormatDate = ({
|
|
2
2
|
date,
|
|
3
|
-
local = 'ES'
|
|
3
|
+
local = 'ES-CO'
|
|
4
4
|
} = {}) => {
|
|
5
5
|
const dateToFormat = new Date(date ?? Date.now())
|
|
6
6
|
const fullDate = dateToFormat.toLocaleDateString(local, { year: 'numeric', month: '2-digit', day: '2-digit' })
|
|
7
7
|
const day = fullDate.split('/')[0]
|
|
8
8
|
const month = fullDate.split('/')[1]
|
|
9
9
|
const year = fullDate.split('/')[2]
|
|
10
|
-
const yearMonthDay = dateToFormat.toLocaleDateString('
|
|
10
|
+
const yearMonthDay = dateToFormat.toLocaleDateString('ES-CO')
|
|
11
11
|
const numberDay = dateToFormat.getDay()
|
|
12
12
|
const shortDayName = dateToFormat.toLocaleDateString(local, { weekday: 'short' })
|
|
13
13
|
const longDayName = dateToFormat.toLocaleDateString(local, { weekday: 'long' })
|
|
@@ -17,9 +17,61 @@ export const useFormatDate = ({
|
|
|
17
17
|
const hourPmAm = new Date(`1/1/1999 ${hour}`).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
|
|
18
18
|
return hour ? hourPmAm : ''
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Formats a date into Colombian time zone and Spanish locale for POS display.
|
|
22
|
+
* Example output: "15 de mayo de 2025 a las 14:23"
|
|
23
|
+
*
|
|
24
|
+
* @param {string | number | Date} dateInput - Date to format (ISO string, timestamp or Date object)
|
|
25
|
+
* @returns {string} - Formatted date string in Colombian time
|
|
26
|
+
*/
|
|
27
|
+
function formatDateInTimeZone (dateInput) {
|
|
28
|
+
const timeZone = 'America/Bogota'
|
|
29
|
+
const locale = 'es-CO'
|
|
30
|
+
const options = {
|
|
31
|
+
timeZone,
|
|
32
|
+
year: 'numeric',
|
|
33
|
+
month: 'long',
|
|
34
|
+
day: '2-digit',
|
|
35
|
+
hour: '2-digit',
|
|
36
|
+
minute: '2-digit',
|
|
37
|
+
hour12: false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const date = new Date(dateInput)
|
|
41
|
+
const formatted = new Intl.DateTimeFormat(locale, options).format(date)
|
|
42
|
+
|
|
43
|
+
// Cambiar coma por "a las" para mejor legibilidad
|
|
44
|
+
return formatted.replace(',', ' a las')
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Formats a date into YYYY-MM-DD using a given IANA timezone.
|
|
48
|
+
*
|
|
49
|
+
* @param {string | number | Date} inputDate - The date to format.
|
|
50
|
+
* @param {string} timeZone - IANA timezone string (default is "America/Bogota").
|
|
51
|
+
* @returns {string} Formatted date string in YYYY-MM-DD.
|
|
52
|
+
* @throws {Error} If the input date is invalid.
|
|
53
|
+
*/
|
|
54
|
+
const formatToLocalDateYMD = (inputDate, timeZone = 'America/Bogota') => {
|
|
55
|
+
const date = new Date(inputDate)
|
|
56
|
+
|
|
57
|
+
if (isNaN(date.getTime())) {
|
|
58
|
+
throw new Error('Invalid date input provided to formatToLocalDateYMD')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const localDateStr = date.toLocaleString('en-US', { timeZone })
|
|
62
|
+
const localDate = new Date(localDateStr)
|
|
63
|
+
|
|
64
|
+
const year = localDate.getFullYear()
|
|
65
|
+
const month = String(localDate.getMonth() + 1).padStart(2, '0')
|
|
66
|
+
const day = String(localDate.getDate()).padStart(2, '0')
|
|
67
|
+
|
|
68
|
+
return `${year}-${month}-${day}`
|
|
69
|
+
}
|
|
70
|
+
|
|
20
71
|
return {
|
|
21
72
|
day,
|
|
22
73
|
fullDate,
|
|
74
|
+
formatToLocalDateYMD,
|
|
23
75
|
hourMinutes12,
|
|
24
76
|
numberDay,
|
|
25
77
|
yearMonthDay,
|
|
@@ -28,6 +80,7 @@ export const useFormatDate = ({
|
|
|
28
80
|
shortDayName,
|
|
29
81
|
month,
|
|
30
82
|
year,
|
|
31
|
-
handleHourPmAM
|
|
83
|
+
handleHourPmAM,
|
|
84
|
+
formatDateInTimeZone
|
|
32
85
|
}
|
|
33
86
|
}
|
|
@@ -6,7 +6,7 @@ export const useGetStoreCookie = () => {
|
|
|
6
6
|
|
|
7
7
|
useEffect(() => {
|
|
8
8
|
const getCookieValue = () => {
|
|
9
|
-
const cookieValue = Cookies.get(process.env.
|
|
9
|
+
const cookieValue = Cookies.get(process.env.NEXT_PUBLIC_SESSION_NAME)
|
|
10
10
|
console.log('Cookie Value:', cookieValue)
|
|
11
11
|
|
|
12
12
|
if (cookieValue) {
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export const createImage = (url) =>
|
|
2
|
+
new Promise((resolve, reject) => {
|
|
3
|
+
const image = new Image()
|
|
4
|
+
image.addEventListener('load', () => resolve(image))
|
|
5
|
+
image.addEventListener('error', (error) => reject(error))
|
|
6
|
+
image.setAttribute('crossOrigin', 'anonymous') // needed to avoid cross-origin issues on CodeSandbox
|
|
7
|
+
image.src = url
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export function getRadianAngle(degreeValue) {
|
|
11
|
+
return (degreeValue * Math.PI) / 180
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Returns the new bounding area of a rotated rectangle.
|
|
16
|
+
*/
|
|
17
|
+
export function rotateSize(width, height, rotation) {
|
|
18
|
+
const rotRad = getRadianAngle(rotation)
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
width:
|
|
22
|
+
Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
|
|
23
|
+
height:
|
|
24
|
+
Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
|
|
33
|
+
*/
|
|
34
|
+
export async function getCroppedImg(
|
|
35
|
+
imageSrc,
|
|
36
|
+
pixelCrop,
|
|
37
|
+
rotation = 0,
|
|
38
|
+
flip = { horizontal: false, vertical: false }
|
|
39
|
+
) {
|
|
40
|
+
const image = await createImage(imageSrc)
|
|
41
|
+
const canvas = document.createElement('canvas')
|
|
42
|
+
const ctx = canvas.getContext('2d')
|
|
43
|
+
|
|
44
|
+
if (!ctx) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const rotRad = getRadianAngle(rotation)
|
|
49
|
+
|
|
50
|
+
// calculate bounding box of the rotated image
|
|
51
|
+
const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
|
|
52
|
+
image.width,
|
|
53
|
+
image.height,
|
|
54
|
+
rotation
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// set canvas size to match the bounding box
|
|
58
|
+
canvas.width = bBoxWidth
|
|
59
|
+
canvas.height = bBoxHeight
|
|
60
|
+
|
|
61
|
+
// translate canvas context to a central location to allow rotating and flipping around the center
|
|
62
|
+
ctx.translate(bBoxWidth / 2, bBoxHeight / 2)
|
|
63
|
+
ctx.rotate(rotRad)
|
|
64
|
+
ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1)
|
|
65
|
+
ctx.translate(-image.width / 2, -image.height / 2)
|
|
66
|
+
|
|
67
|
+
// draw rotated image
|
|
68
|
+
ctx.drawImage(image, 0, 0)
|
|
69
|
+
|
|
70
|
+
const croppedCanvas = document.createElement('canvas')
|
|
71
|
+
|
|
72
|
+
const croppedCtx = croppedCanvas.getContext('2d')
|
|
73
|
+
|
|
74
|
+
if (!croppedCtx) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Set the size of the cropped canvas
|
|
79
|
+
croppedCanvas.width = pixelCrop.width
|
|
80
|
+
croppedCanvas.height = pixelCrop.height
|
|
81
|
+
|
|
82
|
+
// Draw the cropped image onto the new canvas
|
|
83
|
+
croppedCtx.drawImage(
|
|
84
|
+
canvas,
|
|
85
|
+
pixelCrop.x,
|
|
86
|
+
pixelCrop.y,
|
|
87
|
+
pixelCrop.width,
|
|
88
|
+
pixelCrop.height,
|
|
89
|
+
0,
|
|
90
|
+
0,
|
|
91
|
+
pixelCrop.width,
|
|
92
|
+
pixelCrop.height
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
// As Base64 string
|
|
96
|
+
// return croppedCanvas.toDataURL('image/jpeg');
|
|
97
|
+
|
|
98
|
+
// As a blob
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
croppedCanvas.toBlob((file) => {
|
|
101
|
+
resolve(URL.createObjectURL(file))
|
|
102
|
+
}, 'image/png')
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function getRotatedImage(imageSrc, rotation = 0) {
|
|
107
|
+
const image = await createImage(imageSrc)
|
|
108
|
+
const canvas = document.createElement('canvas')
|
|
109
|
+
const ctx = canvas.getContext('2d')
|
|
110
|
+
|
|
111
|
+
const orientationChanged =
|
|
112
|
+
rotation === 90 || rotation === -90 || rotation === 270 || rotation === -270
|
|
113
|
+
if (orientationChanged) {
|
|
114
|
+
canvas.width = image.height
|
|
115
|
+
canvas.height = image.width
|
|
116
|
+
} else {
|
|
117
|
+
canvas.width = image.width
|
|
118
|
+
canvas.height = image.height
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
ctx.translate(canvas.width / 2, canvas.height / 2)
|
|
122
|
+
ctx.rotate((rotation * Math.PI) / 180)
|
|
123
|
+
ctx.drawImage(image, -image.width / 2, -image.height / 2)
|
|
124
|
+
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
canvas.toBlob((file) => {
|
|
127
|
+
resolve(URL.createObjectURL(file))
|
|
128
|
+
}, 'image/png')
|
|
129
|
+
})
|
|
130
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads EXIF orientation from a JPEG file (native, no dependencies)
|
|
3
|
+
* @param {File} file
|
|
4
|
+
* @returns {Promise<number>} Orientation value from 1 to 8
|
|
5
|
+
*/
|
|
6
|
+
export const getOrientation = (file: File, validTypes: string[]): Promise<number> => {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const reader = new FileReader()
|
|
9
|
+
reader.onload = (event) => {
|
|
10
|
+
const view = new DataView(event.target?.result as ArrayBuffer)
|
|
11
|
+
|
|
12
|
+
if (!validTypes.includes(file.type)) {
|
|
13
|
+
return reject(new Error(`Archivo no soportado: ${file.type}`))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let offset = 2
|
|
17
|
+
const length = view.byteLength
|
|
18
|
+
|
|
19
|
+
while (offset < length) {
|
|
20
|
+
if (view.getUint16(offset + 2, false) <= 8) return resolve(1)
|
|
21
|
+
|
|
22
|
+
const marker = view.getUint16(offset, false)
|
|
23
|
+
offset += 2
|
|
24
|
+
|
|
25
|
+
if (marker === 0xFFE1) {
|
|
26
|
+
const little = view.getUint16(offset + 8, false) === 0x4949
|
|
27
|
+
offset += 10
|
|
28
|
+
|
|
29
|
+
const tags = view.getUint16(offset, little)
|
|
30
|
+
offset += 2
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < tags; i++) {
|
|
33
|
+
const tagOffset = offset + i * 12
|
|
34
|
+
const tag = view.getUint16(tagOffset, little)
|
|
35
|
+
if (tag === 0x0112) {
|
|
36
|
+
const orientation = view.getUint16(tagOffset + 8, little)
|
|
37
|
+
return resolve(orientation)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else if ((marker & 0xFF00) !== 0xFF00) {
|
|
41
|
+
break
|
|
42
|
+
} else {
|
|
43
|
+
offset += view.getUint16(offset, false)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resolve(1) // por defecto
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
reader.onerror = () => reject(new Error('Error leyendo archivo'))
|
|
51
|
+
reader.readAsArrayBuffer(file)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { useRef, useState } from 'react'
|
|
2
|
+
import { ORIENTATION_TO_ANGLE } from './helper'
|
|
3
|
+
import { getCroppedImg, getRotatedImage } from './helper/canvasUtils'
|
|
4
|
+
import { getOrientation } from './helper/getOrientation'
|
|
5
|
+
|
|
6
|
+
function readFile(file: File): Promise<string> {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
const reader = new FileReader()
|
|
9
|
+
reader.addEventListener('load', () => resolve(reader.result as string), false)
|
|
10
|
+
reader.readAsDataURL(file)
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SendNotificationFn = (params: {
|
|
15
|
+
description: string
|
|
16
|
+
title: string
|
|
17
|
+
backgroundColor: string
|
|
18
|
+
}) => void
|
|
19
|
+
|
|
20
|
+
interface UseImageUploaderOptions {
|
|
21
|
+
maxSizeMB?: number
|
|
22
|
+
minHeight?: number
|
|
23
|
+
minWidth?: number
|
|
24
|
+
validTypes?: string[]
|
|
25
|
+
sendNotification: SendNotificationFn
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface UseImageUploaderResult {
|
|
29
|
+
crop: { x: number, y: number }
|
|
30
|
+
croppedImage: string | null
|
|
31
|
+
error: string
|
|
32
|
+
image: File
|
|
33
|
+
imageSrc: string | null
|
|
34
|
+
inputRef: React.RefObject<HTMLInputElement>
|
|
35
|
+
loading: boolean
|
|
36
|
+
open: boolean
|
|
37
|
+
preview: string | null
|
|
38
|
+
formattedList: string
|
|
39
|
+
rotation: number
|
|
40
|
+
zoom: number
|
|
41
|
+
validTypes: string[]
|
|
42
|
+
handleClose: () => void
|
|
43
|
+
handleRemoveImage: () => void
|
|
44
|
+
onCropComplete: (croppedArea: any, croppedPixels: any) => void
|
|
45
|
+
onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => Promise<void>
|
|
46
|
+
setCrop: React.Dispatch<React.SetStateAction<{ x: number, y: number }>>
|
|
47
|
+
setRotation: React.Dispatch<React.SetStateAction<number>>
|
|
48
|
+
setZoom: React.Dispatch<React.SetStateAction<number>>
|
|
49
|
+
handleDrop: (event: React.DragEvent<HTMLDivElement>) => Promise<void>
|
|
50
|
+
showCroppedImage: () => Promise<void>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hook for managing image selection, preview, validation and cropping
|
|
55
|
+
* @param {UseImageUploaderOptions} options
|
|
56
|
+
* @returns {UseImageUploaderResult}
|
|
57
|
+
*/
|
|
58
|
+
export const useImageUploaderProduct = (
|
|
59
|
+
options?: UseImageUploaderOptions
|
|
60
|
+
): UseImageUploaderResult => {
|
|
61
|
+
const {
|
|
62
|
+
validTypes = ['image/jpeg', 'image/jpg', 'image/png'],
|
|
63
|
+
maxSizeMB = 20,
|
|
64
|
+
minWidth = 300,
|
|
65
|
+
minHeight = 275,
|
|
66
|
+
sendNotification
|
|
67
|
+
} = options ?? {
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
const readableFormats = validTypes
|
|
71
|
+
.map((type) => type.split('/')[1].toUpperCase())
|
|
72
|
+
|
|
73
|
+
const formatter = new Intl.ListFormat('es', {
|
|
74
|
+
style: 'long',
|
|
75
|
+
type: 'conjunction'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const formattedList = formatter.format(readableFormats)
|
|
79
|
+
|
|
80
|
+
const [loading, setLoading] = useState(false)
|
|
81
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
82
|
+
const [open, setOpen] = useState(false)
|
|
83
|
+
const [preview, setPreview] = useState<string | null>(null)
|
|
84
|
+
const [imageSrc, setImageSrc] = useState<string | null>(null)
|
|
85
|
+
const [croppedImage, setCroppedImage] = useState<string | null>(null)
|
|
86
|
+
const [crop, setCrop] = useState({ x: 0, y: 0 })
|
|
87
|
+
const [zoom, setZoom] = useState(1)
|
|
88
|
+
const [rotation, setRotation] = useState(0)
|
|
89
|
+
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null)
|
|
90
|
+
const [error, setError] = useState('')
|
|
91
|
+
const [image, setImage] = useState<File | null>(null)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
const onCropComplete = (_: any, croppedPixels: any) => {
|
|
95
|
+
setCroppedAreaPixels(croppedPixels)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const showCroppedImage = async () => {
|
|
99
|
+
if (!imageSrc || !croppedAreaPixels) return
|
|
100
|
+
try {
|
|
101
|
+
setLoading(true)
|
|
102
|
+
const base64Image = await getCroppedImg(imageSrc, croppedAreaPixels, rotation) as string
|
|
103
|
+
|
|
104
|
+
setCroppedImage(base64Image)
|
|
105
|
+
setPreview(base64Image)
|
|
106
|
+
|
|
107
|
+
// ✅ Convert base64 to blob and file
|
|
108
|
+
const blob = await (await fetch(base64Image)).blob()
|
|
109
|
+
const file = new File([blob], image?.name ?? 'cropped.jpeg', { type: blob.type })
|
|
110
|
+
setImage(file)
|
|
111
|
+
setLoading(false)
|
|
112
|
+
} catch (e) {
|
|
113
|
+
setLoading(false)
|
|
114
|
+
return sendNotification({
|
|
115
|
+
description: e ?? 'Ocurrió un error',
|
|
116
|
+
title: 'Error',
|
|
117
|
+
backgroundColor: 'error'
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Removes the selected image and resets relevant states
|
|
124
|
+
*/
|
|
125
|
+
const handleRemoveImage = () => {
|
|
126
|
+
setImage(null)
|
|
127
|
+
setPreview(null)
|
|
128
|
+
setImageSrc(null)
|
|
129
|
+
setCroppedImage(null)
|
|
130
|
+
setZoom(1)
|
|
131
|
+
setCrop({ x: 0, y: 0 })
|
|
132
|
+
setRotation(0)
|
|
133
|
+
setError('')
|
|
134
|
+
if (inputRef.current) inputRef.current.value = ''
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
138
|
+
const file = e.target.files?.[0]
|
|
139
|
+
handleRemoveImage()
|
|
140
|
+
if (!file) return
|
|
141
|
+
|
|
142
|
+
if (!validTypes.includes(file.type)) {
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
const error = `Formato inválido. Solo se permiten: ${formattedList}.`
|
|
146
|
+
sendNotification({
|
|
147
|
+
description: error,
|
|
148
|
+
title: 'Error',
|
|
149
|
+
backgroundColor: 'error'
|
|
150
|
+
})
|
|
151
|
+
return setError(error)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
155
|
+
const error = `El archivo supera los ${maxSizeMB} MB.`
|
|
156
|
+
sendNotification({
|
|
157
|
+
description: error,
|
|
158
|
+
title: 'Error',
|
|
159
|
+
backgroundColor: 'error'
|
|
160
|
+
})
|
|
161
|
+
return setError(`El archivo supera los ${maxSizeMB} MB.`)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let imageDataUrl = await readFile(file)
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const orientation = await getOrientation(file, validTypes)
|
|
168
|
+
const rotation = ORIENTATION_TO_ANGLE[orientation]
|
|
169
|
+
if (rotation) {
|
|
170
|
+
imageDataUrl = await getRotatedImage(imageDataUrl, rotation) as string
|
|
171
|
+
setRotation(rotation)
|
|
172
|
+
}
|
|
173
|
+
} catch (e) {
|
|
174
|
+
return sendNotification({
|
|
175
|
+
description: 'Error desconocido',
|
|
176
|
+
title: 'Error',
|
|
177
|
+
backgroundColor: 'error'
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const image = new Image()
|
|
182
|
+
image.src = imageDataUrl
|
|
183
|
+
image.onload = () => {
|
|
184
|
+
if (image.width < minWidth || image.height < minHeight) {
|
|
185
|
+
const error = `Resolución mínima: ${minWidth}x${minHeight}px.`
|
|
186
|
+
sendNotification({
|
|
187
|
+
description: error,
|
|
188
|
+
title: 'Error',
|
|
189
|
+
backgroundColor: 'error'
|
|
190
|
+
})
|
|
191
|
+
setError(error)
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
setImageSrc(imageDataUrl)
|
|
195
|
+
setOpen(true)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const handleClose = () => {
|
|
200
|
+
setOpen(!open)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const handleDrop = async (event: React.DragEvent<HTMLDivElement>): Promise<void> => {
|
|
204
|
+
event.preventDefault()
|
|
205
|
+
handleRemoveImage()
|
|
206
|
+
const file = event.dataTransfer.files?.[0]
|
|
207
|
+
if (!file) return
|
|
208
|
+
|
|
209
|
+
if (!validTypes.includes(file.type)) {
|
|
210
|
+
const error = `Formato inválido. Solo se permiten: ${formattedList}.`
|
|
211
|
+
sendNotification({
|
|
212
|
+
description: error,
|
|
213
|
+
title: 'Error',
|
|
214
|
+
backgroundColor: 'error'
|
|
215
|
+
})
|
|
216
|
+
return setError(error)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (file.size > maxSizeMB * 1024 * 1024) {
|
|
220
|
+
const error = `El archivo supera los ${maxSizeMB} MB.`
|
|
221
|
+
sendNotification({
|
|
222
|
+
description: error,
|
|
223
|
+
title: 'Error',
|
|
224
|
+
backgroundColor: 'error'
|
|
225
|
+
})
|
|
226
|
+
return setError(error)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let imageDataUrl = await readFile(file)
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const orientation = await getOrientation(file, validTypes)
|
|
233
|
+
const rotation = ORIENTATION_TO_ANGLE[orientation]
|
|
234
|
+
if (rotation) {
|
|
235
|
+
imageDataUrl = await getRotatedImage(imageDataUrl, rotation) as string
|
|
236
|
+
setRotation(rotation)
|
|
237
|
+
}
|
|
238
|
+
} catch (e) {
|
|
239
|
+
return sendNotification({
|
|
240
|
+
description: 'Error al procesar la orientación de la imagen',
|
|
241
|
+
title: 'Error',
|
|
242
|
+
backgroundColor: 'error'
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const image = new Image()
|
|
247
|
+
image.src = imageDataUrl
|
|
248
|
+
image.onload = () => {
|
|
249
|
+
if (image.width < minWidth || image.height < minHeight) {
|
|
250
|
+
const error = `Resolución mínima: ${minWidth}x${minHeight}px.`
|
|
251
|
+
sendNotification({
|
|
252
|
+
description: error,
|
|
253
|
+
title: 'Error',
|
|
254
|
+
backgroundColor: 'error'
|
|
255
|
+
})
|
|
256
|
+
setError(error)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
setImageSrc(imageDataUrl)
|
|
260
|
+
setImage(file)
|
|
261
|
+
setOpen(true)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
inputRef,
|
|
270
|
+
open,
|
|
271
|
+
imageSrc,
|
|
272
|
+
preview,
|
|
273
|
+
croppedImage,
|
|
274
|
+
crop,
|
|
275
|
+
zoom,
|
|
276
|
+
image,
|
|
277
|
+
rotation,
|
|
278
|
+
error,
|
|
279
|
+
formattedList,
|
|
280
|
+
loading,
|
|
281
|
+
validTypes,
|
|
282
|
+
onCropComplete,
|
|
283
|
+
showCroppedImage,
|
|
284
|
+
handleRemoveImage,
|
|
285
|
+
handleDrop,
|
|
286
|
+
onFileChange,
|
|
287
|
+
handleClose,
|
|
288
|
+
setCrop,
|
|
289
|
+
setZoom,
|
|
290
|
+
setRotation,
|
|
291
|
+
}
|
|
292
|
+
}
|