mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-16 21:19:38 +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-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
50
pnpm-lock.yaml
generated
@@ -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
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 { 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>
|
||||
|
||||
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 { Sidebar } from "./sidebar";
|
||||
import { Sidebar } from "./sidebar/sidebar";
|
||||
import { Navbar } from "./navbar";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 =
|
||||
(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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"];
|
||||
|
||||
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>
|
||||
<title>Labbel Application</title>
|
||||
<link rel="icon" href="/logo.png" />
|
||||
</Head>
|
||||
<div className="flex">
|
||||
<Canvas />
|
||||
|
||||
@@ -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
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)
|
||||
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
|
||||
Reference in New Issue
Block a user