mirror of
https://github.com/r2r90/canvas-label.git
synced 2026-01-28 21:22:21 +00:00
work in progress
This commit is contained in:
@@ -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}>
|
||||
<Component {...pageProps} />
|
||||
<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