mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-17 05:29:27 +00:00
text toolbar - delete text button added
This commit is contained in:
@@ -24,8 +24,10 @@
|
|||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slider": "^1.1.2",
|
"@radix-ui/react-slider": "^1.1.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||||
|
"@radix-ui/react-toolbar": "^1.0.4",
|
||||||
"@radix-ui/themes": "^2.0.0",
|
"@radix-ui/themes": "^2.0.0",
|
||||||
"@reduxjs/toolkit": "^1.9.6",
|
"@reduxjs/toolkit": "^1.9.6",
|
||||||
"@remotion/google-fonts": "^4.0.51",
|
"@remotion/google-fonts": "^4.0.51",
|
||||||
@@ -49,6 +51,7 @@
|
|||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-icons": "^4.11.0",
|
"react-icons": "^4.11.0",
|
||||||
"react-konva": "^18.2.10",
|
"react-konva": "^18.2.10",
|
||||||
|
"react-konva-utils": "^1.0.5",
|
||||||
"react-redux": "^8.1.3",
|
"react-redux": "^8.1.3",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"tailwind-merge": "^1.14.0",
|
"tailwind-merge": "^1.14.0",
|
||||||
|
|||||||
50
pnpm-lock.yaml
generated
50
pnpm-lock.yaml
generated
@@ -44,12 +44,18 @@ dependencies:
|
|||||||
'@radix-ui/react-slider':
|
'@radix-ui/react-slider':
|
||||||
specifier: ^1.1.2
|
specifier: ^1.1.2
|
||||||
version: 1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.1.2(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-tabs':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-toggle':
|
'@radix-ui/react-toggle':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-toggle-group':
|
'@radix-ui/react-toggle-group':
|
||||||
specifier: ^1.0.4
|
specifier: ^1.0.4
|
||||||
version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-toolbar':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/themes':
|
'@radix-ui/themes':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
version: 2.0.0(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -119,6 +125,9 @@ dependencies:
|
|||||||
react-konva:
|
react-konva:
|
||||||
specifier: ^18.2.10
|
specifier: ^18.2.10
|
||||||
version: 18.2.10(konva@9.2.2)(react-dom@18.2.0)(react@18.2.0)
|
version: 18.2.10(konva@9.2.2)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-konva-utils:
|
||||||
|
specifier: ^1.0.5
|
||||||
|
version: 1.0.5(konva@9.2.2)(react-dom@18.2.0)(react@18.2.0)
|
||||||
react-redux:
|
react-redux:
|
||||||
specifier: ^8.1.3
|
specifier: ^8.1.3
|
||||||
version: 8.1.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
version: 8.1.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1)
|
||||||
@@ -1558,6 +1567,33 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-toolbar@1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.2
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.31)(react@18.2.0)
|
||||||
|
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.31)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-separator': 1.0.3(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.31
|
||||||
|
'@types/react-dom': 18.2.14
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.2.14)(@types/react@18.2.31)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
|
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4126,6 +4162,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-konva-utils@1.0.5(konva@9.2.2)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-MQco0bre5ohm2lS34wAr/QJgT5PCnKbS3V1/aeYDldc8mq5X1UwcjxZWSL7YxGw3jQSHOm6XyX0YgLXQYUWBuQ==}
|
||||||
|
peerDependencies:
|
||||||
|
konva: ^8.3.5 || ^9.0.0
|
||||||
|
react: ^18.2.0
|
||||||
|
react-dom: ^18.2.0
|
||||||
|
dependencies:
|
||||||
|
konva: 9.2.2
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-konva: 18.2.10(konva@9.2.2)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
use-image: 1.1.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-konva@18.2.10(konva@9.2.2)(react-dom@18.2.0)(react@18.2.0):
|
/react-konva@18.2.10(konva@9.2.2)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==}
|
resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 736 KiB |
56
src/components/EditableText.tsx
Normal file
56
src/components/EditableText.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { ComponentPropsWithoutRef, CSSProperties } from "react";
|
||||||
|
import { Html } from "react-konva-utils";
|
||||||
|
|
||||||
|
function getStyle(
|
||||||
|
width: CSSProperties["width"],
|
||||||
|
height: CSSProperties["height"],
|
||||||
|
) {
|
||||||
|
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
|
||||||
|
const baseStyle: CSSProperties = {
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
border: "none",
|
||||||
|
padding: "0px",
|
||||||
|
margin: "0px",
|
||||||
|
background: "none",
|
||||||
|
outline: "none",
|
||||||
|
resize: "none",
|
||||||
|
color: "black",
|
||||||
|
fontSize: "24px",
|
||||||
|
fontFamily: "sans-serif",
|
||||||
|
};
|
||||||
|
if (isFirefox) {
|
||||||
|
return baseStyle;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...baseStyle,
|
||||||
|
marginTop: "-4px",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditableTextInput({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
}: {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: CSSProperties["width"];
|
||||||
|
height: CSSProperties["height"];
|
||||||
|
} & ComponentPropsWithoutRef<"textarea">) {
|
||||||
|
const style = getStyle(width, height);
|
||||||
|
return (
|
||||||
|
<Html groupProps={{ x, y }} divProps={{ style: { opacity: 1 } }}>
|
||||||
|
<textarea
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,11 @@ import { Layer, Stage } from "react-konva";
|
|||||||
import TransformableText from "./transformable-text";
|
import TransformableText from "./transformable-text";
|
||||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||||
import { appSlice, deselectItem } from "@/store/app.slice";
|
import { appSlice, deselectItem } from "@/store/app.slice";
|
||||||
import { Toolbar } from "./toolbar";
|
import { TextToolbar } from "./text-toolbar";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { EditableText } from "@/components/editable-resizable-text";
|
||||||
|
import { ImageToolbar } from "@/components/image-toolbar";
|
||||||
|
import { Toolbar } from "@/components/toolbar";
|
||||||
|
|
||||||
const Canvas = () => {
|
const Canvas = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -26,9 +30,35 @@ const Canvas = () => {
|
|||||||
dispatch(deselectItem());
|
dispatch(deselectItem());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [isTransforming, setIsTransforming] = useState(false);
|
||||||
|
const [text, setText] = useState("Click to resize. Double click to edit.");
|
||||||
|
const [width, setWidth] = useState(200);
|
||||||
|
const [height, setHeight] = useState(200);
|
||||||
|
const [selected, setSelected] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selected && isEditing) {
|
||||||
|
setIsEditing(false);
|
||||||
|
} else if (!selected && isTransforming) {
|
||||||
|
setIsTransforming(false);
|
||||||
|
}
|
||||||
|
}, [selected, isEditing, isTransforming]);
|
||||||
|
|
||||||
|
function toggleEdit() {
|
||||||
|
setIsEditing(!isEditing);
|
||||||
|
setSelected(!isEditing);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTransforming() {
|
||||||
|
setIsTransforming(!isTransforming);
|
||||||
|
setSelected(!isTransforming);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col items-center">
|
<div className="flex h-screen w-full flex-col items-center">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
|
|
||||||
<Stage
|
<Stage
|
||||||
className="m-[3rem] bg-white"
|
className="m-[3rem] bg-white"
|
||||||
width={600}
|
width={600}
|
||||||
@@ -63,6 +93,23 @@ const Canvas = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/*<EditableText*/}
|
||||||
|
{/* x={20}*/}
|
||||||
|
{/* y={40}*/}
|
||||||
|
{/* text={text}*/}
|
||||||
|
{/* width={width}*/}
|
||||||
|
{/* height={height}*/}
|
||||||
|
{/* onResize={(newWidth, newHeight) => {*/}
|
||||||
|
{/* setWidth(newWidth);*/}
|
||||||
|
{/* setHeight(newHeight);*/}
|
||||||
|
{/* }}*/}
|
||||||
|
{/* isEditing={isEditing}*/}
|
||||||
|
{/* isTransforming={isTransforming}*/}
|
||||||
|
{/* onToggleEdit={toggleEdit}*/}
|
||||||
|
{/* onToggleTransform={toggleTransforming}*/}
|
||||||
|
{/* onChange={setText}*/}
|
||||||
|
{/*/>*/}
|
||||||
</Layer>
|
</Layer>
|
||||||
</Stage>
|
</Stage>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
src/components/editable-resizable-text.tsx
Normal file
58
src/components/editable-resizable-text.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { EditableTextInput } from "./EditableText";
|
||||||
|
import TransformableText from "@/components/transformable-text";
|
||||||
|
import { ResizableText } from "@/components/text";
|
||||||
|
|
||||||
|
const RETURN_KEY = 13;
|
||||||
|
const ESCAPE_KEY = 27;
|
||||||
|
|
||||||
|
export function EditableText({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
isEditing,
|
||||||
|
isTransforming,
|
||||||
|
onToggleEdit,
|
||||||
|
onToggleTransform,
|
||||||
|
onChange,
|
||||||
|
onResize,
|
||||||
|
text,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: any) {
|
||||||
|
function handleEscapeKeys(e: any) {
|
||||||
|
if ((e.keyCode === RETURN_KEY && !e.shiftKey) || e.keyCode === ESCAPE_KEY) {
|
||||||
|
onToggleEdit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTextChange(e: any) {
|
||||||
|
onChange(e.currentTarget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<EditableTextInput
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
value={text}
|
||||||
|
onChange={handleTextChange}
|
||||||
|
onKeyDown={handleEscapeKeys}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ResizableText
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
isSelected={isTransforming}
|
||||||
|
onClick={onToggleTransform}
|
||||||
|
onDoubleClick={onToggleEdit}
|
||||||
|
onResize={onResize}
|
||||||
|
text={text}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/components/image-toolbar.tsx
Normal file
5
src/components/image-toolbar.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function ImageToolbar() {
|
||||||
|
return <div>Image toolbar</div>;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
import { Navbar } from "./navbar";
|
import { Navbar } from "./navbar";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { UserNav } from "./user-nav";
|
import { UserNav } from "./user-nav";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import logo from "@/assets/logo.png"
|
import logo from "@/assets/logo.png";
|
||||||
|
|
||||||
export const Navbar = () => {
|
export const Navbar = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTextAdd = () => dispatch(addText({ initialValue: inputText }));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex h-full max-w-[20rem] 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
44
src/components/layout/sidebar/image-input.tsx
Normal file
44
src/components/layout/sidebar/image-input.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import React, { ChangeEvent } from "react";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { PiImageDuotone } from "react-icons/pi";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { addImage } from "@/store/app.slice";
|
||||||
|
import { useAppDispatch } from "@/hooks";
|
||||||
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
function ImageInput() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleImageUploaded = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(addImage(e));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="secondary" className="text-xl">
|
||||||
|
<PiImageDuotone />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="right">
|
||||||
|
{/*<Input type="file" onChange={handleImageUploaded} />*/}
|
||||||
|
|
||||||
|
<Card className="p-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-center">Ajouter votre image</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<Input type="file" onChange={handleImageUploaded} />
|
||||||
|
</Card>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ImageInput;
|
||||||
34
src/components/layout/sidebar/select-template.tsx
Normal file
34
src/components/layout/sidebar/select-template.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { ChangeEvent } from "react";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { PiFrameCornersDuotone, PiImageDuotone } from "react-icons/pi";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useAppDispatch } from "@/hooks";
|
||||||
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export function SelectTemplate() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="secondary" className="text-xl">
|
||||||
|
<PiFrameCornersDuotone />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="right">
|
||||||
|
{/*<Input type="file" onChange={handleImageUploaded} />*/}
|
||||||
|
|
||||||
|
<Card className="p-2">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-center">Choisissez un cadre</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
src/components/layout/sidebar/sidebar.tsx
Normal file
15
src/components/layout/sidebar/sidebar.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { TextInput } from "@/components/layout/sidebar/text-input";
|
||||||
|
import ImageInput from "@/components/layout/sidebar/image-input";
|
||||||
|
import { SelectTemplate } from "@/components/layout/sidebar/select-template";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useAppDispatch } from "@/hooks";
|
||||||
|
|
||||||
|
export function Sidebar() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-20 flex-col gap-2 p-2">
|
||||||
|
<TextInput />
|
||||||
|
<ImageInput />
|
||||||
|
<SelectTemplate />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/components/layout/sidebar/text-input.tsx
Normal file
60
src/components/layout/sidebar/text-input.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { type ChangeEvent, useState } from "react";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { addText } from "@/store/app.slice";
|
||||||
|
import { useAppDispatch } from "@/hooks";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { PiTextT } from "react-icons/pi";
|
||||||
|
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
export function TextInput() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [inputText, setInputText] = useState("");
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const handleTextAdd = () => {
|
||||||
|
dispatch(addText({ initialValue: inputText }));
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
setInputText(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
variant="secondary"
|
||||||
|
className="text-xl"
|
||||||
|
>
|
||||||
|
<PiTextT />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="right" className="mt-4">
|
||||||
|
<Card className="p-3">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Ajouter votre text</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
placeholder="Entrez votre text ..."
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CardFooter className="mt-8 justify-between">
|
||||||
|
<Button variant="ghost" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleTextAdd}>Submit</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
src/components/text-toolbar.tsx
Normal file
61
src/components/text-toolbar.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { FontFamilyPicker } from "./texte-editing-tools/text-family-picker";
|
||||||
|
import { FontSizeSelector } from "./texte-editing-tools/text-size-selector";
|
||||||
|
import type { ChangeEvent } from "react";
|
||||||
|
import { FontStyle } from "./texte-editing-tools/text-style-selector";
|
||||||
|
import { FontAlign } from "./texte-editing-tools/text-align-selector";
|
||||||
|
import { Separator } from "./ui/separator";
|
||||||
|
import { SpacingSettings } from "./texte-editing-tools/text-spacing-settings";
|
||||||
|
import { type TransformableTextProps } from "@/components/transformable-text";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useAppDispatch } from "@/hooks";
|
||||||
|
import { deleteShape } from "@/store/app.slice";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedItemId: string;
|
||||||
|
currentText: TransformableTextProps["textProps"];
|
||||||
|
onTextColorChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
|
};
|
||||||
|
export const TextToolbar = ({
|
||||||
|
currentText,
|
||||||
|
selectedItemId,
|
||||||
|
onTextColorChange,
|
||||||
|
}: Props) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
if (!currentText) return null;
|
||||||
|
const deleteTextHandler = (selectedItemId) => {
|
||||||
|
dispatch(deleteShape(selectedItemId));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FontFamilyPicker
|
||||||
|
currentText={currentText}
|
||||||
|
selectedItemId={selectedItemId}
|
||||||
|
/>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<FontStyle currentText={currentText} selectedItemId={selectedItemId} />
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<FontAlign currentText={currentText} selectedItemId={selectedItemId} />
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<FontSizeSelector
|
||||||
|
currentText={currentText}
|
||||||
|
selectedItemId={selectedItemId}
|
||||||
|
/>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<input
|
||||||
|
className="my-auto "
|
||||||
|
type="color"
|
||||||
|
onChange={onTextColorChange}
|
||||||
|
value={currentText.fill}
|
||||||
|
/>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<SpacingSettings
|
||||||
|
currentText={currentText}
|
||||||
|
selectedItemId={selectedItemId}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => deleteTextHandler(selectedItemId)}>
|
||||||
|
Delete Text
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
71
src/components/text.tsx
Normal file
71
src/components/text.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import React, { useRef, useEffect } from "react";
|
||||||
|
import { Text, Transformer } from "react-konva";
|
||||||
|
|
||||||
|
export function ResizableText({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
text,
|
||||||
|
isSelected,
|
||||||
|
width,
|
||||||
|
onResize,
|
||||||
|
onClick,
|
||||||
|
onDoubleClick,
|
||||||
|
}) {
|
||||||
|
const textRef = useRef(null);
|
||||||
|
const transformerRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSelected && transformerRef.current !== null) {
|
||||||
|
transformerRef.current.nodes([textRef.current]);
|
||||||
|
transformerRef.current.getLayer().batchDraw();
|
||||||
|
}
|
||||||
|
}, [isSelected]);
|
||||||
|
|
||||||
|
function handleResize() {
|
||||||
|
if (textRef.current !== null) {
|
||||||
|
const textNode = textRef.current;
|
||||||
|
const newWidth = textNode.width() * textNode.scaleX();
|
||||||
|
const newHeight = textNode.height() * textNode.scaleY();
|
||||||
|
textNode.setAttrs({
|
||||||
|
width: newWidth,
|
||||||
|
scaleX: 1,
|
||||||
|
});
|
||||||
|
onResize(newWidth, newHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const transformer = isSelected ? (
|
||||||
|
<Transformer
|
||||||
|
ref={transformerRef}
|
||||||
|
rotateEnabled={false}
|
||||||
|
flipEnabled={false}
|
||||||
|
enabledAnchors={["middle-left", "middle-right"]}
|
||||||
|
boundBoxFunc={(oldBox, newBox) => {
|
||||||
|
newBox.width = Math.max(30, newBox.width);
|
||||||
|
return newBox;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text
|
||||||
|
x={x}
|
||||||
|
y={y}
|
||||||
|
ref={textRef}
|
||||||
|
text={text}
|
||||||
|
fill="black"
|
||||||
|
fontFamily="sans-serif"
|
||||||
|
fontSize={24}
|
||||||
|
perfectDrawEnabled={false}
|
||||||
|
onTransform={handleResize}
|
||||||
|
onClick={onClick}
|
||||||
|
onTap={onClick}
|
||||||
|
onDblClick={onDoubleClick}
|
||||||
|
onDblTap={onDoubleClick}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
{transformer}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/components/texte-editing-tools/text-edit-toolbar.tsx
Normal file
5
src/components/texte-editing-tools/text-edit-toolbar.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function TextEditToolbar() {
|
||||||
|
return <>Text Edit toolbar</>;
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ export function FontStyle({ currentText, selectedItemId }: Props) {
|
|||||||
const handleTextDecorationToggle =
|
const handleTextDecorationToggle =
|
||||||
(button: "underline" | "line-through") => () => {
|
(button: "underline" | "line-through") => () => {
|
||||||
if (!selectedItemId) return;
|
if (!selectedItemId) return;
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateText({
|
updateText({
|
||||||
id: selectedItemId,
|
id: selectedItemId,
|
||||||
@@ -47,8 +46,7 @@ export function FontStyle({ currentText, selectedItemId }: Props) {
|
|||||||
onClick={handleFontStyleToggle("bold")}
|
onClick={handleFontStyleToggle("bold")}
|
||||||
value={currentText.fontStyle}
|
value={currentText.fontStyle}
|
||||||
>
|
>
|
||||||
<FontBoldIcon className="mr-2 h-4 w-4" />
|
<FontBoldIcon className="h-4 w-4" />
|
||||||
Bold
|
|
||||||
</Toggle>
|
</Toggle>
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -56,30 +54,22 @@ export function FontStyle({ currentText, selectedItemId }: Props) {
|
|||||||
onClick={handleFontStyleToggle("italic")}
|
onClick={handleFontStyleToggle("italic")}
|
||||||
value={currentText.fontStyle}
|
value={currentText.fontStyle}
|
||||||
>
|
>
|
||||||
<FontItalicIcon className="mr-2 h-4 w-4" />
|
<FontItalicIcon className="h-4 w-4" />
|
||||||
Italic
|
|
||||||
</Toggle>
|
</Toggle>
|
||||||
|
|
||||||
<Separator
|
|
||||||
orientation="vertical"
|
|
||||||
className="mx-2 my-auto h-[2rem] items-center "
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Toggle
|
<Toggle
|
||||||
aria-label="Toggle italic"
|
aria-label="Toggle italic"
|
||||||
onClick={handleTextDecorationToggle("underline")}
|
onClick={handleTextDecorationToggle("underline")}
|
||||||
value={currentText.textDecoration}
|
value={currentText.textDecoration}
|
||||||
>
|
>
|
||||||
<UnderlineIcon className="mr-2 h-4 w-4" />
|
<UnderlineIcon className="h-4 w-4" />
|
||||||
Souligné
|
|
||||||
</Toggle>
|
</Toggle>
|
||||||
<Toggle
|
<Toggle
|
||||||
aria-label="Toggle italic"
|
aria-label="Toggle italic"
|
||||||
onClick={handleTextDecorationToggle("line-through")}
|
onClick={handleTextDecorationToggle("line-through")}
|
||||||
value={currentText.textDecoration}
|
value={currentText.textDecoration}
|
||||||
>
|
>
|
||||||
<StrikethroughIcon className="mr-2 h-4 w-4" />
|
<StrikethroughIcon className="h-4 w-4" />
|
||||||
Barré
|
|
||||||
</Toggle>
|
</Toggle>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,31 +1,19 @@
|
|||||||
|
import React, { type ChangeEvent } from "react";
|
||||||
|
import { ImageToolbar } from "@/components/image-toolbar";
|
||||||
|
import { TextToolbar } from "@/components/text-toolbar";
|
||||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||||
import { updateText } from "@/store/app.slice";
|
import { updateText } from "@/store/app.slice";
|
||||||
import { FontFamilyPicker } from "./texte-editing-tools/text-family-picker";
|
|
||||||
import { FontSizeSelector } from "./texte-editing-tools/text-size-selector";
|
|
||||||
import type { ChangeEvent } from "react";
|
|
||||||
import { FontStyle } from "./texte-editing-tools/text-style-selector";
|
|
||||||
import { FontAlign } from "./texte-editing-tools/text-align-selector";
|
|
||||||
import { Separator } from "./ui/separator";
|
|
||||||
import { SpacingSettings } from "./texte-editing-tools/text-spacing-settings";
|
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
||||||
import {
|
|
||||||
HoverCard,
|
|
||||||
HoverCardContent,
|
|
||||||
HoverCardTrigger,
|
|
||||||
} from "@/components/ui/hover-card";
|
|
||||||
import {
|
|
||||||
AiOutlineAlignCenter,
|
|
||||||
AiOutlineAlignLeft,
|
|
||||||
AiOutlineAlignRight,
|
|
||||||
} from "react-icons/ai";
|
|
||||||
|
|
||||||
export const Toolbar = () => {
|
export const Toolbar = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
||||||
const texts = useAppSelector((state) => state.app.texts);
|
const texts = useAppSelector((state) => state.app.texts);
|
||||||
|
const images = useAppSelector((state) => state.app.images);
|
||||||
|
|
||||||
const currentText = texts.find((t) => t.id === selectedItemId);
|
const currentText = texts.find((t) => t.id === selectedItemId);
|
||||||
if (!currentText) return null;
|
const currentImage = images.find((img) => img.id === selectedItemId);
|
||||||
|
|
||||||
|
if (!selectedItemId) return null;
|
||||||
|
|
||||||
const handleTextColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleTextColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!selectedItemId) return;
|
if (!selectedItemId) return;
|
||||||
@@ -38,32 +26,15 @@ export const Toolbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[5rem] w-full gap-6 border bg-white p-[1rem]">
|
<div className=" flex h-[5rem] w-full gap-6 border border-t-0 bg-white p-[1rem]">
|
||||||
<FontFamilyPicker
|
{currentImage && <ImageToolbar />}
|
||||||
currentText={currentText}
|
{currentText && (
|
||||||
selectedItemId={selectedItemId}
|
<TextToolbar
|
||||||
/>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<FontStyle currentText={currentText} selectedItemId={selectedItemId} />
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<FontAlign currentText={currentText} selectedItemId={selectedItemId} />
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<FontSizeSelector
|
|
||||||
currentText={currentText}
|
|
||||||
selectedItemId={selectedItemId}
|
|
||||||
/>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<input
|
|
||||||
className="my-auto "
|
|
||||||
type="color"
|
|
||||||
onChange={handleTextColorChange}
|
|
||||||
value={currentText.fill}
|
|
||||||
/>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<SpacingSettings
|
|
||||||
currentText={currentText}
|
currentText={currentText}
|
||||||
selectedItemId={selectedItemId}
|
selectedItemId={selectedItemId}
|
||||||
|
onTextColorChange={handleTextColorChange}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||||
import { useRef, type ElementRef, useEffect } from "react";
|
import { useRef, type ElementRef, useEffect } from "react";
|
||||||
import { Text, Transformer } from "react-konva";
|
import { Text, Transformer } from "react-konva";
|
||||||
|
import * as console from "console";
|
||||||
|
|
||||||
type TransformableTextConfig = Omit<TextConfig, "text"> & {
|
type TransformableTextConfig = Omit<TextConfig, "text"> & {
|
||||||
text?: TextConfig["text"];
|
text?: TextConfig["text"];
|
||||||
|
|||||||
79
src/components/ui/card.tsx
Normal file
79
src/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
24
src/components/ui/textarea.tsx
Normal file
24
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export interface TextareaProps
|
||||||
|
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background 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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -8,6 +8,7 @@ export default function Home() {
|
|||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Labbel Application</title>
|
<title>Labbel Application</title>
|
||||||
|
<link rel="icon" href="/logo.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Canvas />
|
<Canvas />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const appSlice = createSlice({
|
|||||||
|
|
||||||
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
|
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
|
||||||
const textId = v1();
|
const textId = v1();
|
||||||
|
console.log(state);
|
||||||
state.texts.push({
|
state.texts.push({
|
||||||
text: action.payload.initialValue,
|
text: action.payload.initialValue,
|
||||||
id: textId,
|
id: textId,
|
||||||
@@ -68,8 +68,22 @@ export const appSlice = createSlice({
|
|||||||
...action.payload,
|
...action.payload,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteShape: (state, action: PayloadAction<string>) => {
|
||||||
|
console.log(state.texts);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
texts: state.texts.filter((shape) => shape.id !== action.payload),
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { addImage, addText, selectItem, deselectItem, updateText } =
|
export const {
|
||||||
appSlice.actions;
|
addImage,
|
||||||
|
addText,
|
||||||
|
selectItem,
|
||||||
|
deselectItem,
|
||||||
|
updateText,
|
||||||
|
deleteShape,
|
||||||
|
} = appSlice.actions;
|
||||||
|
|||||||
10
todo.md
10
todo.md
@@ -1,7 +1,3 @@
|
|||||||
Property 'PopoverTriggerProps' is missing in type '{ handleTextFontFamilyChange: (e: string | undefined) => void; }' but required in type 'Props'.ts(2741)
|
Change Text
|
||||||
font-family-picker.tsx(31, 3): 'PopoverTriggerProps' is declared here.
|
Delete Text
|
||||||
(alias) function FontFamilyPicker({ handleTextFontFamilyChange }: Props): React.JSX.Element
|
TextToolbar - text ? text : image
|
||||||
import FontFamilyPicker
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user