@pagamio/frontend-commons-lib 0.8.267 → 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.
@@ -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';
@@ -53,7 +53,7 @@ const UploadFieldForm = forwardRef(({ field, error, ...props }, ref) => {
53
53
  return _jsx("span", { className: "text-muted-foreground", children: "File uploaded." });
54
54
  }, [preview, field.name]);
55
55
  const previewLabel = preview.type === 'pdf' ? 'Uploaded PDF:' : 'Uploaded Image:';
56
- return (_jsxs("div", { className: "flex flex-col space-y-4", children: [_jsxs("div", { className: "flex flex-col space-y-2", children: [_jsx("label", { htmlFor: field.name, className: "text-sm font-medium text-foreground", children: field.label }), _jsx(UploadField, { ...props, id: field.name, ref: ref, value: props.value || null, onChange: handleFileChange, className: "w-full p-2 border border-input rounded-md", hideUploadButton: field.hideUploadButton, allowedFileTypes: field.allowedFileTypes, helperText: field.fileUploadHelperText }), error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: error.message })] }), field.showFileUploadPreview && (_jsxs("div", { className: "mt-4", children: [_jsx("label", { htmlFor: `${field.name}-preview`, className: "text-sm font-medium text-foreground", children: previewLabel }), _jsx("div", { className: "mt-2 flex flex-col items-center p-4 border border-input rounded-md", children: previewElement })] }))] }));
56
+ return (_jsxs("div", { className: "flex flex-col space-y-4", children: [_jsxs("div", { className: "flex flex-col space-y-2", children: [_jsx("label", { htmlFor: field.name, className: "text-sm font-medium text-foreground", children: field.label }), _jsx(UploadField, { ...props, id: field.name, ref: ref, value: props.value || null, onChange: handleFileChange, className: "w-full p-2 border border-input rounded-md", hideUploadButton: field.hideUploadButton, allowedFileTypes: field.allowedFileTypes, maxFileSize: field.maxFileSize, helperText: field.fileUploadHelperText }), error && _jsx("p", { className: "mt-1 text-sm text-red-500", children: error.message })] }), field.showFileUploadPreview && (_jsxs("div", { className: "mt-4", children: [_jsx("label", { htmlFor: `${field.name}-preview`, className: "text-sm font-medium text-foreground", children: previewLabel }), _jsx("div", { className: "mt-2 flex flex-col items-center p-4 border border-input rounded-md", children: previewElement })] }))] }));
57
57
  });
58
58
  UploadFieldForm.displayName = 'UploadFieldForm';
59
59
  export default UploadFieldForm;
@@ -151,6 +151,8 @@ export interface Field {
151
151
  hideUploadButton?: boolean;
152
152
  /** To validate file types say .png, .pdf etc */
153
153
  allowedFileTypes?: string[];
154
+ /** Maximum file size in bytes for file uploads */
155
+ maxFileSize?: number;
154
156
  /** To hide or show a preview of uploaded image */
155
157
  showFileUploadPreview?: boolean;
156
158
  /** To display file upload helper text */
@@ -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.267",
4
+ "version": "0.8.269",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "provenance": false