mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-17 05:29:27 +00:00
work in progress
This commit is contained in:
@@ -11,8 +11,11 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.17",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@prisma/client": "^5.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@reduxjs/toolkit": "^1.9.6",
|
||||
"@t3-oss/env-nextjs": "^0.6.0",
|
||||
"@tanstack/react-query": "^4.32.6",
|
||||
"@trpc/client": "^10.37.1",
|
||||
@@ -23,11 +26,14 @@
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.0.0",
|
||||
"konva": "^9.2.0",
|
||||
"lucide-react": "^0.279.0",
|
||||
"next": "^13.4.19",
|
||||
"next-auth": "^4.23.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^4.11.0",
|
||||
"react-konva": "^18.2.10",
|
||||
"react-redux": "^8.1.3",
|
||||
"superjson": "^1.13.1",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
|
||||
152
pnpm-lock.yaml
generated
152
pnpm-lock.yaml
generated
@@ -5,12 +5,21 @@ settings:
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@headlessui/react':
|
||||
specifier: ^1.7.17
|
||||
version: 1.7.17(react-dom@18.2.0)(react@18.2.0)
|
||||
'@next-auth/prisma-adapter':
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(@prisma/client@5.2.0)(next-auth@4.23.1)
|
||||
'@prisma/client':
|
||||
specifier: ^5.1.1
|
||||
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':
|
||||
specifier: ^0.6.0
|
||||
version: 0.6.1(typescript@5.2.2)(zod@3.22.2)
|
||||
@@ -41,6 +50,9 @@ dependencies:
|
||||
konva:
|
||||
specifier: ^9.2.0
|
||||
version: 9.2.0
|
||||
lucide-react:
|
||||
specifier: ^0.279.0
|
||||
version: 0.279.0(react@18.2.0)
|
||||
next:
|
||||
specifier: ^13.4.19
|
||||
version: 13.4.19(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -53,9 +65,15 @@ dependencies:
|
||||
react-dom:
|
||||
specifier: 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:
|
||||
specifier: ^18.2.10
|
||||
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:
|
||||
specifier: ^1.13.1
|
||||
version: 1.13.1
|
||||
@@ -176,6 +194,18 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
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:
|
||||
resolution: {integrity: sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -366,6 +396,33 @@ packages:
|
||||
resolution: {integrity: sha512-dT7FOLUCdZmq+AunLqB1Iz+ZH/IIS1Fz2THmKZQ6aFONrQD/BQ5ecJ7g2wGS2OgyUFf4OaLam6/bxmgdOBDqig==}
|
||||
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:
|
||||
resolution: {integrity: sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==}
|
||||
dev: true
|
||||
@@ -479,6 +536,13 @@ packages:
|
||||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||
dev: true
|
||||
@@ -498,7 +562,6 @@ packages:
|
||||
resolution: {integrity: sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.21
|
||||
dev: true
|
||||
|
||||
/@types/react-reconciler@0.28.4:
|
||||
resolution: {integrity: sha512-Xd55E2aLI9Q/ikDQEmfRzIwYJs4oO0h9ZHA3FZDakzt1WR6JMZcpqtCZlF97I72KVjoY4rHXU5TfvkRDOyr/rg==}
|
||||
@@ -520,6 +583,10 @@ packages:
|
||||
resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==}
|
||||
dev: true
|
||||
|
||||
/@types/use-sync-external-store@0.0.3:
|
||||
resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
|
||||
dev: false
|
||||
|
||||
/@types/uuid@9.0.3:
|
||||
resolution: {integrity: sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==}
|
||||
dev: false
|
||||
@@ -1677,11 +1744,21 @@ packages:
|
||||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
|
||||
engines: {node: '>= 4'}
|
||||
dev: true
|
||||
|
||||
/immer@9.0.21:
|
||||
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||
dev: false
|
||||
|
||||
/import-fresh@3.3.0:
|
||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2005,6 +2082,14 @@ packages:
|
||||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -2484,9 +2569,20 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
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:
|
||||
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):
|
||||
resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==}
|
||||
@@ -2515,6 +2611,40 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
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):
|
||||
resolution: {integrity: sha512-yFNHrlVEReVYKsLI5lF05tZoHveA5pGzjFbFJY/3pOqqjGOmMmqx83N4hIjN2n6E1AOa+eQEUxs3CgRnPmT0RQ==}
|
||||
peerDependencies:
|
||||
@@ -2541,6 +2671,20 @@ packages:
|
||||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2565,6 +2709,10 @@ packages:
|
||||
functions-have-names: 1.2.3
|
||||
dev: true
|
||||
|
||||
/reselect@4.1.8:
|
||||
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==}
|
||||
dev: false
|
||||
|
||||
/resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
@@ -4,17 +4,31 @@ import {
|
||||
} from "@/components/transformable-image";
|
||||
|
||||
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 { v1 } from "uuid";
|
||||
import TransformableText, {
|
||||
TransformableTextProps,
|
||||
} 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 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 [selectedTextId, selectText] = useState<string | null>(null);
|
||||
const [inputText, setInputText] = useState("");
|
||||
|
||||
const [images, setImages] = useState<TransformableImageProps["imageProps"][]>(
|
||||
[],
|
||||
@@ -22,47 +36,25 @@ const Canvas = () => {
|
||||
|
||||
const [texts, setTexts] = useState<TransformableTextProps["textProps"][]>([]);
|
||||
|
||||
const checkDeselect = (e: KonvaEventObject<MouseEvent>) => {
|
||||
// deselect when clicked on empty area
|
||||
const clickedOnEmpty = e.target === e.target.getStage();
|
||||
if (clickedOnEmpty) {
|
||||
selectImage(null);
|
||||
}
|
||||
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
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 (
|
||||
<main>
|
||||
<input type="file" onChange={handleImageUploaded} />
|
||||
<input
|
||||
type="text"
|
||||
onChange={handleTextAdd}
|
||||
placeholder="tape your text"
|
||||
/>
|
||||
<main className="flex">
|
||||
|
||||
<Stage width={window.innerWidth} height={window.innerHeight}>
|
||||
<Layer>
|
||||
{images.map((image) => {
|
||||
return (
|
||||
<TransformableImage
|
||||
onSelect={() => selectImage(image.imageId)}
|
||||
isSelected={image.imageId === selectedImageId}
|
||||
onSelect={() => selectItem(image.imageId)}
|
||||
isSelected={image.imageId === selectedItemId}
|
||||
onChange={(newAttrs) => {
|
||||
setImages(
|
||||
images.map((i) =>
|
||||
@@ -78,8 +70,8 @@ const Canvas = () => {
|
||||
{texts.map((text) => {
|
||||
return (
|
||||
<TransformableText
|
||||
onSelect={() => selectText(text.textId)}
|
||||
isSelected={text.textId === selectedTextId}
|
||||
onSelect={() => selectItem(text.textId)}
|
||||
isSelected={text.textId === selectedItemId}
|
||||
onChange={(newAttrs) => {
|
||||
setTexts(
|
||||
texts.map((t) => (t.textId === text.textId ? newAttrs : t)),
|
||||
|
||||
15
src/components/layout/layout.tsx
Normal file
15
src/components/layout/layout.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
37
src/components/layout/sidebar.tsx
Normal file
37
src/components/layout/sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
56
src/components/ui/button.tsx
Normal file
56
src/components/ui/button.tsx
Normal 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 }
|
||||
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal 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
2
src/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./use-app-dispatch"
|
||||
export * from "./use-app-selector"
|
||||
4
src/hooks/use-app-dispatch.ts
Normal file
4
src/hooks/use-app-dispatch.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { AppDispatch } from "@/store/store";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
4
src/hooks/use-app-selector.ts
Normal file
4
src/hooks/use-app-selector.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { RootState } from "@/store/store";
|
||||
import { TypedUseSelectorHook, useSelector } from "react-redux";
|
||||
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
@@ -5,6 +5,7 @@ import { type AppType } from "next/app";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
import "@/styles/globals.css";
|
||||
import { Layout } from "@/components/layout/layout";
|
||||
|
||||
const MyApp: AppType<{ session: Session | null }> = ({
|
||||
Component,
|
||||
@@ -12,7 +13,9 @@ const MyApp: AppType<{ session: Session | null }> = ({
|
||||
}) => {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
|
||||
const Canvas = dynamic(() => import("../components/canvas"), { ssr: false });
|
||||
|
||||
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
47
src/store/app.slice.ts
Normal 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
11
src/store/store.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user