@pagamio/frontend-commons-lib 0.8.268 → 0.8.269
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/lib/components/ui/ResponsiveImage.d.ts +34 -0
- package/lib/components/ui/ResponsiveImage.js +39 -0
- package/lib/components/ui/index.d.ts +2 -0
- package/lib/components/ui/index.js +1 -0
- package/lib/shared/hooks/useImageUpload.d.ts +2 -0
- package/lib/shared/hooks/useImageUpload.js +48 -1
- package/lib/shared/utils/functionHelper.d.ts +26 -1
- package/lib/shared/utils/functionHelper.js +19 -1
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { MultiSizeImageResult } from '../../shared/utils/functionHelper';
|
|
3
|
+
export interface ResponsiveImageProps extends Omit<React.ImgHTMLAttributes<HTMLImageElement>, 'src' | 'srcSet'> {
|
|
4
|
+
/**
|
|
5
|
+
* Image source - can be:
|
|
6
|
+
* - MultiSizeImageResult object (thumb, medium, large, srcset, sizes)
|
|
7
|
+
* - string URL (backward compatible - will use as single source)
|
|
8
|
+
*/
|
|
9
|
+
src: MultiSizeImageResult | string;
|
|
10
|
+
/** Alt text for accessibility */
|
|
11
|
+
alt: string;
|
|
12
|
+
/** Optional custom sizes attribute (overrides default from MultiSizeImageResult) */
|
|
13
|
+
customSizes?: string;
|
|
14
|
+
/** Fallback image URL if src fails to load */
|
|
15
|
+
fallback?: string;
|
|
16
|
+
/** Eager loading for above-the-fold images */
|
|
17
|
+
priority?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* ResponsiveImage component that handles both multi-size responsive images and legacy single URLs
|
|
21
|
+
*
|
|
22
|
+
* Usage with multi-size:
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const imageData = await uploadFileMultiSize(file);
|
|
25
|
+
* <ResponsiveImage src={imageData} alt="Product" />
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Usage with legacy single URL (backward compatible):
|
|
29
|
+
* ```tsx
|
|
30
|
+
* <ResponsiveImage src={product.imageUrl} alt="Product" />
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
declare const ResponsiveImage: React.ForwardRefExoticComponent<ResponsiveImageProps & React.RefAttributes<HTMLImageElement>>;
|
|
34
|
+
export default ResponsiveImage;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { cn } from '../../helpers';
|
|
4
|
+
/**
|
|
5
|
+
* ResponsiveImage component that handles both multi-size responsive images and legacy single URLs
|
|
6
|
+
*
|
|
7
|
+
* Usage with multi-size:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const imageData = await uploadFileMultiSize(file);
|
|
10
|
+
* <ResponsiveImage src={imageData} alt="Product" />
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Usage with legacy single URL (backward compatible):
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <ResponsiveImage src={product.imageUrl} alt="Product" />
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
const ResponsiveImage = React.forwardRef(({ src, alt, customSizes, fallback = '/images/product-placeholder.png', priority = false, className, ...props }, ref) => {
|
|
19
|
+
const [imgSrc, setImgSrc] = React.useState(() => {
|
|
20
|
+
if (typeof src === 'string')
|
|
21
|
+
return src;
|
|
22
|
+
return src.medium; // Default to medium for multi-size
|
|
23
|
+
});
|
|
24
|
+
const [hasError, setHasError] = React.useState(false);
|
|
25
|
+
const handleError = () => {
|
|
26
|
+
if (!hasError && fallback) {
|
|
27
|
+
setHasError(true);
|
|
28
|
+
setImgSrc(fallback);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
// Multi-size responsive image
|
|
32
|
+
if (typeof src === 'object' && 'srcset' in src) {
|
|
33
|
+
return (_jsx("img", { ref: ref, src: imgSrc, srcSet: src.srcset, sizes: customSizes || src.sizes, alt: alt, loading: priority ? 'eager' : 'lazy', onError: handleError, className: cn(className), ...props }));
|
|
34
|
+
}
|
|
35
|
+
// Legacy single URL (backward compatible)
|
|
36
|
+
return (_jsx("img", { ref: ref, src: imgSrc, alt: alt, loading: priority ? 'eager' : 'lazy', onError: handleError, className: cn(className), ...props }));
|
|
37
|
+
});
|
|
38
|
+
ResponsiveImage.displayName = 'ResponsiveImage';
|
|
39
|
+
export default ResponsiveImage;
|
|
@@ -41,6 +41,8 @@ export { default as MultiSelect, type MultiSelectProps } from './MultiSelect';
|
|
|
41
41
|
export { default as PhoneInput, type PhoneInputProps } from './PhoneInput';
|
|
42
42
|
export { default as FilterList, type FilterListProps } from './FilterList';
|
|
43
43
|
export { default as FilterSection, type FilterSectionProps } from './FilterSection';
|
|
44
|
+
export { default as ResponsiveImage } from './ResponsiveImage';
|
|
45
|
+
export type { ResponsiveImageProps } from './ResponsiveImage';
|
|
44
46
|
export { default as Separator } from './Separator';
|
|
45
47
|
export { default as Switch } from './Switch';
|
|
46
48
|
export { default as StatusCell } from './StatusCell';
|
|
@@ -43,6 +43,7 @@ export { default as MultiSelect } from './MultiSelect';
|
|
|
43
43
|
export { default as PhoneInput } from './PhoneInput';
|
|
44
44
|
export { default as FilterList } from './FilterList';
|
|
45
45
|
export { default as FilterSection } from './FilterSection';
|
|
46
|
+
export { default as ResponsiveImage } from './ResponsiveImage';
|
|
46
47
|
export { default as Separator } from './Separator';
|
|
47
48
|
export { default as Switch } from './Switch';
|
|
48
49
|
export { default as StatusCell } from './StatusCell';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MultiSizeImageResult } from '../utils/functionHelper';
|
|
1
2
|
export interface UploadResponse {
|
|
2
3
|
uploadURL: string;
|
|
3
4
|
publicURL: string;
|
|
@@ -12,6 +13,7 @@ export interface UseImageUploadProps {
|
|
|
12
13
|
export declare const useImageUpload: ({ project, env, endpoint, processImage }: UseImageUploadProps) => {
|
|
13
14
|
getPresignedUrl: (fileName: string, contentType: string) => Promise<UploadResponse>;
|
|
14
15
|
uploadFile: (rawFile: File) => Promise<string>;
|
|
16
|
+
uploadFileMultiSize: (rawFile: File, minDimension?: number) => Promise<MultiSizeImageResult>;
|
|
15
17
|
isLoading: boolean;
|
|
16
18
|
error: Error | null;
|
|
17
19
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useState } from 'react';
|
|
2
|
-
import { generateSecureRandomString, processImageForUpload, uploadFileWithXHR } from '../utils/functionHelper';
|
|
2
|
+
import { generateSecureRandomString, processImageForUpload, processImageForUploadMultiSize, uploadFileWithXHR, } from '../utils/functionHelper';
|
|
3
3
|
export const useImageUpload = ({ project, env, endpoint, processImage = true }) => {
|
|
4
4
|
const resolvedEndpoint = endpoint;
|
|
5
5
|
if (!resolvedEndpoint) {
|
|
@@ -61,9 +61,56 @@ export const useImageUpload = ({ project, env, endpoint, processImage = true })
|
|
|
61
61
|
throw error;
|
|
62
62
|
}
|
|
63
63
|
}, [getPresignedUrl, processImage]);
|
|
64
|
+
/**
|
|
65
|
+
* Uploads image in 3 sizes (thumb, medium, large) and returns structured result with srcset
|
|
66
|
+
* Medium URL is backward-compatible with existing code that expects a single string
|
|
67
|
+
*/
|
|
68
|
+
const uploadFileMultiSize = useCallback(async (rawFile, minDimension = 200) => {
|
|
69
|
+
try {
|
|
70
|
+
// Process image into 3 sizes
|
|
71
|
+
const { thumb, medium, large } = await processImageForUploadMultiSize(rawFile, minDimension);
|
|
72
|
+
const timestamp = Date.now();
|
|
73
|
+
const randomString = generateSecureRandomString();
|
|
74
|
+
const baseName = `${timestamp}_${randomString}`;
|
|
75
|
+
// Upload all 3 versions in parallel
|
|
76
|
+
const [thumbResult, mediumResult, largeResult] = await Promise.all([
|
|
77
|
+
(async () => {
|
|
78
|
+
const { uploadURL, publicURL } = await getPresignedUrl(`${baseName}-thumb.webp`, 'image/webp');
|
|
79
|
+
await uploadFileWithXHR(uploadURL, thumb);
|
|
80
|
+
return publicURL;
|
|
81
|
+
})(),
|
|
82
|
+
(async () => {
|
|
83
|
+
const { uploadURL, publicURL } = await getPresignedUrl(`${baseName}-medium.webp`, 'image/webp');
|
|
84
|
+
await uploadFileWithXHR(uploadURL, medium);
|
|
85
|
+
return publicURL;
|
|
86
|
+
})(),
|
|
87
|
+
(async () => {
|
|
88
|
+
const { uploadURL, publicURL } = await getPresignedUrl(`${baseName}-large.webp`, 'image/webp');
|
|
89
|
+
await uploadFileWithXHR(uploadURL, large);
|
|
90
|
+
return publicURL;
|
|
91
|
+
})(),
|
|
92
|
+
]);
|
|
93
|
+
// Build srcset and sizes attributes
|
|
94
|
+
const srcset = `${thumbResult} 400w, ${mediumResult} 1200w, ${largeResult} 1920w`;
|
|
95
|
+
const sizes = '(max-width: 640px) 400px, (max-width: 1024px) 1200px, 1920px';
|
|
96
|
+
return {
|
|
97
|
+
thumb: thumbResult,
|
|
98
|
+
medium: mediumResult,
|
|
99
|
+
large: largeResult,
|
|
100
|
+
srcset,
|
|
101
|
+
sizes,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const error = err instanceof Error ? err : new Error('Multi-size upload failed');
|
|
106
|
+
setError(error);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}, [getPresignedUrl]);
|
|
64
110
|
return {
|
|
65
111
|
getPresignedUrl,
|
|
66
112
|
uploadFile,
|
|
113
|
+
uploadFileMultiSize,
|
|
67
114
|
isLoading,
|
|
68
115
|
error,
|
|
69
116
|
};
|
|
@@ -31,5 +31,30 @@ export declare function uploadFileWithXHR(url: string, file: File, onProgress?:
|
|
|
31
31
|
* - Resizes to fit within maxDimension×maxDimension (never upscales)
|
|
32
32
|
* - Strips EXIF metadata (canvas read-back discards it)
|
|
33
33
|
* - Converts to WebP at the given quality (0–1)
|
|
34
|
+
* - Validates minimum dimensions
|
|
34
35
|
*/
|
|
35
|
-
export declare function processImageForUpload(file: File, maxDimension?: number, quality?: number): Promise<File>;
|
|
36
|
+
export declare function processImageForUpload(file: File, maxDimension?: number, quality?: number, minDimension?: number): Promise<File>;
|
|
37
|
+
/**
|
|
38
|
+
* Multi-size image response containing all variants and metadata
|
|
39
|
+
*/
|
|
40
|
+
export interface MultiSizeImageResult {
|
|
41
|
+
/** Thumbnail URL (400px, ~10-20KB) - for cards, lists */
|
|
42
|
+
thumb: string;
|
|
43
|
+
/** Medium URL (1200px, ~80-120KB) - for detail pages, default */
|
|
44
|
+
medium: string;
|
|
45
|
+
/** Large URL (1920px, ~200-300KB) - for zoom, print */
|
|
46
|
+
large: string;
|
|
47
|
+
/** Responsive srcset string ready to use in img tags */
|
|
48
|
+
srcset: string;
|
|
49
|
+
/** Default sizes attribute for responsive images */
|
|
50
|
+
sizes: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Processes an image into 3 responsive sizes (thumb, medium, large)
|
|
54
|
+
* Returns File objects ready for upload with -thumb, -medium, -large suffixes
|
|
55
|
+
*/
|
|
56
|
+
export declare function processImageForUploadMultiSize(file: File, minDimension?: number): Promise<{
|
|
57
|
+
thumb: File;
|
|
58
|
+
medium: File;
|
|
59
|
+
large: File;
|
|
60
|
+
}>;
|
|
@@ -134,14 +134,19 @@ export function uploadFileWithXHR(url, file, onProgress) {
|
|
|
134
134
|
* - Resizes to fit within maxDimension×maxDimension (never upscales)
|
|
135
135
|
* - Strips EXIF metadata (canvas read-back discards it)
|
|
136
136
|
* - Converts to WebP at the given quality (0–1)
|
|
137
|
+
* - Validates minimum dimensions
|
|
137
138
|
*/
|
|
138
|
-
export function processImageForUpload(file, maxDimension = 1920, quality = 0.92) {
|
|
139
|
+
export function processImageForUpload(file, maxDimension = 1920, quality = 0.92, minDimension = 200) {
|
|
139
140
|
return new Promise((resolve, reject) => {
|
|
140
141
|
const img = new window.Image();
|
|
141
142
|
const objectUrl = URL.createObjectURL(file);
|
|
142
143
|
img.onload = () => {
|
|
143
144
|
URL.revokeObjectURL(objectUrl);
|
|
144
145
|
const { width, height } = img;
|
|
146
|
+
// Validate minimum dimensions
|
|
147
|
+
if (width < minDimension || height < minDimension) {
|
|
148
|
+
return reject(new Error(`Image too small. Minimum: ${minDimension}×${minDimension}px. Got: ${width}×${height}px`));
|
|
149
|
+
}
|
|
145
150
|
const scale = Math.min(1, maxDimension / Math.max(width, height));
|
|
146
151
|
const canvas = document.createElement('canvas');
|
|
147
152
|
canvas.width = Math.round(width * scale);
|
|
@@ -164,3 +169,16 @@ export function processImageForUpload(file, maxDimension = 1920, quality = 0.92)
|
|
|
164
169
|
img.src = objectUrl;
|
|
165
170
|
});
|
|
166
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Processes an image into 3 responsive sizes (thumb, medium, large)
|
|
174
|
+
* Returns File objects ready for upload with -thumb, -medium, -large suffixes
|
|
175
|
+
*/
|
|
176
|
+
export async function processImageForUploadMultiSize(file, minDimension = 200) {
|
|
177
|
+
// Generate all 3 versions in parallel
|
|
178
|
+
const [thumb, medium, large] = await Promise.all([
|
|
179
|
+
processImageForUpload(file, 400, 0.65, minDimension), // ~10-20KB
|
|
180
|
+
processImageForUpload(file, 1200, 0.8, minDimension), // ~80-120KB
|
|
181
|
+
processImageForUpload(file, 1920, 0.85, minDimension), // ~200-300KB
|
|
182
|
+
]);
|
|
183
|
+
return { thumb, medium, large };
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagamio/frontend-commons-lib",
|
|
3
3
|
"description": "Pagamio library for Frontend reusable components like the form engine and table container",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.269",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"provenance": false
|