@wazobiatech/wazui 0.0.10 → 0.0.12

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.
Files changed (77) hide show
  1. package/.storybook/main.ts +41 -0
  2. package/.storybook/preview-head.html +3 -0
  3. package/.storybook/preview.ts +53 -0
  4. package/package.json +9 -3
  5. package/src/assets/dashboard/book.svg +6 -0
  6. package/src/assets/dashboard/edit.svg +5 -0
  7. package/src/assets/dashboard/emerald.svg +9 -0
  8. package/src/assets/dashboard/eye.svg +4 -0
  9. package/src/assets/dashboard/gold.svg +9 -0
  10. package/src/assets/dashboard/percentage-square.svg +6 -0
  11. package/src/assets/dashboard/sapphire.svg +9 -0
  12. package/src/assets/dashboard/send-2.svg +4 -0
  13. package/src/assets/desktop.svg +7 -0
  14. package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.otf +0 -0
  15. package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.woff +0 -0
  16. package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.woff2 +0 -0
  17. package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.otf +0 -0
  18. package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.woff +0 -0
  19. package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.woff2 +0 -0
  20. package/src/assets/fonts/UniversalJungle/UniversalJungle-420.otf +0 -0
  21. package/src/assets/fonts/UniversalJungle/UniversalJungle-420.woff +0 -0
  22. package/src/assets/fonts/UniversalJungle/UniversalJungle-420.woff2 +0 -0
  23. package/src/assets/fonts/UniversalJungle/UniversalJungle-515.otf +0 -0
  24. package/src/assets/fonts/UniversalJungle/UniversalJungle-515.woff +0 -0
  25. package/src/assets/fonts/UniversalJungle/UniversalJungle-515.woff2 +0 -0
  26. package/src/assets/instagram.svg +9 -0
  27. package/src/assets/mobile.svg +4 -0
  28. package/src/assets/no-data-image.svg +41 -0
  29. package/src/assets/no-image-icon.svg +84 -0
  30. package/src/assets/pexels.svg +9 -0
  31. package/src/assets/react.svg +1 -0
  32. package/src/assets/sounds/draft.mp3 +0 -0
  33. package/src/assets/sounds/error.mp3 +0 -0
  34. package/src/assets/sounds/publish.mp3 +0 -0
  35. package/src/assets/tablet.svg +4 -0
  36. package/src/assets/unsplash.svg +3 -0
  37. package/src/assets/upload-image.svg +43 -0
  38. package/src/assets/user.svg +4 -0
  39. package/src/components/shadcn/index.ts +31 -0
  40. package/src/config/env.ts +15 -0
  41. package/src/contexts/ThemeContext.tsx +11 -0
  42. package/src/data/users.json +6014 -0
  43. package/src/decorators/ActionModalDecorator.tsx +10 -0
  44. package/src/decorators/ApolloDecorator.tsx +19 -0
  45. package/src/decorators/DolosApolloDecorator.tsx +31 -0
  46. package/src/decorators/JestDecorators.tsx +34 -0
  47. package/src/decorators/ReactQueryDecorator.tsx +13 -0
  48. package/src/decorators/RouterDecorator.tsx +5 -0
  49. package/src/decorators/UnsafePropsDecorator.tsx +0 -0
  50. package/src/env.ts +99 -0
  51. package/src/hooks/use-autogrow.ts +28 -0
  52. package/src/hooks/use-mobile.ts +19 -0
  53. package/src/hooks/useAuthors.ts +78 -0
  54. package/src/hooks/useDebounce.ts +18 -0
  55. package/src/hooks/useMercuryProjectToken.ts +112 -0
  56. package/src/hooks/useProjectToken.ts +21 -0
  57. package/src/hooks/useTheme.ts +12 -0
  58. package/src/index.css +397 -0
  59. package/src/index.ts +104 -0
  60. package/src/mocks/env.ts +12 -0
  61. package/src/providers/QueryProvider/index.tsx +10 -0
  62. package/src/providers/ThemeProvider/index.tsx +52 -0
  63. package/src/types/category.types.ts +13 -0
  64. package/src/types/image.types.ts +44 -0
  65. package/src/types/post.types.ts +121 -0
  66. package/src/types/theme.types.ts +10 -0
  67. package/src/types/users.types.ts +5 -0
  68. package/src/utils/apolloClient.ts +160 -0
  69. package/src/utils/articleMetadata.ts +97 -0
  70. package/src/utils/audio.ts +10 -0
  71. package/src/utils/imageProcessing.ts +399 -0
  72. package/src/utils/images.ts +279 -0
  73. package/src/utils/mercury.ts +0 -0
  74. package/src/utils/utils.ts +638 -0
  75. package/src/utils/videos.ts +29 -0
  76. package/src/vite-env.d.ts +1 -0
  77. package/.env.test +0 -5
@@ -0,0 +1,399 @@
1
+ import { Coordinates } from 'react-advanced-cropper';
2
+ import { Transformation, TransformationState } from '../components/wazui/WazUiGqlMediaLibrary/types';
3
+
4
+ export interface ImageTransformOptions {
5
+ rotation?: number;
6
+ flipHorizontal?: boolean;
7
+ flipVertical?: boolean;
8
+ coordinates?: Coordinates;
9
+ quality?: number;
10
+ format?: 'image/jpeg' | 'image/png' | 'image/webp';
11
+ }
12
+
13
+ /**
14
+ * Apply transformations to an image file and return a new processed file
15
+ */
16
+ export const applyImageTransformations = async (
17
+ file: File,
18
+ transformations: ImageTransformOptions
19
+ ): Promise<File> => {
20
+ return new Promise((resolve, reject) => {
21
+ const canvas = document.createElement('canvas');
22
+ const ctx = canvas.getContext('2d');
23
+ const img = new Image();
24
+
25
+ if (!ctx) {
26
+ reject(new Error('Canvas context not available'));
27
+ return;
28
+ }
29
+
30
+ img.onload = () => {
31
+ try {
32
+ const {
33
+ rotation = 0,
34
+ flipHorizontal = false,
35
+ flipVertical = false,
36
+ coordinates,
37
+ quality = 0.9,
38
+ format = 'image/jpeg',
39
+ } = transformations;
40
+
41
+ // First, handle rotation and flips
42
+ let canvasWidth = img.width;
43
+ let canvasHeight = img.height;
44
+
45
+ if (rotation === 90 || rotation === 270) {
46
+ canvasWidth = img.height;
47
+ canvasHeight = img.width;
48
+ }
49
+
50
+ canvas.width = canvasWidth;
51
+ canvas.height = canvasHeight;
52
+
53
+ // Clear canvas
54
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
55
+
56
+ // Save context state
57
+ ctx.save();
58
+
59
+ // Move to center for rotation/flip operations
60
+ ctx.translate(canvasWidth / 2, canvasHeight / 2);
61
+
62
+ // Apply rotation
63
+ if (rotation) {
64
+ ctx.rotate((rotation * Math.PI) / 180);
65
+ }
66
+
67
+ // Apply flips
68
+ const scaleX = flipHorizontal ? -1 : 1;
69
+ const scaleY = flipVertical ? -1 : 1;
70
+ ctx.scale(scaleX, scaleY);
71
+
72
+ // Draw the image centered
73
+ const drawWidth = rotation === 90 || rotation === 270 ? img.height : img.width;
74
+ const drawHeight = rotation === 90 || rotation === 270 ? img.width : img.height;
75
+
76
+ ctx.drawImage(img, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight);
77
+
78
+ // Restore context state
79
+ ctx.restore();
80
+
81
+ // Handle cropping if specified
82
+ if (coordinates && coordinates.width > 0 && coordinates.height > 0) {
83
+ const cropCanvas = document.createElement('canvas');
84
+ const cropCtx = cropCanvas.getContext('2d');
85
+
86
+ if (!cropCtx) {
87
+ reject(new Error('Crop canvas context not available'));
88
+ return;
89
+ }
90
+
91
+ // Ensure crop coordinates are within canvas bounds
92
+ const cropX = Math.max(0, Math.min(coordinates.left, canvasWidth));
93
+ const cropY = Math.max(0, Math.min(coordinates.top, canvasHeight));
94
+ const cropWidth = Math.min(coordinates.width, canvasWidth - cropX);
95
+ const cropHeight = Math.min(coordinates.height, canvasHeight - cropY);
96
+
97
+ cropCanvas.width = cropWidth;
98
+ cropCanvas.height = cropHeight;
99
+
100
+ // Draw the cropped portion
101
+ cropCtx.drawImage(
102
+ canvas,
103
+ cropX,
104
+ cropY,
105
+ cropWidth,
106
+ cropHeight,
107
+ 0,
108
+ 0,
109
+ cropWidth,
110
+ cropHeight
111
+ );
112
+
113
+ // Convert cropped canvas to blob
114
+ cropCanvas.toBlob(
115
+ blob => {
116
+ if (blob) {
117
+ const processedFile = new File([blob], file.name, {
118
+ type: format,
119
+ lastModified: Date.now(),
120
+ });
121
+ resolve(processedFile);
122
+ } else {
123
+ reject(new Error('Failed to create blob from cropped canvas'));
124
+ }
125
+ },
126
+ format,
127
+ quality
128
+ );
129
+ } else {
130
+ // Convert main canvas to blob
131
+ canvas.toBlob(
132
+ blob => {
133
+ if (blob) {
134
+ const processedFile = new File([blob], file.name, {
135
+ type: format,
136
+ lastModified: Date.now(),
137
+ });
138
+ resolve(processedFile);
139
+ } else {
140
+ reject(new Error('Failed to create blob from canvas'));
141
+ }
142
+ },
143
+ format,
144
+ quality
145
+ );
146
+ }
147
+ } catch (error) {
148
+ reject(error);
149
+ }
150
+ };
151
+
152
+ img.onerror = () => {
153
+ reject(new Error('Failed to load image'));
154
+ };
155
+
156
+ // Create object URL for the file
157
+ const url = URL.createObjectURL(file);
158
+ img.src = url;
159
+
160
+ // Clean up object URL after image loads or errors
161
+ const cleanup = () => URL.revokeObjectURL(url);
162
+ img.addEventListener('load', cleanup, { once: true });
163
+ img.addEventListener('error', cleanup, { once: true });
164
+ });
165
+ };
166
+
167
+ /**
168
+ * Apply transformations from all screen sizes and return processed files
169
+ */
170
+ export const applyTransformationsForAllScreens = async (
171
+ file: File,
172
+ transformations: TransformationState
173
+ ): Promise<{
174
+ desktop?: File;
175
+ tablet?: File;
176
+ mobile?: File;
177
+ original: File;
178
+ }> => {
179
+ const results: {
180
+ desktop?: File;
181
+ tablet?: File;
182
+ mobile?: File;
183
+ original: File;
184
+ } = {
185
+ original: file,
186
+ };
187
+
188
+ try {
189
+ // Helper function to check if transformations are meaningful
190
+ const hasTransformations = (transform: Transformation) => {
191
+ return (
192
+ (transform.coordinates &&
193
+ transform.coordinates.width > 0 &&
194
+ transform.coordinates.height > 0) ||
195
+ (transform.rotation && transform.rotation !== 0) ||
196
+ transform.flipHorizontal ||
197
+ transform.flipVertical
198
+ );
199
+ };
200
+
201
+ // Process desktop transformations
202
+ if (hasTransformations(transformations.desktop)) {
203
+ try {
204
+ results.desktop = await applyImageTransformations(file, {
205
+ rotation: transformations.desktop.rotation,
206
+ flipHorizontal: transformations.desktop.flipHorizontal,
207
+ flipVertical: transformations.desktop.flipVertical,
208
+ coordinates: transformations.desktop.coordinates,
209
+ });
210
+ } catch (error) {
211
+ console.warn('Desktop transformation failed:', error);
212
+ }
213
+ }
214
+
215
+ // Process tablet transformations
216
+ if (hasTransformations(transformations.tablet)) {
217
+ try {
218
+ results.tablet = await applyImageTransformations(file, {
219
+ rotation: transformations.tablet.rotation,
220
+ flipHorizontal: transformations.tablet.flipHorizontal,
221
+ flipVertical: transformations.tablet.flipVertical,
222
+ coordinates: transformations.tablet.coordinates,
223
+ });
224
+ } catch (error) {
225
+ console.warn('Tablet transformation failed:', error);
226
+ }
227
+ }
228
+
229
+ // Process mobile transformations
230
+ if (hasTransformations(transformations.mobile)) {
231
+ try {
232
+ results.mobile = await applyImageTransformations(file, {
233
+ rotation: transformations.mobile.rotation,
234
+ flipHorizontal: transformations.mobile.flipHorizontal,
235
+ flipVertical: transformations.mobile.flipVertical,
236
+ coordinates: transformations.mobile.coordinates,
237
+ });
238
+ } catch (error) {
239
+ console.warn('Mobile transformation failed:', error);
240
+ }
241
+ }
242
+
243
+ return results;
244
+ } catch (error) {
245
+ console.error('Error applying transformations:', error);
246
+ throw error;
247
+ }
248
+ };
249
+
250
+ /**
251
+ * Get the most appropriate processed file from transformation results
252
+ */
253
+ export const getBestProcessedFile = (processedFiles: {
254
+ desktop?: File;
255
+ tablet?: File;
256
+ mobile?: File;
257
+ original: File;
258
+ }): File => {
259
+ // Return the first available processed file, prioritizing desktop > tablet > mobile > original
260
+ return (
261
+ processedFiles.desktop ||
262
+ processedFiles.tablet ||
263
+ processedFiles.mobile ||
264
+ processedFiles.original
265
+ );
266
+ };
267
+
268
+ /**
269
+ * Check if a file needs processing based on transformations
270
+ */
271
+ export const needsProcessing = (transformations: TransformationState): boolean => {
272
+ return Object.values(transformations).some(
273
+ transform =>
274
+ (transform.coordinates &&
275
+ transform.coordinates.width > 0 &&
276
+ transform.coordinates.height > 0) ||
277
+ (transform.rotation && transform.rotation !== 0) ||
278
+ transform.flipHorizontal ||
279
+ transform.flipVertical
280
+ );
281
+ };
282
+
283
+ /**
284
+ * Helper function to resize image while maintaining aspect ratio
285
+ */
286
+ export const resizeImage = async (
287
+ file: File,
288
+ maxWidth: number,
289
+ maxHeight: number,
290
+ quality: number = 0.9
291
+ ): Promise<File> => {
292
+ return new Promise((resolve, reject) => {
293
+ const canvas = document.createElement('canvas');
294
+ const ctx = canvas.getContext('2d');
295
+ const img = new Image();
296
+
297
+ if (!ctx) {
298
+ reject(new Error('Canvas context not available'));
299
+ return;
300
+ }
301
+
302
+ img.onload = () => {
303
+ // Calculate new dimensions while maintaining aspect ratio
304
+ let { width, height } = img;
305
+
306
+ if (width > height) {
307
+ if (width > maxWidth) {
308
+ height = (height * maxWidth) / width;
309
+ width = maxWidth;
310
+ }
311
+ } else {
312
+ if (height > maxHeight) {
313
+ width = (width * maxHeight) / height;
314
+ height = maxHeight;
315
+ }
316
+ }
317
+
318
+ canvas.width = width;
319
+ canvas.height = height;
320
+
321
+ // Draw resized image
322
+ ctx.drawImage(img, 0, 0, width, height);
323
+
324
+ canvas.toBlob(
325
+ blob => {
326
+ if (blob) {
327
+ const resizedFile = new File([blob], file.name, {
328
+ type: file.type,
329
+ lastModified: Date.now(),
330
+ });
331
+ resolve(resizedFile);
332
+ } else {
333
+ reject(new Error('Failed to create resized image blob'));
334
+ }
335
+ },
336
+ file.type,
337
+ quality
338
+ );
339
+ };
340
+
341
+ img.onerror = () => {
342
+ reject(new Error('Failed to load image for resizing'));
343
+ };
344
+
345
+ const url = URL.createObjectURL(file);
346
+ img.src = url;
347
+
348
+ // Clean up object URL after image loads or errors
349
+ const cleanup = () => URL.revokeObjectURL(url);
350
+ img.addEventListener('load', cleanup, { once: true });
351
+ img.addEventListener('error', cleanup, { once: true });
352
+ });
353
+ };
354
+
355
+ /**
356
+ * Convert image to different format
357
+ */
358
+ export const convertImageFormat = async (
359
+ file: File,
360
+ targetFormat: 'image/jpeg' | 'image/png' | 'image/webp',
361
+ quality: number = 0.9
362
+ ): Promise<File> => {
363
+ return applyImageTransformations(file, {
364
+ format: targetFormat,
365
+ quality,
366
+ });
367
+ };
368
+
369
+ /**
370
+ * Process multiple files with transformations
371
+ */
372
+ export const processMultipleFiles = async (
373
+ files: File[],
374
+ transformations: TransformationState,
375
+ onProgress?: (current: number, total: number) => void
376
+ ): Promise<File[]> => {
377
+ const processedFiles: File[] = [];
378
+
379
+ for (let i = 0; i < files.length; i++) {
380
+ const file = files[i];
381
+ onProgress?.(i + 1, files.length);
382
+
383
+ try {
384
+ if (needsProcessing(transformations)) {
385
+ const processedResults = await applyTransformationsForAllScreens(file, transformations);
386
+ const bestProcessedFile = getBestProcessedFile(processedResults);
387
+ processedFiles.push(bestProcessedFile);
388
+ } else {
389
+ processedFiles.push(file);
390
+ }
391
+ } catch (error) {
392
+ console.warn(`Failed to process file ${file.name}:`, error);
393
+ // Use original file if processing fails
394
+ processedFiles.push(file);
395
+ }
396
+ }
397
+
398
+ return processedFiles;
399
+ };
@@ -0,0 +1,279 @@
1
+ import {
2
+ ImageTransformation,
3
+ DeviceCoordinates,
4
+ ParsedConfig,
5
+ ImageUrlResult,
6
+ } from '../types/image.types';
7
+ import { Coordinates } from 'react-advanced-cropper';
8
+ import { env } from '../env';
9
+ // Constants
10
+ const TRANSFORMATION_KEYS = {
11
+ width: 'w',
12
+ height: 'h',
13
+ quality: 'q',
14
+ format: 'f',
15
+ coordinates: 'cr',
16
+ rotation: 'rt',
17
+ blur: 'bl',
18
+ } as const;
19
+
20
+ const DEVICE_BREAKPOINTS = {
21
+ desktop: '1100w',
22
+ tablet: '750w',
23
+ mobile: '450w',
24
+ } as const;
25
+
26
+ /**
27
+ * Builds transformation parameters string from ImageTransformation object
28
+ */
29
+ const buildTransformationParams = (transformations: ImageTransformation): string => {
30
+ const params: string[] = [];
31
+
32
+ Object.entries(transformations).forEach(([key, value]) => {
33
+ if (value === undefined || value === null) return;
34
+
35
+ const transformKey = TRANSFORMATION_KEYS[key as keyof typeof TRANSFORMATION_KEYS];
36
+ if (!transformKey) {
37
+ console.warn(`Unknown transformation key: ${key}`);
38
+ return;
39
+ }
40
+
41
+ if (key === 'coordinates') {
42
+ const { width, top, left, height } = value as Coordinates;
43
+ params.push(`${transformKey}-${top}.${left}.${height}.${width}`);
44
+ } else {
45
+ params.push(`${transformKey}-${value}`);
46
+ }
47
+ });
48
+
49
+ return params.join(',');
50
+ };
51
+
52
+ /**
53
+ * Parses base64 encoded config from URL
54
+ */
55
+ const parseConfig = (config: string | null): ParsedConfig | null => {
56
+ if (!config) return null;
57
+
58
+ try {
59
+ const decodedConfig = atob(config);
60
+ return JSON.parse(decodeURIComponent(decodedConfig));
61
+ } catch (error) {
62
+ console.error('Error parsing config:', error);
63
+ return null;
64
+ }
65
+ };
66
+
67
+ /**
68
+ * Builds crop parameters for a device configuration
69
+ */
70
+ const buildDeviceCrop = (
71
+ deviceConfig: (DeviceCoordinates & { rotation?: number }) | undefined
72
+ ): string => {
73
+ if (!deviceConfig) return '';
74
+
75
+ const { left, top, right, bottom, rotation } = deviceConfig;
76
+ const hasAllValues = [left, top, right, bottom, rotation].every(
77
+ v => v !== null && v !== undefined
78
+ );
79
+
80
+ if (!hasAllValues) return '';
81
+
82
+ return `cr-${left}.${top}.${right}.${bottom},rt-${rotation || 0}`;
83
+ };
84
+
85
+ const createTransformationUrl = (
86
+ baseImageUrl: string,
87
+ transformationParams: string,
88
+ deviceCrop: string = '',
89
+ config: string = ''
90
+ ): string => {
91
+ // Remove w- and h- parameters from transformationParams
92
+ const cleanedParams = transformationParams
93
+ .split(',')
94
+ .filter(p => !/^w-\d+/.test(p) && !/^h-\d+/.test(p))
95
+ .join(',');
96
+
97
+ const url = new URL(baseImageUrl);
98
+ const combinedParams = [cleanedParams, deviceCrop].filter(Boolean).join(',');
99
+
100
+ url.searchParams.set('tr', combinedParams);
101
+ if (config) {
102
+ url.searchParams.set('config', config);
103
+ }
104
+
105
+ return url.toString();
106
+ };
107
+
108
+ /**
109
+ * Generates srcset string for responsive images
110
+ */
111
+ const generateSrcset = (
112
+ baseImageUrl: string,
113
+ transformationParams: string,
114
+ deviceCrops: { desktop: string; tablet: string; mobile: string },
115
+ config: string,
116
+ isBackground: boolean
117
+ ) => {
118
+ const desktopUrl = decodeURIComponent(
119
+ createTransformationUrl(baseImageUrl, transformationParams, deviceCrops.desktop, config)
120
+ );
121
+ const tabletUrl = decodeURIComponent(
122
+ createTransformationUrl(baseImageUrl, transformationParams, deviceCrops.tablet)
123
+ );
124
+ const mobileUrl = decodeURIComponent(
125
+ createTransformationUrl(baseImageUrl, transformationParams, deviceCrops.mobile)
126
+ );
127
+
128
+ const srcset = isBackground
129
+ ? `url("${mobileUrl}") 1x, url("${tabletUrl}") 2x, url("${desktopUrl}") 3x`
130
+ : `${desktopUrl} ${DEVICE_BREAKPOINTS.desktop}, ${tabletUrl} ${DEVICE_BREAKPOINTS.tablet}, ${mobileUrl} ${DEVICE_BREAKPOINTS.mobile} `;
131
+
132
+ return {
133
+ desktop: decodeURIComponent(desktopUrl.toString()),
134
+ tablet: decodeURIComponent(tabletUrl.toString()),
135
+ mobile: decodeURIComponent(mobileUrl.toString()),
136
+ srcset,
137
+ };
138
+ };
139
+
140
+ /**
141
+ * Main function to generate image transformation URLs with responsive srcset
142
+ */
143
+ export const getImageTransformationsUrl = ({
144
+ url = '',
145
+ transformationsUrl,
146
+ transformations,
147
+ isBackground = false,
148
+ }: {
149
+ url: string;
150
+ transformationsUrl?: string;
151
+ transformations?: ImageTransformation;
152
+ isBackground?: boolean;
153
+ }): ImageUrlResult => {
154
+ // Early return for empty URL
155
+ if (!url) {
156
+ console.warn('getImageTransformationsUrl: No URL provided');
157
+ return { url: '', srcset: '', desktop: '', tablet: '', mobile: '' };
158
+ }
159
+
160
+ if (typeof url !== 'string' || url.trim() === '') {
161
+ console.warn('getImageTransformationsUrl: Invalid URL format:', url);
162
+ return { url: '', srcset: '', desktop: '', tablet: '', mobile: '' };
163
+ }
164
+
165
+ const trimmedUrl = url.trim();
166
+ if (!trimmedUrl.startsWith('http://') && !trimmedUrl.startsWith('https://') && !trimmedUrl.startsWith('/')) {
167
+ console.warn('getImageTransformationsUrl: URL must be absolute (http/https) or relative path:', url);
168
+ return { url: '', srcset: '', desktop: '', tablet: '', mobile: '' };
169
+ }
170
+
171
+ try {
172
+ const parsedUrl = trimmedUrl.startsWith('/')
173
+ ? new URL(trimmedUrl, 'https://placeholder.com')
174
+ : new URL(trimmedUrl);
175
+ const config = parsedUrl.searchParams.get('config') || '';
176
+
177
+ // Build base transformation URL
178
+ const imageTransformUrl =
179
+ transformationsUrl || (env.VITE_DOLOS_TRANSFORMATION_ENDPOINT as string) || 'images';
180
+ const shouldSkipHostname = parsedUrl.hostname === 'img.wazobia.tech';
181
+ const baseImageUrl = shouldSkipHostname
182
+ ? `${imageTransformUrl}${parsedUrl.pathname}`
183
+ : `${imageTransformUrl}${imageTransformUrl.endsWith('/') ? '' : '/'}${parsedUrl.hostname}${parsedUrl.pathname}`;
184
+
185
+ // Build transformation parameters
186
+ const transformationParams = transformations ? buildTransformationParams(transformations) : '';
187
+
188
+ // Parse config for device-specific crops
189
+ const parsedConfig = parseConfig(config);
190
+ const deviceCrops = {
191
+ desktop: buildDeviceCrop(parsedConfig?.coordinates?.desktop),
192
+ tablet:
193
+ buildDeviceCrop(parsedConfig?.coordinates?.tablet) ||
194
+ buildDeviceCrop(parsedConfig?.coordinates?.desktop),
195
+ mobile:
196
+ buildDeviceCrop(parsedConfig?.coordinates?.mobile) ||
197
+ buildDeviceCrop(parsedConfig?.coordinates?.desktop),
198
+ };
199
+
200
+ // Create base URL and srcset
201
+ const baseUrl = createTransformationUrl(baseImageUrl, transformationParams, '', config);
202
+ const { desktop, srcset, mobile, tablet } = generateSrcset(
203
+ baseImageUrl,
204
+ transformationParams,
205
+ deviceCrops,
206
+ config,
207
+ isBackground
208
+ );
209
+
210
+ return {
211
+ url: desktop || baseUrl.toString(),
212
+ srcset,
213
+ mobile,
214
+ tablet,
215
+ desktop,
216
+ };
217
+ } catch (error) {
218
+ console.error('Error generating image transformation URL:', error);
219
+ return { url, srcset: url, mobile: '', desktop: '', tablet: '' }; // Fallback to original URL
220
+ }
221
+ };
222
+
223
+ /**
224
+ * Simplified function to get image with specific dimensions
225
+ */
226
+ export const getImageWithDimensions = ({
227
+ url,
228
+ transformationsUrl,
229
+ width,
230
+ height,
231
+ blur,
232
+ quality = 70,
233
+ }: {
234
+ url: string;
235
+ transformationsUrl?: string;
236
+ width?: number;
237
+ height?: number;
238
+ blur?: number;
239
+ quality?: number;
240
+ }): ImageUrlResult => {
241
+ return getImageTransformationsUrl({
242
+ url,
243
+ transformationsUrl,
244
+ transformations: {
245
+ width,
246
+ height,
247
+ format: 'WEBP',
248
+ quality,
249
+ blur,
250
+ },
251
+ });
252
+ };
253
+
254
+ /**
255
+ * Calculate crop coordinates string from coordinate object
256
+ */
257
+ export const calculateCropCoordinates = (
258
+ coordinates?: Record<'left' | 'top' | 'right' | 'bottom', number>
259
+ ): string => {
260
+ if (!coordinates) return '';
261
+ const { left, top, right, bottom } = coordinates;
262
+ return `${left}.${top}.${right}.${bottom}`;
263
+ };
264
+
265
+ /**
266
+ * Default sizes attribute for responsive images
267
+ */
268
+ export const imageSizes = '(min-width: 1024px) 1100px, (min-width: 500px) 750px, 450px';
269
+
270
+ export const removeConfigParam = (url: string): string => {
271
+ try {
272
+ const parsedUrl = new URL(url);
273
+ parsedUrl.searchParams.delete('config');
274
+ return parsedUrl.toString();
275
+ } catch (error) {
276
+ console.error('Error removing config param:', error);
277
+ return url;
278
+ }
279
+ };
File without changes