text toolbar - delete text button added

This commit is contained in:
Artur AGH
2023-10-27 15:45:49 +02:00
parent 74accdcb14
commit 46d0d98886
25 changed files with 657 additions and 114 deletions

View File

@@ -24,8 +24,10 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@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-group": "^1.0.4",
"@radix-ui/react-toolbar": "^1.0.4",
"@radix-ui/themes": "^2.0.0",
"@reduxjs/toolkit": "^1.9.6",
"@remotion/google-fonts": "^4.0.51",
@@ -49,6 +51,7 @@
"react-dom": "18.2.0",
"react-icons": "^4.11.0",
"react-konva": "^18.2.10",
"react-konva-utils": "^1.0.5",
"react-redux": "^8.1.3",
"superjson": "^1.13.1",
"tailwind-merge": "^1.14.0",

50
pnpm-lock.yaml generated
View File

@@ -44,12 +44,18 @@ dependencies:
'@radix-ui/react-slider':
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)
'@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':
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)
'@radix-ui/react-toggle-group':
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-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':
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)
@@ -119,6 +125,9 @@ dependencies:
react-konva:
specifier: ^18.2.10
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:
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)
@@ -1558,6 +1567,33 @@ packages:
react-dom: 18.2.0(react@18.2.0)
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):
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
peerDependencies:
@@ -4126,6 +4162,20 @@ packages:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
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):
resolution: {integrity: sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==}
peerDependencies:

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

View 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>
);
}

View File

@@ -6,7 +6,11 @@ import { Layer, Stage } from "react-konva";
import TransformableText from "./transformable-text";
import { useAppDispatch, useAppSelector } from "@/hooks";
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 dispatch = useAppDispatch();
@@ -26,9 +30,35 @@ const Canvas = () => {
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 (
<div className="flex h-screen w-full flex-col items-center">
<Toolbar />
<Stage
className="m-[3rem] bg-white"
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>
</Stage>
</div>

View 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}
/>
);
}

View File

@@ -0,0 +1,5 @@
import React from "react";
export function ImageToolbar() {
return <div>Image toolbar</div>;
}

View File

@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import { Sidebar } from "./sidebar";
import { Sidebar } from "./sidebar/sidebar";
import { Navbar } from "./navbar";
type Props = {

View File

@@ -1,6 +1,6 @@
import { UserNav } from "./user-nav";
import Image from "next/image";
import logo from "@/assets/logo.png"
import logo from "@/assets/logo.png";
export const Navbar = () => {
return (

View File

@@ -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>
);
}

View 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;

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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
View 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}
</>
);
}

View File

@@ -0,0 +1,5 @@
import React from "react";
export function TextEditToolbar() {
return <>Text Edit toolbar</>;
}

View File

@@ -31,7 +31,6 @@ export function FontStyle({ currentText, selectedItemId }: Props) {
const handleTextDecorationToggle =
(button: "underline" | "line-through") => () => {
if (!selectedItemId) return;
dispatch(
updateText({
id: selectedItemId,
@@ -47,8 +46,7 @@ export function FontStyle({ currentText, selectedItemId }: Props) {
onClick={handleFontStyleToggle("bold")}
value={currentText.fontStyle}
>
<FontBoldIcon className="mr-2 h-4 w-4" />
Bold
<FontBoldIcon className="h-4 w-4" />
</Toggle>
<Toggle
@@ -56,30 +54,22 @@ export function FontStyle({ currentText, selectedItemId }: Props) {
onClick={handleFontStyleToggle("italic")}
value={currentText.fontStyle}
>
<FontItalicIcon className="mr-2 h-4 w-4" />
Italic
<FontItalicIcon className="h-4 w-4" />
</Toggle>
<Separator
orientation="vertical"
className="mx-2 my-auto h-[2rem] items-center "
/>
<Toggle
aria-label="Toggle italic"
onClick={handleTextDecorationToggle("underline")}
value={currentText.textDecoration}
>
<UnderlineIcon className="mr-2 h-4 w-4" />
Souligné
<UnderlineIcon className="h-4 w-4" />
</Toggle>
<Toggle
aria-label="Toggle italic"
onClick={handleTextDecorationToggle("line-through")}
value={currentText.textDecoration}
>
<StrikethroughIcon className="mr-2 h-4 w-4" />
Barré
<StrikethroughIcon className="h-4 w-4" />
</Toggle>
</div>
</>

View File

@@ -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 { 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 = () => {
const dispatch = useAppDispatch();
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
const texts = useAppSelector((state) => state.app.texts);
const images = useAppSelector((state) => state.app.images);
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>) => {
if (!selectedItemId) return;
@@ -38,32 +26,15 @@ export const Toolbar = () => {
};
return (
<div className="flex h-[5rem] w-full gap-6 border bg-white p-[1rem]">
<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={handleTextColorChange}
value={currentText.fill}
/>
<Separator orientation="vertical" />
<SpacingSettings
currentText={currentText}
selectedItemId={selectedItemId}
/>
<div className=" flex h-[5rem] w-full gap-6 border border-t-0 bg-white p-[1rem]">
{currentImage && <ImageToolbar />}
{currentText && (
<TextToolbar
currentText={currentText}
selectedItemId={selectedItemId}
onTextColorChange={handleTextColorChange}
/>
)}
</div>
);
};

View File

@@ -1,6 +1,7 @@
import type { TextConfig } from "konva/lib/shapes/Text";
import { useRef, type ElementRef, useEffect } from "react";
import { Text, Transformer } from "react-konva";
import * as console from "console";
type TransformableTextConfig = Omit<TextConfig, "text"> & {
text?: TextConfig["text"];

View 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 }

View 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 }

View File

@@ -8,6 +8,7 @@ export default function Home() {
<>
<Head>
<title>Labbel Application</title>
<link rel="icon" href="/logo.png" />
</Head>
<div className="flex">
<Canvas />

View File

@@ -36,7 +36,7 @@ export const appSlice = createSlice({
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
const textId = v1();
console.log(state);
state.texts.push({
text: action.payload.initialValue,
id: textId,
@@ -68,8 +68,22 @@ export const appSlice = createSlice({
...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 } =
appSlice.actions;
export const {
addImage,
addText,
selectItem,
deselectItem,
updateText,
deleteShape,
} = appSlice.actions;

10
todo.md
View File

@@ -1,7 +1,3 @@
Property 'PopoverTriggerProps' is missing in type '{ handleTextFontFamilyChange: (e: string | undefined) => void; }' but required in type 'Props'.ts(2741)
font-family-picker.tsx(31, 3): 'PopoverTriggerProps' is declared here.
(alias) function FontFamilyPicker({ handleTextFontFamilyChange }: Props): React.JSX.Element
import FontFamilyPicker
Change Text
Delete Text
TextToolbar - text ? text : image