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,54 @@
1
+ name: CI/CD Pipeline
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+
7
+ jobs:
8
+ build-and-deploy:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - name: Checkout Repository
12
+ uses: actions/checkout@v4
13
+
14
+ - name: Setup Node.js
15
+ uses: actions/setup-node@v4
16
+ with:
17
+ node-version: '22'
18
+ registry-url: 'https://registry.npmjs.org'
19
+
20
+ - name: Install pnpm globally
21
+ run: npm install -g pnpm@9
22
+
23
+ # Install regular project dependencies
24
+ - name: Install Dependencies
25
+ run: pnpm install --frozen-lockfile
26
+
27
+ - name: Run Vitest Tests
28
+ run: pnpm test
29
+
30
+ - name: Run TypeScript Type Check
31
+ run: pnpm type-check
32
+
33
+ - name: Run Unified Build Configuration
34
+ run: pnpm build
35
+
36
+ # --- 1. Deploy to Cloudflare Pages ---
37
+ # This uploads the dist folder containing both the SPA app and the widget CDN
38
+ # Make sure you have CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID in GitHub Secrets
39
+ - name: Deploy SPA & Widget CDN to Cloudflare Pages
40
+ uses: cloudflare/pages-action@v1
41
+ with:
42
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
43
+ accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
44
+ projectName: 'pixedi' # The project name inside your Cloudflare Pages account
45
+ directory: 'dist' # Uploads everything generated in the previous step
46
+
47
+ # --- 2. EXISTING WORKING STEP: PUBLISH TO NPM ---
48
+ - name: Publish React Component to NPM
49
+ run: |
50
+ pnpm config set registry https://registry.npmjs.org
51
+ pnpm config set //registry.npmjs.org/:_authToken "$NODE_AUTH_TOKEN"
52
+ pnpm publish --no-git-checks
53
+ env:
54
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.prettierr ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "semi": true,
3
+ "trailingComma": "all",
4
+ "singleQuote": true,
5
+ "printWidth": 100,
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "endOfLine": "lf"
9
+ }
@@ -0,0 +1,16 @@
1
+ import type { StorybookConfig } from "@storybook/react-vite";
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
5
+ addons: [
6
+ "@storybook/addon-essentials",
7
+ "@storybook/addon-actions",
8
+ "@storybook/addon-onboarding",
9
+ ],
10
+ framework: {
11
+ name: "@storybook/react-vite",
12
+ options: {},
13
+ },
14
+ };
15
+
16
+ export default config;
@@ -0,0 +1,53 @@
1
+ import React from "react";
2
+ import type { Preview } from "@storybook/react";
3
+ import "@/app/styles/index.css";
4
+ import "@/shared/components/ImageEditor/index.css";
5
+
6
+ const preview: Preview = {
7
+ parameters: {
8
+ controls: {
9
+ matchers: {
10
+ color: /(background|color)$/i,
11
+ date: /Date$/i,
12
+ },
13
+ },
14
+ },
15
+ globalTypes: {
16
+ theme: {
17
+ description: "Global theme for components",
18
+ defaultValue: "light",
19
+ toolbar: {
20
+ title: "Theme",
21
+ icon: "circlehollow",
22
+ items: [
23
+ { value: "light", icon: "sun", title: "Light Mode" },
24
+ { value: "dark", icon: "moon", title: "Dark Mode" },
25
+ ],
26
+ dynamicTitle: true,
27
+ },
28
+ },
29
+ },
30
+ decorators: [
31
+ (Story, context) => {
32
+ const selectedTheme = context.globals.theme || "light";
33
+
34
+ return React.createElement(
35
+ "div",
36
+ {
37
+ "data-theme": selectedTheme,
38
+ className: "root",
39
+ style: {
40
+ padding: 24,
41
+ minHeight: "100vh",
42
+ backgroundColor:
43
+ selectedTheme === "dark" ? "#22252a" : "oklch(1 0 0)",
44
+ transition: "background-color 0.2s ease",
45
+ },
46
+ },
47
+ React.createElement(Story),
48
+ );
49
+ },
50
+ ],
51
+ };
52
+
53
+ export default preview;
@@ -0,0 +1,6 @@
1
+ {
2
+ "recommendations": [
3
+ "esbenp.prettier-vscode",
4
+ "dbaeumer.vscode-eslint"
5
+ ]
6
+ }
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # React + TypeScript + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
17
+
18
+ ```js
19
+ export default defineConfig([
20
+ globalIgnores(['dist']),
21
+ {
22
+ files: ['**/*.{ts,tsx}'],
23
+ extends: [
24
+ // Other configs...
25
+
26
+ // Remove tseslint.configs.recommended and replace with this
27
+ tseslint.configs.recommendedTypeChecked,
28
+ // Alternatively, use this for stricter rules
29
+ tseslint.configs.strictTypeChecked,
30
+ // Optionally, add this for stylistic rules
31
+ tseslint.configs.stylisticTypeChecked,
32
+
33
+ // Other configs...
34
+ ],
35
+ languageOptions: {
36
+ parserOptions: {
37
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
38
+ tsconfigRootDir: import.meta.dirname,
39
+ },
40
+ // other options...
41
+ },
42
+ },
43
+ ])
44
+
45
+ ```
46
+
47
+ You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
48
+
49
+ ```js
50
+ // eslint.config.js
51
+ import reactX from 'eslint-plugin-react-x'
52
+ import reactDom from 'eslint-plugin-react-dom'
53
+
54
+ export default defineConfig([
55
+ globalIgnores(['dist']),
56
+ {
57
+ files: ['**/*.{ts,tsx}'],
58
+ extends: [
59
+ // Other configs...
60
+ // Enable lint rules for React
61
+ reactX.configs['recommended-typescript'],
62
+ // Enable lint rules for React DOM
63
+ reactDom.configs.recommended,
64
+ ],
65
+ languageOptions: {
66
+ parserOptions: {
67
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
68
+ tsconfigRootDir: import.meta.dirname,
69
+ },
70
+ // other options...
71
+ },
72
+ },
73
+ ])
74
+
75
+ ```
@@ -0,0 +1,22 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+ import { defineConfig, globalIgnores } from 'eslint/config'
7
+
8
+ export default defineConfig([
9
+ globalIgnores(['dist']),
10
+ {
11
+ files: ['**/*.{ts,tsx}'],
12
+ extends: [
13
+ js.configs.recommended,
14
+ tseslint.configs.recommended,
15
+ reactHooks.configs.flat.recommended,
16
+ reactRefresh.configs.vite,
17
+ ],
18
+ languageOptions: {
19
+ globals: globals.browser,
20
+ },
21
+ },
22
+ ])
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>picedi</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "pixedi",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "./dist/lib/index.umd.js",
6
+ "module": "./dist/lib/index.js",
7
+ "types": "./dist/lib/index.d.ts",
8
+ "dependencies": {
9
+ "react-router": "^8.0.1"
10
+ },
11
+ "peerDependencies": {
12
+ "react": "^19.2.7",
13
+ "react-dom": "^19.2.7"
14
+ },
15
+ "devDependencies": {
16
+ "@eslint/js": "^10.0.1",
17
+ "@storybook/addon-actions": "^8.6.0",
18
+ "@storybook/addon-essentials": "^8.6.0",
19
+ "@storybook/addon-onboarding": "^8.6.0",
20
+ "@storybook/blocks": "^8.6.0",
21
+ "@storybook/react": "^8.6.0",
22
+ "@storybook/react-vite": "^8.6.0",
23
+ "@storybook/test": "^8.6.0",
24
+ "@types/node": "^24.13.2",
25
+ "@types/react": "^19.2.17",
26
+ "@types/react-dom": "^19.2.3",
27
+ "@vitejs/plugin-react": "^6.0.3",
28
+ "eslint": "^10.6.0",
29
+ "eslint-plugin-react-hooks": "^7.1.1",
30
+ "eslint-plugin-react-refresh": "^0.5.3",
31
+ "globals": "^17.7.0",
32
+ "prettier": "^3.8.5",
33
+ "storybook": "^8.6.0",
34
+ "typescript": "~6.0.2",
35
+ "typescript-eslint": "^8.62.0",
36
+ "vite": "^8.1.1",
37
+ "vite-plugin-css-injected-by-js": "^5.0.1",
38
+ "@testing-library/jest-dom": "^6.9.1",
39
+ "@testing-library/react": "^16.3.2",
40
+ "jsdom": "^27.4.0",
41
+ "vitest": "^4.0.18"
42
+ },
43
+ "scripts": {
44
+ "dev": "vite",
45
+ "build:app": "tsc -b tsconfig.app.json && vite build",
46
+ "build:widget": "vite build --config vite.widget.config.ts --mode production",
47
+ "build:lib": "vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json",
48
+ "build": "pnpm build:app && pnpm build:widget && pnpm build:lib",
49
+ "lint": "eslint .",
50
+ "preview": "vite preview",
51
+ "storybook": "storybook dev -p 6006",
52
+ "build-storybook": "storybook build",
53
+ "type-check": "tsc -b tsconfig.app.json",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest"
56
+ }
57
+ }
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - '.'
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,10 @@
1
+ import { RouterProvider } from "@/app/providers";
2
+ import { AppRouter } from "@/app/router/AppRouter";
3
+
4
+ export default function App() {
5
+ return (
6
+ <RouterProvider>
7
+ <AppRouter />
8
+ </RouterProvider>
9
+ );
10
+ }
@@ -0,0 +1,145 @@
1
+ import type { Image } from "@/shared/types";
2
+
3
+ export const imagesData: Image[] = [
4
+ {
5
+ id: "20",
6
+ author: "Aleks Dorohovich",
7
+ title: "Creative desktop with laptop and smartphone",
8
+ width: 918,
9
+ height: 616,
10
+ ext: "jpg",
11
+ fileName: "sunset_over_lake.jpg",
12
+ },
13
+ {
14
+ id: "35",
15
+ author: "Shane Colella",
16
+ title: "Mountain Peaks in Spring Surrounded",
17
+ width: 2758,
18
+ height: 3622,
19
+ ext: "jpg",
20
+ fileName: "mountain_peaks.jpg",
21
+ },
22
+ {
23
+ id: "42",
24
+ author: "Luke Chesser",
25
+ width: 3456,
26
+ height: 2304,
27
+ title: "Morning in a cozy cafe",
28
+ ext: "jpg",
29
+ fileName: "calm_forest_morning.jpg",
30
+ },
31
+ {
32
+ id: "43",
33
+ author: "Oleg Chursin",
34
+ title: "Old Town Alleyway Bathed in Warm Golden Sunlight",
35
+ width: 1280,
36
+ height: 831,
37
+ ext: "jpg",
38
+ fileName: "old_town_alleyway_sunlight.jpg",
39
+ },
40
+ {
41
+ id: "57",
42
+ author: "Nicholas Swanson",
43
+ title: "Golden Wheat Fields",
44
+ width: 2448,
45
+ height: 3264,
46
+ ext: "jpg",
47
+ fileName: "golden_wheat_fields.jpg",
48
+ },
49
+ {
50
+ id: "75",
51
+ author: "Jessy Onyae",
52
+ title: "Ocean Waves Crashing Against Rocky Shores at Sunset",
53
+ width: 1999,
54
+ height: 2998,
55
+ ext: "jpg",
56
+ fileName: "ocean_waves_rocky.jpg",
57
+ },
58
+ {
59
+ id: "78",
60
+ author: "Paul Evens",
61
+ title: "Quiet Countryside Road",
62
+ width: 1584,
63
+ height: 2376,
64
+ ext: "jpg",
65
+ fileName: "quiet_countryside_road.jpg",
66
+ },
67
+ {
68
+ id: "90",
69
+ author: "Rula Sibai",
70
+ title: "Blooming Spring Garden",
71
+ width: 3000,
72
+ height: 1992,
73
+ ext: "jpg",
74
+ fileName: "blooming_spring_garden.jpg",
75
+ },
76
+ {
77
+ id: "106",
78
+ author: "Arvee Marie",
79
+ title: "Snowy Mountain Trail",
80
+ width: 2592,
81
+ height: 1728,
82
+ ext: "jpg",
83
+ fileName: "snowy_mountain_trail.jpg",
84
+ },
85
+ {
86
+ id: "110",
87
+ author: "Kenneth Thewissen",
88
+ title: "Starry Night Sky Illuminated by the Milky Way",
89
+ width: 5000,
90
+ height: 3333,
91
+ ext: "jpg",
92
+ fileName: "starry_night_milky_way.jpg",
93
+ },
94
+ {
95
+ id: "163",
96
+ author: "Lyhn Nguyen",
97
+ title: "Peaceful Riverside View",
98
+ width: 2000,
99
+ height: 1333,
100
+ ext: "jpg",
101
+ fileName: "peaceful_riverside_view.jpg",
102
+ },
103
+ {
104
+ id: "21",
105
+ author: "Alejandro Escamilla",
106
+ title: "White shoes on the woody floor",
107
+ width: 3008,
108
+ height: 2008,
109
+ ext: "jpg",
110
+ fileName: "white_shoes_wooden_floor.jpg",
111
+ },
112
+ {
113
+ id: "26",
114
+ author: "Vadim Sherbakov",
115
+ title: "Accessories",
116
+ width: 4209,
117
+ height: 2769,
118
+ ext: "jpg",
119
+ fileName: "accessories.jpg",
120
+ },
121
+ {
122
+ id: "88",
123
+ author: "Barcelona",
124
+ title: "Car traffic",
125
+ width: 1280,
126
+ height: 1707,
127
+ ext: "jpg",
128
+ fileName: "car_traffic.jpg",
129
+ },
130
+ {
131
+ id: "116",
132
+ author: "Anton Sulsky",
133
+ title: "Tuscany Hills",
134
+ width: 3504,
135
+ height: 2336,
136
+ ext: "jpg",
137
+ fileName: "tuscany_hills.jpg",
138
+ },
139
+ ].map((image) => ({
140
+ ...image,
141
+ thumbnail: `https://picsum.photos/id/${image.id}/400/300`,
142
+ original: `https://picsum.photos/id/${image.id}/${image.width}/${image.height}`,
143
+ }));
144
+
145
+ //https://picsum.photos/id/26/4209/2769
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from "react";
2
+ import { BrowserRouter } from "react-router";
3
+
4
+ export const RouterProvider = ({ children }: { children: ReactNode }) => {
5
+ return <BrowserRouter>{children}</BrowserRouter>;
6
+ };
@@ -0,0 +1 @@
1
+ export { RouterProvider } from "./RouterProvider";
@@ -0,0 +1,12 @@
1
+ import { Route, Routes } from "react-router";
2
+ import { ROUTES } from "@/shared/config";
3
+ import { GalleryPage, ImageEditPage } from "@/features/gallery";
4
+
5
+ export const AppRouter = () => {
6
+ return (
7
+ <Routes>
8
+ <Route path={ROUTES.HOME} element={<GalleryPage />} />
9
+ <Route path={ROUTES.IMAGE_EDIT} element={<ImageEditPage />} />
10
+ </Routes>
11
+ );
12
+ };
File without changes
@@ -0,0 +1,4 @@
1
+ body {
2
+ margin: 0;
3
+ padding: 0;
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" style={{ width: "100%", height: "100vh" }}>
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";
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import "@/app/styles/index.css";
4
+ import App from "@/app/App.tsx";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );