pixedi 1.0.0

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 (110) hide show
  1. package/.github/workflows/deploy.yml +54 -0
  2. package/.prettierr +9 -0
  3. package/.storybook/main.ts +16 -0
  4. package/.storybook/preview.ts +53 -0
  5. package/.vscode/extensions.json +6 -0
  6. package/README.md +75 -0
  7. package/eslint.config.js +22 -0
  8. package/index.html +13 -0
  9. package/package.json +57 -0
  10. package/pnpm-workspace.yaml +2 -0
  11. package/public/favicon.svg +1 -0
  12. package/src/app/App.tsx +10 -0
  13. package/src/app/entries/index.ts +145 -0
  14. package/src/app/providers/RouterProvider.tsx +6 -0
  15. package/src/app/providers/index.ts +1 -0
  16. package/src/app/router/AppRouter.tsx +12 -0
  17. package/src/app/router/index.ts +0 -0
  18. package/src/app/styles/index.css +4 -0
  19. package/src/features/gallery/GalleryPage.tsx +33 -0
  20. package/src/features/gallery/ImageEditPage.tsx +37 -0
  21. package/src/features/gallery/index.ts +2 -0
  22. package/src/main.tsx +10 -0
  23. package/src/shared/components/ImageCard/ImageCard.tsx +54 -0
  24. package/src/shared/components/ImageCard/index.ts +1 -0
  25. package/src/shared/components/ImageEditor/ImageEditor.tsx +48 -0
  26. package/src/shared/components/ImageEditor/assets/icons.tsx +114 -0
  27. package/src/shared/components/ImageEditor/constants.ts +118 -0
  28. package/src/shared/components/ImageEditor/crop/Crop.tsx +60 -0
  29. package/src/shared/components/ImageEditor/crop/CropBorders.tsx +28 -0
  30. package/src/shared/components/ImageEditor/crop/CropLines.tsx +8 -0
  31. package/src/shared/components/ImageEditor/crop/CropPointer.tsx +21 -0
  32. package/src/shared/components/ImageEditor/crop/CropPointers.tsx +44 -0
  33. package/src/shared/components/ImageEditor/crop/CropTools.tsx +138 -0
  34. package/src/shared/components/ImageEditor/crop/crop.css +180 -0
  35. package/src/shared/components/ImageEditor/crop/index.ts +8 -0
  36. package/src/shared/components/ImageEditor/crop/useCropInteraction.ts +230 -0
  37. package/src/shared/components/ImageEditor/eventBus.ts +8 -0
  38. package/src/shared/components/ImageEditor/frame/Frame.tsx +48 -0
  39. package/src/shared/components/ImageEditor/frame/frame.css +40 -0
  40. package/src/shared/components/ImageEditor/frame/index.ts +1 -0
  41. package/src/shared/components/ImageEditor/header/Header.tsx +81 -0
  42. package/src/shared/components/ImageEditor/header/header.css +27 -0
  43. package/src/shared/components/ImageEditor/header/index.ts +1 -0
  44. package/src/shared/components/ImageEditor/hooks/index.ts +5 -0
  45. package/src/shared/components/ImageEditor/hooks/useAbove.tsx +18 -0
  46. package/src/shared/components/ImageEditor/hooks/useBellow.tsx +18 -0
  47. package/src/shared/components/ImageEditor/hooks/useImageLoader.tsx +110 -0
  48. package/src/shared/components/ImageEditor/hooks/useImageProcessor.test.tsx +147 -0
  49. package/src/shared/components/ImageEditor/hooks/useImageProcessor.ts +225 -0
  50. package/src/shared/components/ImageEditor/hooks/useMobile.tsx +33 -0
  51. package/src/shared/components/ImageEditor/index.css +145 -0
  52. package/src/shared/components/ImageEditor/index.ts +1 -0
  53. package/src/shared/components/ImageEditor/provider/ImageEditorProvider.test.tsx +126 -0
  54. package/src/shared/components/ImageEditor/provider/ImageEditorProvider.tsx +107 -0
  55. package/src/shared/components/ImageEditor/provider/StoreContext.tsx +20 -0
  56. package/src/shared/components/ImageEditor/provider/initialState.ts +30 -0
  57. package/src/shared/components/ImageEditor/provider/useImageEditorContext.tsx +12 -0
  58. package/src/shared/components/ImageEditor/resize/ResizeTools.tsx +183 -0
  59. package/src/shared/components/ImageEditor/resize/index.ts +1 -0
  60. package/src/shared/components/ImageEditor/resize/resize.css +56 -0
  61. package/src/shared/components/ImageEditor/sidebar/Sidebar.tsx +54 -0
  62. package/src/shared/components/ImageEditor/sidebar/SidebarCrop.tsx +57 -0
  63. package/src/shared/components/ImageEditor/sidebar/SidebarPresets.tsx +54 -0
  64. package/src/shared/components/ImageEditor/sidebar/SidebarResize.tsx +26 -0
  65. package/src/shared/components/ImageEditor/sidebar/index.ts +1 -0
  66. package/src/shared/components/ImageEditor/sidebar/sidebar.css +101 -0
  67. package/src/shared/components/ImageEditor/types.ts +59 -0
  68. package/src/shared/components/ImageEditor/ui/button/Button.stories.tsx +42 -0
  69. package/src/shared/components/ImageEditor/ui/button/Button.tsx +21 -0
  70. package/src/shared/components/ImageEditor/ui/button/button.css +69 -0
  71. package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.stories.tsx +26 -0
  72. package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.tsx +29 -0
  73. package/src/shared/components/ImageEditor/ui/button-crop/button-crop.css +38 -0
  74. package/src/shared/components/ImageEditor/ui/index.ts +6 -0
  75. package/src/shared/components/ImageEditor/ui/input/Input.stories.tsx +45 -0
  76. package/src/shared/components/ImageEditor/ui/input/Input.tsx +20 -0
  77. package/src/shared/components/ImageEditor/ui/input/input.css +48 -0
  78. package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.stories.tsx +33 -0
  79. package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.tsx +23 -0
  80. package/src/shared/components/ImageEditor/ui/input-pixel/input-pixel.css +24 -0
  81. package/src/shared/components/ImageEditor/ui/select/Select.stories.tsx +67 -0
  82. package/src/shared/components/ImageEditor/ui/select/Select.tsx +161 -0
  83. package/src/shared/components/ImageEditor/ui/select/select.css +147 -0
  84. package/src/shared/components/ImageEditor/ui/separator/Separator.stories.tsx +37 -0
  85. package/src/shared/components/ImageEditor/ui/separator/Separator.tsx +27 -0
  86. package/src/shared/components/ImageEditor/ui/separator/separator.css +19 -0
  87. package/src/shared/components/ImageEditor/utils.test.ts +136 -0
  88. package/src/shared/components/ImageEditor/utils.ts +497 -0
  89. package/src/shared/components/ui/index.ts +1 -0
  90. package/src/shared/components/ui/preloader.tsx +78 -0
  91. package/src/shared/config/index.ts +3 -0
  92. package/src/shared/config/routes.ts +4 -0
  93. package/src/shared/features/gallery/GalleryPage.tsx +33 -0
  94. package/src/shared/features/gallery/ImageEditPage.tsx +37 -0
  95. package/src/shared/features/gallery/index.ts +2 -0
  96. package/src/shared/hooks/index.ts +1 -0
  97. package/src/shared/hooks/useImagePreload.tsx +49 -0
  98. package/src/shared/types/index.ts +11 -0
  99. package/src/test/setup.ts +14 -0
  100. package/src/virtual-css-injected-by-js.d.ts +8 -0
  101. package/src/widget.tsx +119 -0
  102. package/tsconfig.app.json +29 -0
  103. package/tsconfig.json +12 -0
  104. package/tsconfig.lib.json +18 -0
  105. package/tsconfig.node.json +23 -0
  106. package/vite-env.d.ts +1 -0
  107. package/vite.config.ts +24 -0
  108. package/vite.lib.config.ts +33 -0
  109. package/vite.widget.config.ts +39 -0
  110. package/wrangler.json +5 -0
@@ -0,0 +1,497 @@
1
+ import type { CropRect, Direction } from "./types";
2
+
3
+ const minSize = 32;
4
+
5
+ const getDefaultCrop = (el: HTMLElement): CropRect => ({
6
+ x: el.offsetLeft,
7
+ y: el.offsetTop,
8
+ w: el.offsetWidth,
9
+ h: el.offsetHeight,
10
+ });
11
+
12
+ const getCropR = (
13
+ isFree: boolean,
14
+ ratio: number,
15
+ startX: number,
16
+ clientX: number,
17
+ width: number,
18
+ height: number,
19
+ crop: CropRect,
20
+ el: HTMLElement,
21
+ ): CropRect => {
22
+ const { x } = crop;
23
+ let { y, w, h } = crop;
24
+
25
+ if (isFree) {
26
+ w += clientX - startX;
27
+ if (w + x > width) w = width - x;
28
+ if (w < minSize) w = minSize;
29
+ } else {
30
+ const difW = clientX - startX;
31
+ const difH = Math.round(difW / ratio);
32
+ w += difW;
33
+ h += difH;
34
+ y -= difH / 2;
35
+ if (
36
+ y < 0 ||
37
+ h + y > height ||
38
+ w + x > width ||
39
+ w < minSize ||
40
+ h < minSize
41
+ ) {
42
+ return getDefaultCrop(el);
43
+ }
44
+ }
45
+
46
+ return { x, y, w, h };
47
+ };
48
+
49
+ const getCropL = (
50
+ isFree: boolean,
51
+ ratio: number,
52
+ startX: number,
53
+ clientX: number,
54
+ width: number,
55
+ height: number,
56
+ crop: CropRect,
57
+ el: HTMLElement,
58
+ ): CropRect => {
59
+ let { x, y, w, h } = crop;
60
+
61
+ if (isFree) {
62
+ const right = width - crop.w - crop.x;
63
+ x += clientX - startX;
64
+ if (x < 0) x = 0;
65
+ w = width - x - right;
66
+ if (w < minSize) {
67
+ w = minSize;
68
+ x = width - right - minSize;
69
+ }
70
+ } else {
71
+ const difW = clientX - startX;
72
+ const difH = Math.round(difW / ratio);
73
+ w -= difW;
74
+ h -= difH;
75
+ x += difW;
76
+ y += difH / 2;
77
+ if (y < 0 || x < 0 || h + y > height || w < minSize || h < minSize) {
78
+ return getDefaultCrop(el);
79
+ }
80
+ }
81
+
82
+ return { x, y, w, h };
83
+ };
84
+
85
+ const getCropT = (
86
+ isFree: boolean,
87
+ ratio: number,
88
+ startY: number,
89
+ clientY: number,
90
+ width: number,
91
+ height: number,
92
+ crop: CropRect,
93
+ el: HTMLElement,
94
+ ): CropRect => {
95
+ let { x, y, w, h } = crop;
96
+
97
+ const bottom = height - h - y;
98
+ if (isFree) {
99
+ y += clientY - startY;
100
+ if (y < 0) y = 0;
101
+ h = height - y - bottom;
102
+ if (h < minSize) {
103
+ h = minSize;
104
+ y = height - bottom - minSize;
105
+ }
106
+ } else {
107
+ const difH = clientY - startY;
108
+ const difW = Math.round(difH * ratio);
109
+ h -= difH;
110
+ w -= difW;
111
+ x += difW / 2;
112
+ y += difH;
113
+ if (y < 0 || x < 0 || w + x > width || w < minSize || h < minSize) {
114
+ return getDefaultCrop(el);
115
+ }
116
+ }
117
+
118
+ return { x, y, w, h };
119
+ };
120
+
121
+ const getCropB = (
122
+ isFree: boolean,
123
+ ratio: number,
124
+ startY: number,
125
+ clientY: number,
126
+ width: number,
127
+ height: number,
128
+ crop: CropRect,
129
+ el: HTMLElement,
130
+ ): CropRect => {
131
+ const { y } = crop;
132
+ let { x, w, h } = crop;
133
+
134
+ if (isFree) {
135
+ h += clientY - startY;
136
+ if (h + y > height) h = height - y;
137
+ if (h < minSize) h = minSize;
138
+ } else {
139
+ const difH = clientY - startY;
140
+ const difW = Math.round(difH * ratio);
141
+ h += difH;
142
+ w += difW;
143
+ x -= difW / 2;
144
+ if (
145
+ h + y > height ||
146
+ x < 0 ||
147
+ w + x > width ||
148
+ w < minSize ||
149
+ h < minSize
150
+ ) {
151
+ return getDefaultCrop(el);
152
+ }
153
+ }
154
+
155
+ return { x, y, w, h };
156
+ };
157
+
158
+ const getCropTL = (
159
+ isFree: boolean,
160
+ ratio: number,
161
+ startX: number,
162
+ startY: number,
163
+ clientX: number,
164
+ clientY: number,
165
+ width: number,
166
+ height: number,
167
+ crop: CropRect,
168
+ el: HTMLElement,
169
+ ): CropRect => {
170
+ let { x, y, w, h } = crop;
171
+
172
+ if (isFree) {
173
+ const right = width - crop.w - crop.x;
174
+ x += clientX - startX;
175
+ if (x < 0) x = 0;
176
+ w = width - x - right;
177
+ if (w < minSize) {
178
+ w = minSize;
179
+ x = width - right - minSize;
180
+ }
181
+
182
+ const bottom = height - h - y;
183
+ y += clientY - startY;
184
+ if (y < 0) y = 0;
185
+ h = height - y - bottom;
186
+ if (h < minSize) {
187
+ h = minSize;
188
+ y = height - bottom - minSize;
189
+ }
190
+ } else {
191
+ if (ratio >= 1) {
192
+ const difW = clientX - startX;
193
+ const difH = Math.round(difW / ratio);
194
+ w -= difW;
195
+ h -= difH;
196
+ x += difW;
197
+ y += difH;
198
+ } else {
199
+ const difH = clientY - startY;
200
+ const difW = Math.round(difH * ratio);
201
+ w -= difW;
202
+ h -= difH;
203
+ x += difW;
204
+ y += difH;
205
+ }
206
+ if (x < 0 || y < 0 || w < minSize || h < minSize) {
207
+ return getDefaultCrop(el);
208
+ }
209
+ }
210
+
211
+ return { x, y, w, h };
212
+ };
213
+
214
+ const getCropTR = (
215
+ isFree: boolean,
216
+ ratio: number,
217
+ startX: number,
218
+ startY: number,
219
+ clientX: number,
220
+ clientY: number,
221
+ width: number,
222
+ height: number,
223
+ crop: CropRect,
224
+ el: HTMLElement,
225
+ ): CropRect => {
226
+ const { x } = crop;
227
+ let { y, w, h } = crop;
228
+
229
+ if (isFree) {
230
+ w += clientX - startX;
231
+ if (w + x > width) w = width - x;
232
+ if (w < minSize) w = minSize;
233
+
234
+ const bottom = height - h - y;
235
+ y += clientY - startY;
236
+ if (y < 0) y = 0;
237
+ h = height - y - bottom;
238
+ if (h < minSize) {
239
+ h = minSize;
240
+ y = height - bottom - minSize;
241
+ }
242
+ } else {
243
+ if (ratio > 1) {
244
+ const difW = clientX - startX;
245
+ const difH = Math.round(difW / ratio);
246
+ w += difW;
247
+ h += difH;
248
+ y -= difH;
249
+ } else {
250
+ const difH = clientY - startY;
251
+ const difW = Math.round(difH * ratio);
252
+ w -= difW;
253
+ h -= difH;
254
+ y += difH;
255
+ }
256
+ if (w + x > width || y < 0 || w < minSize || h < minSize) {
257
+ return getDefaultCrop(el);
258
+ }
259
+ }
260
+
261
+ return { x, y, w, h };
262
+ };
263
+
264
+ const getCropBL = (
265
+ isFree: boolean,
266
+ ratio: number,
267
+ startX: number,
268
+ startY: number,
269
+ clientX: number,
270
+ clientY: number,
271
+ width: number,
272
+ height: number,
273
+ crop: CropRect,
274
+ el: HTMLElement,
275
+ ): CropRect => {
276
+ const { y } = crop;
277
+ let { x, w, h } = crop;
278
+
279
+ if (isFree) {
280
+ h += clientY - startY;
281
+ if (h + y > height) h = height - y;
282
+ if (h < minSize) h = minSize;
283
+
284
+ const right = width - crop.w - crop.x;
285
+ x += clientX - startX;
286
+ if (x < 0) x = 0;
287
+ w = width - x - right;
288
+ if (w < minSize) {
289
+ w = minSize;
290
+ x = width - right - minSize;
291
+ }
292
+ } else {
293
+ if (ratio >= 1) {
294
+ const difW = clientX - startX;
295
+ const difH = Math.round(difW / ratio);
296
+ w -= difW;
297
+ h -= difH;
298
+ x += difW;
299
+ } else {
300
+ const difH = clientY - startY;
301
+ const difW = Math.round(difH * ratio);
302
+ w += difW;
303
+ h += difH;
304
+ x -= difW;
305
+ }
306
+ if (h + y > height || x < 0 || w < minSize || h < minSize) {
307
+ return getDefaultCrop(el);
308
+ }
309
+ }
310
+
311
+ return { x, y, w, h };
312
+ };
313
+
314
+ const getCropBR = (
315
+ isFree: boolean,
316
+ ratio: number,
317
+ startX: number,
318
+ startY: number,
319
+ clientX: number,
320
+ clientY: number,
321
+ width: number,
322
+ height: number,
323
+ crop: CropRect,
324
+ el: HTMLElement,
325
+ ): CropRect => {
326
+ const { x, y } = crop;
327
+ let { w, h } = crop;
328
+
329
+ if (isFree) {
330
+ w += clientX - startX;
331
+ if (w + x > width) w = width - x;
332
+ if (w < minSize) w = minSize;
333
+
334
+ h += clientY - startY;
335
+ if (h + y > height) h = height - y;
336
+ if (h < minSize) h = minSize;
337
+ } else {
338
+ if (ratio >= 1) {
339
+ const difW = clientX - startX;
340
+ const difH = Math.round(difW / ratio);
341
+ w += difW;
342
+ h += difH;
343
+ } else {
344
+ const difH = clientY - startY;
345
+ const difW = Math.round(difH * ratio);
346
+ w += difW;
347
+ h += difH;
348
+ }
349
+ if (h + y > height || w + x > width || w < minSize || h < minSize) {
350
+ return getDefaultCrop(el);
351
+ }
352
+ }
353
+
354
+ return { x, y, w, h };
355
+ };
356
+
357
+ export const getCropPoints = (
358
+ dir: Direction,
359
+ isFree: boolean,
360
+ ratio: number,
361
+ startX: number,
362
+ startY: number,
363
+ clientX: number,
364
+ clientY: number,
365
+ width: number,
366
+ height: number,
367
+ crop: CropRect,
368
+ el: HTMLElement,
369
+ ): CropRect => {
370
+ if (dir === "r")
371
+ return getCropR(isFree, ratio, startX, clientX, width, height, crop, el);
372
+ if (dir === "l")
373
+ return getCropL(isFree, ratio, startX, clientX, width, height, crop, el);
374
+ if (dir === "t")
375
+ return getCropT(isFree, ratio, startY, clientY, width, height, crop, el);
376
+ if (dir === "b")
377
+ return getCropB(isFree, ratio, startY, clientY, width, height, crop, el);
378
+ if (dir === "tl")
379
+ return getCropTL(
380
+ isFree,
381
+ ratio,
382
+ startX,
383
+ startY,
384
+ clientX,
385
+ clientY,
386
+ width,
387
+ height,
388
+ crop,
389
+ el,
390
+ );
391
+ if (dir === "tr")
392
+ return getCropTR(
393
+ isFree,
394
+ ratio,
395
+ startX,
396
+ startY,
397
+ clientX,
398
+ clientY,
399
+ width,
400
+ height,
401
+ crop,
402
+ el,
403
+ );
404
+ if (dir === "bl")
405
+ return getCropBL(
406
+ isFree,
407
+ ratio,
408
+ startX,
409
+ startY,
410
+ clientX,
411
+ clientY,
412
+ width,
413
+ height,
414
+ crop,
415
+ el,
416
+ );
417
+ if (dir === "br")
418
+ return getCropBR(
419
+ isFree,
420
+ ratio,
421
+ startX,
422
+ startY,
423
+ clientX,
424
+ clientY,
425
+ width,
426
+ height,
427
+ crop,
428
+ el,
429
+ );
430
+ return { x: 0, y: 0, w: 0, h: 0 };
431
+ };
432
+
433
+ export const getInitalCrop = (
434
+ cropRatio: number,
435
+ width: number,
436
+ height: number,
437
+ ): CropRect => {
438
+ const offsetPercent = 0.1;
439
+ const frameRatio = width / height;
440
+ let wPx = 0;
441
+ let hPx = 0;
442
+
443
+ if (cropRatio === 1) {
444
+ wPx = (frameRatio > 1 ? height : width) * (1 - offsetPercent * 2);
445
+ hPx = wPx;
446
+ }
447
+
448
+ if (cropRatio > 1) {
449
+ if (frameRatio > cropRatio) {
450
+ hPx = height * (1 - offsetPercent * 2);
451
+ wPx = hPx * cropRatio;
452
+ } else {
453
+ wPx = width * (1 - offsetPercent * 2);
454
+ hPx = wPx / cropRatio;
455
+ }
456
+ }
457
+
458
+ if (cropRatio < 1) {
459
+ if (frameRatio > cropRatio) {
460
+ hPx = height * (1 - offsetPercent * 2);
461
+ wPx = hPx * cropRatio;
462
+ } else {
463
+ wPx = width * (1 - offsetPercent * 2);
464
+ hPx = wPx / cropRatio;
465
+ }
466
+ }
467
+
468
+ const w = (wPx / width) * 100;
469
+ const h = (hPx / height) * 100;
470
+ const x = ((width - wPx) / 2 / width) * 100;
471
+ const y = ((height - hPx) / 2 / height) * 100;
472
+ const xPx = Math.round((width - wPx) / 2);
473
+ const yPx = Math.round((height - hPx) / 2);
474
+
475
+ return { x, y, w, h, xPx, yPx, wPx, hPx };
476
+ };
477
+
478
+ export const getCropByNewSizes = (
479
+ crop: CropRect,
480
+ width: number,
481
+ height: number,
482
+ frameWidth: number,
483
+ ): CropRect => {
484
+ const diffPercent = width / frameWidth;
485
+ const x = Math.floor(crop.x * diffPercent);
486
+ const y = Math.floor(crop.y * diffPercent);
487
+ const w = Math.ceil(crop.w * diffPercent);
488
+ const h = Math.ceil(crop.h * diffPercent);
489
+ return {
490
+ x: x < 0 ? 0 : x,
491
+ y: y < 0 ? 0 : y,
492
+ w: w > width ? width : w,
493
+ h: h > height ? height : h,
494
+ };
495
+ };
496
+
497
+ export const degree2Rad = (degree: number): number => (degree * Math.PI) / 180;
@@ -0,0 +1 @@
1
+ export { Preloader } from "./preloader";
@@ -0,0 +1,78 @@
1
+ type PreloaderProps = {
2
+ size?: number;
3
+ color?: string;
4
+ className?: string;
5
+ style?: React.CSSProperties;
6
+ };
7
+
8
+ function Preloader({
9
+ size = 36,
10
+ className,
11
+ style,
12
+ color = "currentColor",
13
+ }: PreloaderProps) {
14
+ return (
15
+ <svg
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ x="0px"
18
+ y="0px"
19
+ viewBox="0 0 80 80"
20
+ width={size}
21
+ height={size}
22
+ fill={color}
23
+ className={className}
24
+ style={style}
25
+ >
26
+ <path
27
+ d="M10,40c0,0,0-0.4,0-1.1c0-0.3,0-0.8,0-1.3c0-0.3,0-0.5,0-0.8c0-0.3,0.1-0.6,0.1-0.9c0.1-0.6,0.1-1.4,0.2-2.1
28
+ c0.2-0.8,0.3-1.6,0.5-2.5c0.2-0.9,0.6-1.8,0.8-2.8c0.3-1,0.8-1.9,1.2-3c0.5-1,1.1-2,1.7-3.1c0.7-1,1.4-2.1,2.2-3.1
29
+ c1.6-2.1,3.7-3.9,6-5.6c2.3-1.7,5-3,7.9-4.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.3,1.5-0.3,2.3-0.5c0.8-0.2,1.5-0.3,2.3-0.4l1.2-0.1
30
+ l0.6-0.1l0.3,0l0.1,0l0.1,0l0,0c0.1,0-0.1,0,0.1,0c1.5,0,2.9-0.1,4.5,0.2c0.8,0.1,1.6,0.1,2.4,0.3c0.8,0.2,1.5,0.3,2.3,0.5
31
+ c3,0.8,5.9,2,8.5,3.6c2.6,1.6,4.9,3.4,6.8,5.4c1,1,1.8,2.1,2.7,3.1c0.8,1.1,1.5,2.1,2.1,3.2c0.6,1.1,1.2,2.1,1.6,3.1
32
+ c0.4,1,0.9,2,1.2,3c0.3,1,0.6,1.9,0.8,2.7c0.2,0.9,0.3,1.6,0.5,2.4c0.1,0.4,0.1,0.7,0.2,1c0,0.3,0.1,0.6,0.1,0.9
33
+ c0.1,0.6,0.1,1,0.1,1.4C74,39.6,74,40,74,40c0.2,2.2-1.5,4.1-3.7,4.3s-4.1-1.5-4.3-3.7c0-0.1,0-0.2,0-0.3l0-0.4c0,0,0-0.3,0-0.9
34
+ c0-0.3,0-0.7,0-1.1c0-0.2,0-0.5,0-0.7c0-0.2-0.1-0.5-0.1-0.8c-0.1-0.6-0.1-1.2-0.2-1.9c-0.1-0.7-0.3-1.4-0.4-2.2
35
+ c-0.2-0.8-0.5-1.6-0.7-2.4c-0.3-0.8-0.7-1.7-1.1-2.6c-0.5-0.9-0.9-1.8-1.5-2.7c-0.6-0.9-1.2-1.8-1.9-2.7c-1.4-1.8-3.2-3.4-5.2-4.9
36
+ c-2-1.5-4.4-2.7-6.9-3.6c-0.6-0.2-1.3-0.4-1.9-0.6c-0.7-0.2-1.3-0.3-1.9-0.4c-1.2-0.3-2.8-0.4-4.2-0.5l-2,0c-0.7,0-1.4,0.1-2.1,0.1
37
+ c-0.7,0.1-1.4,0.1-2,0.3c-0.7,0.1-1.3,0.3-2,0.4c-2.6,0.7-5.2,1.7-7.5,3.1c-2.2,1.4-4.3,2.9-6,4.7c-0.9,0.8-1.6,1.8-2.4,2.7
38
+ c-0.7,0.9-1.3,1.9-1.9,2.8c-0.5,1-1,1.9-1.4,2.8c-0.4,0.9-0.8,1.8-1,2.6c-0.3,0.9-0.5,1.6-0.7,2.4c-0.2,0.7-0.3,1.4-0.4,2.1
39
+ c-0.1,0.3-0.1,0.6-0.2,0.9c0,0.3-0.1,0.6-0.1,0.8c0,0.5-0.1,0.9-0.1,1.3C10,39.6,10,40,10,40z"
40
+ >
41
+ <animateTransform
42
+ attributeType="xml"
43
+ attributeName="transform"
44
+ type="rotate"
45
+ from="0 40 40"
46
+ to="360 40 40"
47
+ dur="0.8s"
48
+ repeatCount="indefinite"
49
+ />
50
+ </path>
51
+ <path
52
+ fill="currentColor"
53
+ d="M62,40.1c0,0,0,0.2-0.1,0.7c0,0.2,0,0.5-0.1,0.8c0,0.2,0,0.3,0,0.5c0,0.2-0.1,0.4-0.1,0.7
54
+ c-0.1,0.5-0.2,1-0.3,1.6c-0.2,0.5-0.3,1.1-0.5,1.8c-0.2,0.6-0.5,1.3-0.7,1.9c-0.3,0.7-0.7,1.3-1,2.1c-0.4,0.7-0.9,1.4-1.4,2.1
55
+ c-0.5,0.7-1.1,1.4-1.7,2c-1.2,1.3-2.7,2.5-4.4,3.6c-1.7,1-3.6,1.8-5.5,2.4c-2,0.5-4,0.7-6.2,0.7c-1.9-0.1-4.1-0.4-6-1.1
56
+ c-1.9-0.7-3.7-1.5-5.2-2.6c-1.5-1.1-2.9-2.3-4-3.7c-0.6-0.6-1-1.4-1.5-2c-0.4-0.7-0.8-1.4-1.2-2c-0.3-0.7-0.6-1.3-0.8-2
57
+ c-0.2-0.6-0.4-1.2-0.6-1.8c-0.1-0.6-0.3-1.1-0.4-1.6c-0.1-0.5-0.1-1-0.2-1.4c-0.1-0.9-0.1-1.5-0.1-2c0-0.5,0-0.7,0-0.7
58
+ s0,0.2,0.1,0.7c0.1,0.5,0,1.1,0.2,2c0.1,0.4,0.2,0.9,0.3,1.4c0.1,0.5,0.3,1,0.5,1.6c0.2,0.6,0.4,1.1,0.7,1.8
59
+ c0.3,0.6,0.6,1.2,0.9,1.9c0.4,0.6,0.8,1.3,1.2,1.9c0.5,0.6,1,1.3,1.6,1.8c1.1,1.2,2.5,2.3,4,3.2c1.5,0.9,3.2,1.6,5,2.1
60
+ c1.8,0.5,3.6,0.6,5.6,0.6c1.8-0.1,3.7-0.4,5.4-1c1.7-0.6,3.3-1.4,4.7-2.4c1.4-1,2.6-2.1,3.6-3.3c0.5-0.6,0.9-1.2,1.3-1.8
61
+ c0.4-0.6,0.7-1.2,1-1.8c0.3-0.6,0.6-1.2,0.8-1.8c0.2-0.6,0.4-1.1,0.5-1.7c0.1-0.5,0.2-1,0.3-1.5c0.1-0.4,0.1-0.8,0.1-1.2
62
+ c0-0.2,0-0.4,0.1-0.5c0-0.2,0-0.4,0-0.5c0-0.3,0-0.6,0-0.8c0-0.5,0-0.7,0-0.7c0-1.1,0.9-2,2-2s2,0.9,2,2C62,40,62,40.1,62,40.1z"
63
+ >
64
+ <animateTransform
65
+ attributeType="xml"
66
+ attributeName="transform"
67
+ type="rotate"
68
+ from="0 40 40"
69
+ to="-360 40 40"
70
+ dur="0.6s"
71
+ repeatCount="indefinite"
72
+ />
73
+ </path>
74
+ </svg>
75
+ );
76
+ }
77
+
78
+ export { Preloader };
@@ -0,0 +1,3 @@
1
+ import { ROUTES } from "./routes";
2
+
3
+ export { ROUTES };
@@ -0,0 +1,4 @@
1
+ export const ROUTES = {
2
+ HOME: "/",
3
+ IMAGE_EDIT: "images/:id",
4
+ };
@@ -0,0 +1,33 @@
1
+ import { imagesData } from "@/app/entries";
2
+ import { ImageCard } from "@/shared/components/ImageCard";
3
+ import { Preloader } from "@/shared/components/ui";
4
+ import { useImagePreload } from "@/shared/hooks";
5
+
6
+ export const GalleryPage = () => {
7
+ const isLoaded = useImagePreload(imagesData.map((image) => image.original));
8
+
9
+ if (!isLoaded) {
10
+ return (
11
+ <div className="w-full min-h-screen flex items-center justify-center">
12
+ <Preloader size={64} />
13
+ </div>
14
+ );
15
+ }
16
+
17
+ return (
18
+ <div
19
+ className={`
20
+ max-w-4xl mx-6
21
+ my-6
22
+ grid content-start gap-6
23
+ grid-cols-1
24
+ sm:grid-cols-2
25
+ md:grid-cols-3 md:mx-auto
26
+ `}
27
+ >
28
+ {imagesData.map((image) => (
29
+ <ImageCard key={image.id} image={image} />
30
+ ))}
31
+ </div>
32
+ );
33
+ };
@@ -0,0 +1,37 @@
1
+ import { useParams, useNavigate } from "react-router";
2
+ import { imagesData } from "@/app/entries";
3
+ import { ImageEditor } from "@/shared/components/ImageEditor";
4
+
5
+ export const ImageEditPage = () => {
6
+ const navigate = useNavigate();
7
+ const { id } = useParams<{ id: string }>();
8
+ const image = imagesData.find((img) => img.id === id);
9
+
10
+ if (!image) {
11
+ return <div>Image not found</div>;
12
+ }
13
+
14
+ const handleSave = async (base64: string) => {
15
+ return new Promise<void>((resolve) => {
16
+ setTimeout(() => {
17
+ // TODO: Save the image
18
+ console.log(base64);
19
+ resolve();
20
+ }, 2000);
21
+ });
22
+ };
23
+
24
+ const handleCancel = () => {
25
+ navigate("/");
26
+ };
27
+
28
+ return (
29
+ <div className="w-full h-screen">
30
+ <ImageEditor
31
+ image={image.original}
32
+ onBack={handleCancel}
33
+ onSave={handleSave}
34
+ />
35
+ </div>
36
+ );
37
+ };
@@ -0,0 +1,2 @@
1
+ export { GalleryPage } from "./GalleryPage";
2
+ export { ImageEditPage } from "./ImageEditPage";
@@ -0,0 +1 @@
1
+ export { useImagePreload } from "./useImagePreload";
@@ -0,0 +1,49 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export function useImagePreload(
4
+ imageUrls: string[],
5
+ delay: number = 1000,
6
+ ): boolean {
7
+ const [loadedUrls, setLoadedUrls] = useState<string[]>([]);
8
+
9
+ const isAlreadyLoaded =
10
+ imageUrls.length === loadedUrls.length &&
11
+ imageUrls.every((url, i) => url === loadedUrls[i]);
12
+
13
+ useEffect(() => {
14
+ if (imageUrls.length === 0 || isAlreadyLoaded) {
15
+ return;
16
+ }
17
+
18
+ let isMounted = true;
19
+
20
+ const minimumDelayPromise = new Promise((resolve) =>
21
+ setTimeout(resolve, delay),
22
+ );
23
+
24
+ const imagePromises = imageUrls.map((src) => {
25
+ return new Promise<void>((resolve) => {
26
+ const img = new Image();
27
+ img.src = src;
28
+ img.onload = () => resolve();
29
+ img.onerror = () => resolve();
30
+ });
31
+ });
32
+
33
+ Promise.all([...imagePromises, minimumDelayPromise]).then(() => {
34
+ if (isMounted) {
35
+ setLoadedUrls(imageUrls);
36
+ }
37
+ });
38
+
39
+ return () => {
40
+ isMounted = false;
41
+ };
42
+ }, [imageUrls, isAlreadyLoaded]);
43
+
44
+ if (imageUrls.length === 0) {
45
+ return true;
46
+ }
47
+
48
+ return isAlreadyLoaded;
49
+ }