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.
- package/.github/workflows/deploy.yml +54 -0
- package/.prettierr +9 -0
- package/.storybook/main.ts +16 -0
- package/.storybook/preview.ts +53 -0
- package/.vscode/extensions.json +6 -0
- package/README.md +75 -0
- package/eslint.config.js +22 -0
- package/index.html +13 -0
- package/package.json +57 -0
- package/pnpm-workspace.yaml +2 -0
- package/public/favicon.svg +1 -0
- package/src/app/App.tsx +10 -0
- package/src/app/entries/index.ts +145 -0
- package/src/app/providers/RouterProvider.tsx +6 -0
- package/src/app/providers/index.ts +1 -0
- package/src/app/router/AppRouter.tsx +12 -0
- package/src/app/router/index.ts +0 -0
- package/src/app/styles/index.css +4 -0
- package/src/features/gallery/GalleryPage.tsx +33 -0
- package/src/features/gallery/ImageEditPage.tsx +37 -0
- package/src/features/gallery/index.ts +2 -0
- package/src/main.tsx +10 -0
- package/src/shared/components/ImageCard/ImageCard.tsx +54 -0
- package/src/shared/components/ImageCard/index.ts +1 -0
- package/src/shared/components/ImageEditor/ImageEditor.tsx +48 -0
- package/src/shared/components/ImageEditor/assets/icons.tsx +114 -0
- package/src/shared/components/ImageEditor/constants.ts +118 -0
- package/src/shared/components/ImageEditor/crop/Crop.tsx +60 -0
- package/src/shared/components/ImageEditor/crop/CropBorders.tsx +28 -0
- package/src/shared/components/ImageEditor/crop/CropLines.tsx +8 -0
- package/src/shared/components/ImageEditor/crop/CropPointer.tsx +21 -0
- package/src/shared/components/ImageEditor/crop/CropPointers.tsx +44 -0
- package/src/shared/components/ImageEditor/crop/CropTools.tsx +138 -0
- package/src/shared/components/ImageEditor/crop/crop.css +180 -0
- package/src/shared/components/ImageEditor/crop/index.ts +8 -0
- package/src/shared/components/ImageEditor/crop/useCropInteraction.ts +230 -0
- package/src/shared/components/ImageEditor/eventBus.ts +8 -0
- package/src/shared/components/ImageEditor/frame/Frame.tsx +48 -0
- package/src/shared/components/ImageEditor/frame/frame.css +40 -0
- package/src/shared/components/ImageEditor/frame/index.ts +1 -0
- package/src/shared/components/ImageEditor/header/Header.tsx +81 -0
- package/src/shared/components/ImageEditor/header/header.css +27 -0
- package/src/shared/components/ImageEditor/header/index.ts +1 -0
- package/src/shared/components/ImageEditor/hooks/index.ts +5 -0
- package/src/shared/components/ImageEditor/hooks/useAbove.tsx +18 -0
- package/src/shared/components/ImageEditor/hooks/useBellow.tsx +18 -0
- package/src/shared/components/ImageEditor/hooks/useImageLoader.tsx +110 -0
- package/src/shared/components/ImageEditor/hooks/useImageProcessor.test.tsx +147 -0
- package/src/shared/components/ImageEditor/hooks/useImageProcessor.ts +225 -0
- package/src/shared/components/ImageEditor/hooks/useMobile.tsx +33 -0
- package/src/shared/components/ImageEditor/index.css +145 -0
- package/src/shared/components/ImageEditor/index.ts +1 -0
- package/src/shared/components/ImageEditor/provider/ImageEditorProvider.test.tsx +126 -0
- package/src/shared/components/ImageEditor/provider/ImageEditorProvider.tsx +107 -0
- package/src/shared/components/ImageEditor/provider/StoreContext.tsx +20 -0
- package/src/shared/components/ImageEditor/provider/initialState.ts +30 -0
- package/src/shared/components/ImageEditor/provider/useImageEditorContext.tsx +12 -0
- package/src/shared/components/ImageEditor/resize/ResizeTools.tsx +183 -0
- package/src/shared/components/ImageEditor/resize/index.ts +1 -0
- package/src/shared/components/ImageEditor/resize/resize.css +56 -0
- package/src/shared/components/ImageEditor/sidebar/Sidebar.tsx +54 -0
- package/src/shared/components/ImageEditor/sidebar/SidebarCrop.tsx +57 -0
- package/src/shared/components/ImageEditor/sidebar/SidebarPresets.tsx +54 -0
- package/src/shared/components/ImageEditor/sidebar/SidebarResize.tsx +26 -0
- package/src/shared/components/ImageEditor/sidebar/index.ts +1 -0
- package/src/shared/components/ImageEditor/sidebar/sidebar.css +101 -0
- package/src/shared/components/ImageEditor/types.ts +59 -0
- package/src/shared/components/ImageEditor/ui/button/Button.stories.tsx +42 -0
- package/src/shared/components/ImageEditor/ui/button/Button.tsx +21 -0
- package/src/shared/components/ImageEditor/ui/button/button.css +69 -0
- package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.stories.tsx +26 -0
- package/src/shared/components/ImageEditor/ui/button-crop/ButtonCrop.tsx +29 -0
- package/src/shared/components/ImageEditor/ui/button-crop/button-crop.css +38 -0
- package/src/shared/components/ImageEditor/ui/index.ts +6 -0
- package/src/shared/components/ImageEditor/ui/input/Input.stories.tsx +45 -0
- package/src/shared/components/ImageEditor/ui/input/Input.tsx +20 -0
- package/src/shared/components/ImageEditor/ui/input/input.css +48 -0
- package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.stories.tsx +33 -0
- package/src/shared/components/ImageEditor/ui/input-pixel/InputPixel.tsx +23 -0
- package/src/shared/components/ImageEditor/ui/input-pixel/input-pixel.css +24 -0
- package/src/shared/components/ImageEditor/ui/select/Select.stories.tsx +67 -0
- package/src/shared/components/ImageEditor/ui/select/Select.tsx +161 -0
- package/src/shared/components/ImageEditor/ui/select/select.css +147 -0
- package/src/shared/components/ImageEditor/ui/separator/Separator.stories.tsx +37 -0
- package/src/shared/components/ImageEditor/ui/separator/Separator.tsx +27 -0
- package/src/shared/components/ImageEditor/ui/separator/separator.css +19 -0
- package/src/shared/components/ImageEditor/utils.test.ts +136 -0
- package/src/shared/components/ImageEditor/utils.ts +497 -0
- package/src/shared/components/ui/index.ts +1 -0
- package/src/shared/components/ui/preloader.tsx +78 -0
- package/src/shared/config/index.ts +3 -0
- package/src/shared/config/routes.ts +4 -0
- package/src/shared/features/gallery/GalleryPage.tsx +33 -0
- package/src/shared/features/gallery/ImageEditPage.tsx +37 -0
- package/src/shared/features/gallery/index.ts +2 -0
- package/src/shared/hooks/index.ts +1 -0
- package/src/shared/hooks/useImagePreload.tsx +49 -0
- package/src/shared/types/index.ts +11 -0
- package/src/test/setup.ts +14 -0
- package/src/virtual-css-injected-by-js.d.ts +8 -0
- package/src/widget.tsx +119 -0
- package/tsconfig.app.json +29 -0
- package/tsconfig.json +12 -0
- package/tsconfig.lib.json +18 -0
- package/tsconfig.node.json +23 -0
- package/vite-env.d.ts +1 -0
- package/vite.config.ts +24 -0
- package/vite.lib.config.ts +33 -0
- package/vite.widget.config.ts +39 -0
- 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,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;
|
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
|
+
```
|
package/eslint.config.js
ADDED
|
@@ -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 @@
|
|
|
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>
|
package/src/app/App.tsx
ADDED
|
@@ -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 @@
|
|
|
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,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
|
+
};
|
package/src/main.tsx
ADDED