mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-16 21:19:38 +00:00
toolbar added
This commit is contained in:
@@ -14,77 +14,63 @@ import TransformableText, {
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||
import { appSlice } from "@/store/app.slice";
|
||||
import { appSlice, checkDeselect } from "@/store/app.slice";
|
||||
import { Toolbar } from "./toolbar";
|
||||
|
||||
// Provider *
|
||||
// Provider *
|
||||
|
||||
const Canvas = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
||||
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id));
|
||||
const texts = useAppSelector((state) => state.app.texts);
|
||||
const images = useAppSelector((state) => state.app.images);
|
||||
|
||||
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"][]>(
|
||||
[],
|
||||
);
|
||||
|
||||
const [texts, setTexts] = useState<TransformableTextProps["textProps"][]>([]);
|
||||
|
||||
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInputText(e.target.value);
|
||||
const deselectHandler = (e) => {
|
||||
dispatch(checkDeselect(e));
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<main className="flex">
|
||||
|
||||
<Stage width={window.innerWidth} height={window.innerHeight}>
|
||||
<div className="flex h-screen w-full flex-col items-center justify-between">
|
||||
<Toolbar />
|
||||
<Stage
|
||||
className="bg-white"
|
||||
width={600}
|
||||
height={500}
|
||||
onTouchStart={deselectHandler}
|
||||
onMouseDown={deselectHandler}
|
||||
>
|
||||
<Layer>
|
||||
{images.map((image) => {
|
||||
return (
|
||||
<TransformableImage
|
||||
onSelect={() => selectItem(image.imageId)}
|
||||
isSelected={image.imageId === selectedItemId}
|
||||
onSelect={() => selectItem(image.id)}
|
||||
isSelected={image.id === selectedItemId}
|
||||
onChange={(newAttrs) => {
|
||||
setImages(
|
||||
images.map((i) =>
|
||||
i.imageId === image.imageId ? newAttrs : i,
|
||||
),
|
||||
);
|
||||
images.map((i) => (i.id === image.id ? newAttrs : i));
|
||||
}}
|
||||
imageProps={image}
|
||||
key={image.imageId}
|
||||
key={image.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{texts.map((text) => {
|
||||
return (
|
||||
<TransformableText
|
||||
onSelect={() => selectItem(text.textId)}
|
||||
isSelected={text.textId === selectedItemId}
|
||||
onSelect={() => selectItem(text.id)}
|
||||
isSelected={text.id === selectedItemId}
|
||||
onChange={(newAttrs) => {
|
||||
setTexts(
|
||||
texts.map((t) => (t.textId === text.textId ? newAttrs : t)),
|
||||
);
|
||||
texts.map((t) => (t.id === text.id ? newAttrs : t));
|
||||
}}
|
||||
textProps={text}
|
||||
key={text.textId}
|
||||
key={text.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Layer>
|
||||
</Stage>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Sidebar } from "./sidebar";
|
||||
import { Navbar } from "./navbar";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
@@ -7,9 +8,12 @@ type Props = {
|
||||
|
||||
export const Layout = ({ children }: Props) => {
|
||||
return (
|
||||
<div className="flex h-full ">
|
||||
<Sidebar {} />
|
||||
<main>{children}</main>
|
||||
</div>
|
||||
<>
|
||||
<Navbar />
|
||||
<div className="flex h-full ">
|
||||
<Sidebar />
|
||||
<main className="h-full w-full bg-slate-300">{children}</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
7
src/components/layout/navbar.tsx
Normal file
7
src/components/layout/navbar.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export const Navbar = () => {
|
||||
return (
|
||||
<div>
|
||||
Navbar
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
import { addImage, addText } from "@/store/app.slice";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { useAppDispatch } from "@/hooks";
|
||||
import { ChangeEvent, useState } from "react";
|
||||
|
||||
type Props = {
|
||||
handleImageUploaded: any;
|
||||
handleInputChange: any;
|
||||
inputText: any;
|
||||
handleTextAdd: any;
|
||||
};
|
||||
|
||||
export function Sidebar({
|
||||
handleImageUploaded,
|
||||
handleInputChange,
|
||||
handleTextAdd,
|
||||
inputText,
|
||||
}: Props) {
|
||||
export function Sidebar() {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [inputText, setInputText] = useState("");
|
||||
|
||||
const handleImageUploaded = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(addImage(e));
|
||||
};
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setInputText(e.target.value);
|
||||
};
|
||||
|
||||
console.log(inputText);
|
||||
|
||||
|
||||
const handleTextAdd = () => dispatch(addText({initialValue: inputText}));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col ">
|
||||
<div className="h-full flex flex-col ">
|
||||
<Input
|
||||
type="file"
|
||||
className="m-[2rem] w-auto "
|
||||
|
||||
51
src/components/toolbar.tsx
Normal file
51
src/components/toolbar.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||
import { updateText } from "@/store/app.slice";
|
||||
|
||||
export const Toolbar = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
||||
const texts = useAppSelector((state) => state.app.texts);
|
||||
|
||||
const currentText = texts.find((t) => t.id === selectedItemId);
|
||||
|
||||
console.log(currentText);
|
||||
|
||||
if (!currentText) return null;
|
||||
|
||||
const handleFontStyleToggle = (button: "bold" | "italic") => () => {
|
||||
dispatch(
|
||||
updateText({
|
||||
...currentText,
|
||||
fontStyle: getFontStyle(
|
||||
currentText.fontStyle as string | undefined,
|
||||
button,
|
||||
),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-6">
|
||||
<button onClick={handleFontStyleToggle("italic")}>Italic</button>
|
||||
<button onClick={handleFontStyleToggle("bold")}>Bold</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFontStyle = (
|
||||
currentStyle: string | undefined,
|
||||
buttonClicked: "bold" | "italic",
|
||||
) => {
|
||||
if (buttonClicked === "bold") {
|
||||
if (currentStyle?.includes("bold")) {
|
||||
return currentStyle?.replace("bold", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " bold").trim();
|
||||
}
|
||||
if (buttonClicked === "italic") {
|
||||
if (currentStyle?.includes("italic")) {
|
||||
return currentStyle?.replace("italic", "").trim();
|
||||
}
|
||||
return ((currentStyle ?? "") + " italic").trim();
|
||||
}
|
||||
};
|
||||
@@ -6,17 +6,21 @@ import { api } from "@/utils/api";
|
||||
|
||||
import "@/styles/globals.css";
|
||||
import { Layout } from "@/components/layout/layout";
|
||||
import { Provider } from "react-redux";
|
||||
import { store } from "@/store/store";
|
||||
|
||||
const MyApp: AppType<{ session: Session | null }> = ({
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}) => {
|
||||
return (
|
||||
<SessionProvider session={session}>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
<Provider store={store}>
|
||||
<SessionProvider session={session}>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
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 type { TransformableImageProps } from "@/components/transformable-image";
|
||||
import type { TransformableTextProps } from "@/components/transformable-text";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
import type { KonvaEventObject } from "konva/lib/Node";
|
||||
import type { ChangeEvent } from "react";
|
||||
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { v1 } from "uuid";
|
||||
import { TextConfig } from "konva/lib/shapes/Text";
|
||||
|
||||
const initialState = {
|
||||
selectedItemId: null as string | null,
|
||||
@@ -25,6 +28,7 @@ export const appSlice = createSlice({
|
||||
},
|
||||
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
|
||||
const textId = v1();
|
||||
|
||||
state.texts.push({ text: action.payload.initialValue, id: textId });
|
||||
},
|
||||
|
||||
@@ -43,5 +47,20 @@ export const appSlice = createSlice({
|
||||
state.selectedItemId = null;
|
||||
}
|
||||
},
|
||||
|
||||
updateText: (
|
||||
state,
|
||||
action: PayloadAction<Omit<TextConfig, "id"> & { id: string }>,
|
||||
) => {
|
||||
const textToUpdate = state.texts.findIndex(
|
||||
(t) => t.id === action.payload.id,
|
||||
);
|
||||
state.texts[textToUpdate] = action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { addImage, addText, selectItem, checkDeselect, updateText } =
|
||||
appSlice.actions;
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
@@ -9,63 +9,63 @@
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
@@ -73,4 +73,16 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
#__next {
|
||||
height: 100%;
|
||||
}
|
||||
Reference in New Issue
Block a user