work in progress

This commit is contained in:
Artur AGH
2023-10-02 15:02:45 +02:00
parent 658dfde935
commit 55cd3f963d
14 changed files with 398 additions and 38 deletions

View File

@@ -11,8 +11,11 @@
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.17",
"@next-auth/prisma-adapter": "^1.0.7", "@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.1.1", "@prisma/client": "^5.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@reduxjs/toolkit": "^1.9.6",
"@t3-oss/env-nextjs": "^0.6.0", "@t3-oss/env-nextjs": "^0.6.0",
"@tanstack/react-query": "^4.32.6", "@tanstack/react-query": "^4.32.6",
"@trpc/client": "^10.37.1", "@trpc/client": "^10.37.1",
@@ -23,11 +26,14 @@
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"konva": "^9.2.0", "konva": "^9.2.0",
"lucide-react": "^0.279.0",
"next": "^13.4.19", "next": "^13.4.19",
"next-auth": "^4.23.0", "next-auth": "^4.23.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.11.0",
"react-konva": "^18.2.10", "react-konva": "^18.2.10",
"react-redux": "^8.1.3",
"superjson": "^1.13.1", "superjson": "^1.13.1",
"tailwind-merge": "^1.14.0", "tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

152
pnpm-lock.yaml generated
View File

@@ -5,12 +5,21 @@ settings:
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
dependencies: dependencies:
'@headlessui/react':
specifier: ^1.7.17
version: 1.7.17(react-dom@18.2.0)(react@18.2.0)
'@next-auth/prisma-adapter': '@next-auth/prisma-adapter':
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.0.7(@prisma/client@5.2.0)(next-auth@4.23.1) version: 1.0.7(@prisma/client@5.2.0)(next-auth@4.23.1)
'@prisma/client': '@prisma/client':
specifier: ^5.1.1 specifier: ^5.1.1
version: 5.2.0(prisma@5.2.0) version: 5.2.0(prisma@5.2.0)
'@radix-ui/react-icons':
specifier: ^1.3.0
version: 1.3.0(react@18.2.0)
'@reduxjs/toolkit':
specifier: ^1.9.6
version: 1.9.6(react-redux@8.1.3)(react@18.2.0)
'@t3-oss/env-nextjs': '@t3-oss/env-nextjs':
specifier: ^0.6.0 specifier: ^0.6.0
version: 0.6.1(typescript@5.2.2)(zod@3.22.2) version: 0.6.1(typescript@5.2.2)(zod@3.22.2)
@@ -41,6 +50,9 @@ dependencies:
konva: konva:
specifier: ^9.2.0 specifier: ^9.2.0
version: 9.2.0 version: 9.2.0
lucide-react:
specifier: ^0.279.0
version: 0.279.0(react@18.2.0)
next: next:
specifier: ^13.4.19 specifier: ^13.4.19
version: 13.4.19(react-dom@18.2.0)(react@18.2.0) version: 13.4.19(react-dom@18.2.0)(react@18.2.0)
@@ -53,9 +65,15 @@ dependencies:
react-dom: react-dom:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
react-icons:
specifier: ^4.11.0
version: 4.11.0(react@18.2.0)
react-konva: react-konva:
specifier: ^18.2.10 specifier: ^18.2.10
version: 18.2.10(konva@9.2.0)(react-dom@18.2.0)(react@18.2.0) version: 18.2.10(konva@9.2.0)(react-dom@18.2.0)(react@18.2.0)
react-redux:
specifier: ^8.1.3
version: 8.1.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
superjson: superjson:
specifier: ^1.13.1 specifier: ^1.13.1
version: 1.13.1 version: 1.13.1
@@ -176,6 +194,18 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/@headlessui/react@1.7.17(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==}
engines: {node: '>=10'}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
dependencies:
client-only: 0.0.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@humanwhocodes/config-array@0.11.11: /@humanwhocodes/config-array@0.11.11:
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==} resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@@ -366,6 +396,33 @@ packages:
resolution: {integrity: sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==} resolution: {integrity: sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==}
requiresBuild: true requiresBuild: true
/@radix-ui/react-icons@1.3.0(react@18.2.0):
resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
peerDependencies:
react: ^16.x || ^17.x || ^18.x
dependencies:
react: 18.2.0
dev: false
/@reduxjs/toolkit@1.9.6(react-redux@8.1.3)(react@18.2.0):
resolution: {integrity: sha512-Gc4ikl90ORF4viIdAkY06JNUnODjKfGxZRwATM30EdHq8hLSVoSrwXne5dd739yenP5bJxAX7tLuOWK5RPGtrw==}
peerDependencies:
react: ^16.9.0 || ^17.0.0 || ^18
react-redux: ^7.2.1 || ^8.0.2
peerDependenciesMeta:
react:
optional: true
react-redux:
optional: true
dependencies:
immer: 9.0.21
react: 18.2.0
react-redux: 8.1.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
redux: 4.2.1
redux-thunk: 2.4.2(redux@4.2.1)
reselect: 4.1.8
dev: false
/@rushstack/eslint-patch@1.3.3: /@rushstack/eslint-patch@1.3.3:
resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==} resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==}
dev: true dev: true
@@ -479,6 +536,13 @@ packages:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
dev: true dev: true
/@types/hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==}
dependencies:
'@types/react': 18.2.21
hoist-non-react-statics: 3.3.2
dev: false
/@types/json-schema@7.0.12: /@types/json-schema@7.0.12:
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
dev: true dev: true
@@ -498,7 +562,6 @@ packages:
resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==} resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==}
dependencies: dependencies:
'@types/react': 18.2.21 '@types/react': 18.2.21
dev: true
/@types/react-reconciler@0.28.4: /@types/react-reconciler@0.28.4:
resolution: {integrity: sha512-Xd55E2aLI9Q/ikDQEmfRzIwYJs4oO0h9ZHA3FZDakzt1WR6JMZcpqtCZlF97I72KVjoY4rHXU5TfvkRDOyr/rg==} resolution: {integrity: sha512-Xd55E2aLI9Q/ikDQEmfRzIwYJs4oO0h9ZHA3FZDakzt1WR6JMZcpqtCZlF97I72KVjoY4rHXU5TfvkRDOyr/rg==}
@@ -520,6 +583,10 @@ packages:
resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==} resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==}
dev: true dev: true
/@types/use-sync-external-store@0.0.3:
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
dev: false
/@types/uuid@9.0.3: /@types/uuid@9.0.3:
resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==} resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==}
dev: false dev: false
@@ -1677,11 +1744,21 @@ packages:
dependencies: dependencies:
function-bind: 1.1.1 function-bind: 1.1.1
/hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies:
react-is: 16.13.1
dev: false
/ignore@5.2.4: /ignore@5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
dev: true dev: true
/immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
dev: false
/import-fresh@3.3.0: /import-fresh@3.3.0:
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
engines: {node: '>=6'} engines: {node: '>=6'}
@@ -2005,6 +2082,14 @@ packages:
dependencies: dependencies:
yallist: 4.0.0 yallist: 4.0.0
/lucide-react@0.279.0(react@18.2.0):
resolution: {integrity: sha512-LJ8g66+Bxc3t3x9vKTeK3wn3xucrOQGfJ9ou9GsBwCt2offsrT2BB90XrTrIzE1noYYDe2O8jZaRHi6sAHXNxw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/merge2@1.4.1: /merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -2484,9 +2569,20 @@ packages:
scheduler: 0.23.0 scheduler: 0.23.0
dev: false dev: false
/react-icons@4.11.0(react@18.2.0):
resolution: {integrity: sha512-V+4khzYcE5EBk/BvcuYRq6V/osf11ODUM2J8hg2FDSswRrGvqiYUYPRy4OdrWaQOBj4NcpJfmHZLNaD+VH0TyA==}
peerDependencies:
react: '*'
dependencies:
react: 18.2.0
dev: false
/react-is@16.13.1: /react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: true
/react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: false
/react-konva@18.2.10(konva@9.2.0)(react-dom@18.2.0)(react@18.2.0): /react-konva@18.2.10(konva@9.2.0)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==} resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==}
@@ -2515,6 +2611,40 @@ packages:
scheduler: 0.23.0 scheduler: 0.23.0
dev: false dev: false
/react-redux@8.1.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1):
resolution: {integrity: sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==}
peerDependencies:
'@types/react': ^16.8 || ^17.0 || ^18.0
'@types/react-dom': ^16.8 || ^17.0 || ^18.0
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
react-native: '>=0.59'
redux: ^4 || ^5.0.0-beta.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
react-dom:
optional: true
react-native:
optional: true
redux:
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@types/hoist-non-react-statics': 3.3.2
'@types/react': 18.2.21
'@types/react-dom': 18.2.7
'@types/use-sync-external-store': 0.0.3
hoist-non-react-statics: 3.3.2
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-is: 18.2.0
redux: 4.2.1
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/react-ssr-prepass@1.5.0(react@18.2.0): /react-ssr-prepass@1.5.0(react@18.2.0):
resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==} resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==}
peerDependencies: peerDependencies:
@@ -2541,6 +2671,20 @@ packages:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
/redux-thunk@2.4.2(redux@4.2.1):
resolution: {integrity: sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==}
peerDependencies:
redux: ^4
dependencies:
redux: 4.2.1
dev: false
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
'@babel/runtime': 7.22.15
dev: false
/reflect.getprototypeof@1.0.4: /reflect.getprototypeof@1.0.4:
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2565,6 +2709,10 @@ packages:
functions-have-names: 1.2.3 functions-have-names: 1.2.3
dev: true dev: true
/reselect@4.1.8:
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
dev: false
/resolve-from@4.0.0: /resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'} engines: {node: '>=4'}

View File

@@ -4,17 +4,31 @@ import {
} from "@/components/transformable-image"; } from "@/components/transformable-image";
import type { KonvaEventObject } from "konva/lib/Node"; import type { KonvaEventObject } from "konva/lib/Node";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, FormEventHandler, useState } from "react";
import { Layer, Stage } from "react-konva"; import { Layer, Stage } from "react-konva";
import { v1 } from "uuid"; import { v1 } from "uuid";
import TransformableText, { import TransformableText, {
TransformableTextProps, TransformableTextProps,
} from "./transformable-text"; } from "./transformable-text";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { useAppDispatch, useAppSelector } from "@/hooks";
import { appSlice } from "@/store/app.slice";
// Provider *
const Canvas = () => { const Canvas = () => {
const dispatch = useAppDispatch()
const selectedItemId = useAppSelector((state) => state.app.selectedItemId)
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id))
const [selectedImageId, selectImage] = useState<string | null>(null); const [selectedImageId, selectImage] = useState<string | null>(null);
const [selectedTextId, selectText] = useState<string | null>(null); const [selectedTextId, selectText] = useState<string | null>(null);
const [inputText, setInputText] = useState("");
const [images, setImages] = useState<TransformableImageProps["imageProps"][]>( const [images, setImages] = useState<TransformableImageProps["imageProps"][]>(
[], [],
@@ -22,47 +36,25 @@ const Canvas = () => {
const [texts, setTexts] = useState<TransformableTextProps["textProps"][]>([]); const [texts, setTexts] = useState<TransformableTextProps["textProps"][]>([]);
const checkDeselect = (e: KonvaEventObject<MouseEvent>) => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage(); const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (clickedOnEmpty) { setInputText(e.target.value);
selectImage(null);
}
}; };
const handleImageUploaded = (e: ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const imageId = v1();
const imageUrl = URL.createObjectURL(file);
setImages((prev) => [...prev, { imageUrl, imageId }]);
};
const handleTextAdd = (e: ChangeEvent<HTMLInputElement>) => {
const text = e.target.value;
const textId = v1();
setTexts((prev) => [...prev, { text, textId }]);
};
/*console.log(texts, " ++++ ", images);*/
return ( return (
<main> <main className="flex">
<input type="file" onChange={handleImageUploaded} />
<input
type="text"
onChange={handleTextAdd}
placeholder="tape your text"
/>
<Stage width={window.innerWidth} height={window.innerHeight}> <Stage width={window.innerWidth} height={window.innerHeight}>
<Layer> <Layer>
{images.map((image) => { {images.map((image) => {
return ( return (
<TransformableImage <TransformableImage
onSelect={() => selectImage(image.imageId)} onSelect={() => selectItem(image.imageId)}
isSelected={image.imageId === selectedImageId} isSelected={image.imageId === selectedItemId}
onChange={(newAttrs) => { onChange={(newAttrs) => {
setImages( setImages(
images.map((i) => images.map((i) =>
@@ -78,8 +70,8 @@ const Canvas = () => {
{texts.map((text) => { {texts.map((text) => {
return ( return (
<TransformableText <TransformableText
onSelect={() => selectText(text.textId)} onSelect={() => selectItem(text.textId)}
isSelected={text.textId === selectedTextId} isSelected={text.textId === selectedItemId}
onChange={(newAttrs) => { onChange={(newAttrs) => {
setTexts( setTexts(
texts.map((t) => (t.textId === text.textId ? newAttrs : t)), texts.map((t) => (t.textId === text.textId ? newAttrs : t)),

View File

@@ -0,0 +1,15 @@
import { ReactNode } from "react";
import { Sidebar } from "./sidebar";
type Props = {
children: ReactNode;
};
export const Layout = ({ children }: Props) => {
return (
<div className="flex h-full ">
<Sidebar {} />
<main>{children}</main>
</div>
);
};

View File

@@ -0,0 +1,37 @@
import { Button } from "../ui/button";
import { Input } from "../ui/input";
type Props = {
handleImageUploaded: any;
handleInputChange: any;
inputText: any;
handleTextAdd: any;
};
export function Sidebar({
handleImageUploaded,
handleInputChange,
handleTextAdd,
inputText,
}: Props) {
return (
<div className="flex flex-col ">
<Input
type="file"
className="m-[2rem] w-auto "
onChange={handleImageUploaded}
/>
<div className="m-[2rem] flex max-w-md justify-between">
<Input
type="text"
placeholder="enter the text"
value={inputText}
onChange={handleInputChange}
/>
<Button className="mx-[2rem] text-xs" onClick={handleTextAdd}>
Add new Text
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from '@radix-ui/react-icons'
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

2
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./use-app-dispatch"
export * from "./use-app-selector"

View File

@@ -0,0 +1,4 @@
import type { AppDispatch } from "@/store/store";
import { useDispatch } from "react-redux";
export const useAppDispatch: () => AppDispatch = useDispatch

View File

@@ -0,0 +1,4 @@
import type { RootState } from "@/store/store";
import { TypedUseSelectorHook, useSelector } from "react-redux";
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

View File

@@ -5,6 +5,7 @@ import { type AppType } from "next/app";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { Layout } from "@/components/layout/layout";
const MyApp: AppType<{ session: Session | null }> = ({ const MyApp: AppType<{ session: Session | null }> = ({
Component, Component,
@@ -12,7 +13,9 @@ const MyApp: AppType<{ session: Session | null }> = ({
}) => { }) => {
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<Component {...pageProps} /> <Layout>
<Component {...pageProps} />
</Layout>
</SessionProvider> </SessionProvider>
); );
}; };

View File

@@ -1,7 +1,17 @@
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import Head from "next/head";
const Canvas = dynamic(() => import("../components/canvas"), { ssr: false }); const Canvas = dynamic(() => import("../components/canvas"), { ssr: false });
export default function Home() { export default function Home() {
return <Canvas />; return (
<>
<Head>
<title>Labbel Application</title>
</Head>
<div className="flex">
<Canvas />
</div>
</>
);
} }

47
src/store/app.slice.ts Normal file
View File

@@ -0,0 +1,47 @@
import { TransformableImageProps } from "@/components/transformable-image";
import { TransformableTextProps } from "@/components/transformable-text";
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
import { KonvaEventObject } from "konva/lib/Node";
import { ChangeEvent } from "react";
import { v1 } from "uuid";
const initialState = {
selectedItemId: null as string | null,
images: [] as TransformableImageProps["imageProps"][],
texts: [] as TransformableTextProps["textProps"][],
};
export const appSlice = createSlice({
name: "app",
initialState,
reducers: {
addImage: (state, action: PayloadAction<ChangeEvent<HTMLInputElement>>) => {
const file = action.payload.target.files?.[0];
if (!file) return;
const imageId = v1();
const imageUrl = URL.createObjectURL(file);
state.images.push({ imageUrl, id: imageId });
},
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
const textId = v1();
state.texts.push({ text: action.payload.initialValue, id: textId });
},
selectItem: (state, action: PayloadAction<string>) => {
state.selectedItemId = action.payload;
},
checkDeselect: (
state,
action: PayloadAction<KonvaEventObject<MouseEvent>>,
) => {
// deselect when clicked on empty area
const clickedOnEmpty =
action.payload.target === action.payload.target.getStage();
if (clickedOnEmpty) {
state.selectedItemId = null;
}
},
},
});

11
src/store/store.ts Normal file
View File

@@ -0,0 +1,11 @@
import { configureStore } from "@reduxjs/toolkit";
import { appSlice } from "./app.slice";
export const store = configureStore({
reducer: {
[appSlice.name]: appSlice.reducer,
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;