@wazobiatech/wazui 0.0.1 → 0.0.3
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.test +5 -0
- package/package.json +7 -5
- package/postcss.config.js +3 -0
- package/tailwind.config.js +70 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +13 -5
- package/.storybook/main.ts +0 -41
- package/.storybook/preview-head.html +0 -3
- package/.storybook/preview.ts +0 -53
- package/src/assets/dashboard/book.svg +0 -6
- package/src/assets/dashboard/edit.svg +0 -5
- package/src/assets/dashboard/emerald.svg +0 -9
- package/src/assets/dashboard/eye.svg +0 -4
- package/src/assets/dashboard/gold.svg +0 -9
- package/src/assets/dashboard/percentage-square.svg +0 -6
- package/src/assets/dashboard/sapphire.svg +0 -9
- package/src/assets/dashboard/send-2.svg +0 -4
- package/src/assets/desktop.svg +0 -7
- package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.otf +0 -0
- package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.woff +0 -0
- package/src/assets/fonts/UniversalFeature/FeatureJungle-Light.woff2 +0 -0
- package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.otf +0 -0
- package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.woff +0 -0
- package/src/assets/fonts/UniversalFeature/FeatureJungle-Regular.woff2 +0 -0
- package/src/assets/fonts/UniversalJungle/UniversalJungle-420.otf +0 -0
- package/src/assets/fonts/UniversalJungle/UniversalJungle-420.woff +0 -0
- package/src/assets/fonts/UniversalJungle/UniversalJungle-420.woff2 +0 -0
- package/src/assets/fonts/UniversalJungle/UniversalJungle-515.otf +0 -0
- package/src/assets/fonts/UniversalJungle/UniversalJungle-515.woff +0 -0
- package/src/assets/fonts/UniversalJungle/UniversalJungle-515.woff2 +0 -0
- package/src/assets/instagram.svg +0 -9
- package/src/assets/mobile.svg +0 -4
- package/src/assets/no-data-image.svg +0 -41
- package/src/assets/no-image-icon.svg +0 -84
- package/src/assets/pexels.svg +0 -9
- package/src/assets/react.svg +0 -1
- package/src/assets/sounds/draft.mp3 +0 -0
- package/src/assets/sounds/error.mp3 +0 -0
- package/src/assets/sounds/publish.mp3 +0 -0
- package/src/assets/tablet.svg +0 -4
- package/src/assets/unsplash.svg +0 -3
- package/src/assets/upload-image.svg +0 -43
- package/src/assets/user.svg +0 -4
- package/src/components/shadcn/index.ts +0 -31
- package/src/config/env.ts +0 -15
- package/src/contexts/ThemeContext.tsx +0 -11
- package/src/data/users.json +0 -6014
- package/src/decorators/ActionModalDecorator.tsx +0 -10
- package/src/decorators/ApolloDecorator.tsx +0 -19
- package/src/decorators/DolosApolloDecorator.tsx +0 -31
- package/src/decorators/JestDecorators.tsx +0 -34
- package/src/decorators/ReactQueryDecorator.tsx +0 -13
- package/src/decorators/RouterDecorator.tsx +0 -5
- package/src/decorators/UnsafePropsDecorator.tsx +0 -0
- package/src/env.ts +0 -99
- package/src/hooks/use-autogrow.ts +0 -28
- package/src/hooks/use-mobile.ts +0 -19
- package/src/hooks/useAuthors.ts +0 -78
- package/src/hooks/useDebounce.ts +0 -18
- package/src/hooks/useMercuryProjectToken.ts +0 -112
- package/src/hooks/useProjectToken.ts +0 -21
- package/src/hooks/useTheme.ts +0 -12
- package/src/index.css +0 -397
- package/src/index.ts +0 -59
- package/src/mocks/env.ts +0 -12
- package/src/providers/QueryProvider/index.tsx +0 -10
- package/src/providers/ThemeProvider/index.tsx +0 -52
- package/src/types/category.types.ts +0 -13
- package/src/types/image.types.ts +0 -44
- package/src/types/post.types.ts +0 -121
- package/src/types/theme.types.ts +0 -10
- package/src/types/users.types.ts +0 -5
- package/src/utils/apolloClient.ts +0 -160
- package/src/utils/articleMetadata.ts +0 -92
- package/src/utils/audio.ts +0 -10
- package/src/utils/imageProcessing.ts +0 -399
- package/src/utils/images.ts +0 -279
- package/src/utils/mercury.ts +0 -0
- package/src/utils/utils.ts +0 -638
- package/src/utils/videos.ts +0 -29
- package/src/vite-env.d.ts +0 -1
|
@@ -1,399 +0,0 @@
|
|
|
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
|
-
};
|
package/src/utils/images.ts
DELETED
|
@@ -1,279 +0,0 @@
|
|
|
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
|
-
};
|
package/src/utils/mercury.ts
DELETED
|
File without changes
|