mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-16 21:19:38 +00:00
added css finitions, background bug corrected, added item state
This commit is contained in:
@@ -15,6 +15,10 @@ const config = {
|
|||||||
// Feel free to reconfigure them to your own preference.
|
// Feel free to reconfigure them to your own preference.
|
||||||
"@typescript-eslint/array-type": "off",
|
"@typescript-eslint/array-type": "off",
|
||||||
"@typescript-eslint/consistent-type-definitions": "off",
|
"@typescript-eslint/consistent-type-definitions": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
"warn",
|
"warn",
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ await import("./src/env.mjs");
|
|||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
typescript: {
|
||||||
|
// !! WARN !!
|
||||||
|
// Dangerously allow production builds to successfully complete even if
|
||||||
|
// your project has type errors.
|
||||||
|
// !! WARN !!
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* If you are using `appDir` then you must comment the below `i18n` config out.
|
* If you are using `appDir` then you must comment the below `i18n` config out.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "next start"
|
"start": "next start --port 3333"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"font-picker-react": "^3.5.2",
|
"font-picker-react": "^3.5.2",
|
||||||
|
"immer": "^10.0.3",
|
||||||
"konva": "^9.2.0",
|
"konva": "^9.2.0",
|
||||||
"lucide-react": "^0.279.0",
|
"lucide-react": "^0.279.0",
|
||||||
"next": "^13.4.19",
|
"next": "^13.4.19",
|
||||||
|
|||||||
7
pnpm-lock.yaml
generated
7
pnpm-lock.yaml
generated
@@ -101,6 +101,9 @@ dependencies:
|
|||||||
font-picker-react:
|
font-picker-react:
|
||||||
specifier: ^3.5.2
|
specifier: ^3.5.2
|
||||||
version: 3.5.2(@types/react@18.2.31)(react@18.2.0)
|
version: 3.5.2(@types/react@18.2.31)(react@18.2.0)
|
||||||
|
immer:
|
||||||
|
specifier: ^10.0.3
|
||||||
|
version: 10.0.3
|
||||||
konva:
|
konva:
|
||||||
specifier: ^9.2.0
|
specifier: ^9.2.0
|
||||||
version: 9.2.2
|
version: 9.2.2
|
||||||
@@ -3320,6 +3323,10 @@ packages:
|
|||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/immer@10.0.3:
|
||||||
|
resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/immer@9.0.21:
|
/immer@9.0.21:
|
||||||
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
import { TransformableImage } from "@/components/transformable-image";
|
import { TransformableImage } from "@/components/transformable-image";
|
||||||
import type { KonvaEventObject } from "konva/lib/Node";
|
import type { KonvaEventObject } from "konva/lib/Node";
|
||||||
import { Layer, Rect, Stage } from "react-konva";
|
import { Layer, Rect, Stage } from "react-konva";
|
||||||
import TransformableText from "./transformable-text";
|
|
||||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||||
import { appSlice, deselectItem } from "@/store/app.slice";
|
import {
|
||||||
import { useEffect, useState } from "react";
|
appSlice,
|
||||||
|
deselectItem,
|
||||||
|
setStageScale,
|
||||||
|
StageItemType,
|
||||||
|
updateImage,
|
||||||
|
updateText,
|
||||||
|
} from "@/store/app.slice";
|
||||||
|
import { useEffect, useState, type MouseEvent } from "react";
|
||||||
import { Toolbar } from "@/components/toolbar";
|
import { Toolbar } from "@/components/toolbar";
|
||||||
|
import type Konva from "konva";
|
||||||
|
import TransformableText from "@/components/transformable-text";
|
||||||
|
|
||||||
const Canvas = () => {
|
const Canvas = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const stage = useAppSelector((state) => state.app.stage);
|
const stage = useAppSelector((state) => state.app.stage);
|
||||||
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
||||||
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id));
|
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id));
|
||||||
const texts = useAppSelector((state) => state.app.texts);
|
const items = useAppSelector((state) => state.app.items);
|
||||||
const images = useAppSelector((state) => state.app.images);
|
|
||||||
const backgroundRects = useAppSelector((state) => state.app.backgroundRects);
|
|
||||||
|
|
||||||
|
const backgroundRect = useAppSelector((state) => state.app.background);
|
||||||
|
const backgroundId = "background";
|
||||||
const deselectHandler = (
|
const deselectHandler = (
|
||||||
e: KonvaEventObject<MouseEvent> | KonvaEventObject<TouchEvent>,
|
e: KonvaEventObject<MouseEvent> | KonvaEventObject<TouchEvent>,
|
||||||
) => {
|
) => {
|
||||||
const clickedOnEmpty = e.target === e.target.getStage();
|
const target = e.target;
|
||||||
|
const clickedOnEmpty =
|
||||||
|
target === target.getStage() ||
|
||||||
|
(typeof target === "object" && target.attrs.id === backgroundId);
|
||||||
|
|
||||||
if (!clickedOnEmpty) {
|
if (!clickedOnEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(target);
|
||||||
dispatch(deselectItem());
|
dispatch(deselectItem());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,60 +61,123 @@ const Canvas = () => {
|
|||||||
setSelected(!isTransforming);
|
setSelected(!isTransforming);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleWheel = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
||||||
|
e.evt.preventDefault();
|
||||||
|
|
||||||
|
const scaleBy = 1.02;
|
||||||
|
const targetStage = e.target.getStage()!;
|
||||||
|
const oldScale = targetStage.scaleX();
|
||||||
|
|
||||||
|
const mousePointTo = {
|
||||||
|
x:
|
||||||
|
targetStage.getPointerPosition().x / oldScale -
|
||||||
|
targetStage.x() / oldScale,
|
||||||
|
y:
|
||||||
|
targetStage.getPointerPosition().y / oldScale -
|
||||||
|
targetStage.y() / oldScale,
|
||||||
|
};
|
||||||
|
|
||||||
|
const newScale = e.evt.deltaY < 0 ? oldScale * scaleBy : oldScale / scaleBy;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setStageScale({
|
||||||
|
scale: newScale,
|
||||||
|
x:
|
||||||
|
(targetStage.getPointerPosition().x / newScale - mousePointTo.x) *
|
||||||
|
newScale,
|
||||||
|
y:
|
||||||
|
(targetStage.getPointerPosition().y / newScale - mousePointTo.y) *
|
||||||
|
newScale,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-screen w-full flex-col items-center">
|
<div className="relative flex h-full w-full flex-col items-center">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
|
<div
|
||||||
<Stage
|
className={"flex h-full w-full items-center justify-center"}
|
||||||
className="m-[3rem] bg-white"
|
onClick={(e) => {
|
||||||
width={stage.width}
|
if (e.target !== e.currentTarget) {
|
||||||
height={stage.height}
|
return;
|
||||||
onTouchStart={deselectHandler}
|
}
|
||||||
onMouseDown={deselectHandler}
|
dispatch(deselectItem());
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Layer>
|
<Stage
|
||||||
{images.map((image) => {
|
className="bg-white"
|
||||||
return (
|
width={stage.width}
|
||||||
<TransformableImage
|
height={stage.height}
|
||||||
onSelect={() => selectItem(image.id)}
|
onTouchStart={deselectHandler}
|
||||||
isSelected={image.id === selectedItemId}
|
onMouseDown={deselectHandler}
|
||||||
onChange={(newAttrs) => {
|
x={stage.x}
|
||||||
images.map((i) => (i.id === image.id ? newAttrs : i));
|
y={stage.y}
|
||||||
}}
|
scaleX={stage.scale}
|
||||||
imageProps={image}
|
scaleY={stage.scale}
|
||||||
key={image.id}
|
// onWheel={handleWheel}
|
||||||
/>
|
>
|
||||||
);
|
<Layer>
|
||||||
})}
|
{!!backgroundRect && (
|
||||||
{texts.map((text) => {
|
|
||||||
return (
|
|
||||||
<TransformableText
|
|
||||||
onSelect={() => selectItem(text.id)}
|
|
||||||
isSelected={text.id === selectedItemId}
|
|
||||||
onChange={(newAttrs) => {
|
|
||||||
texts.map((t) => (t.id === text.id ? newAttrs : t));
|
|
||||||
}}
|
|
||||||
textProps={text}
|
|
||||||
key={text.id}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{backgroundRects.map((rect, i) => {
|
|
||||||
return (
|
|
||||||
<Rect
|
<Rect
|
||||||
key={i}
|
width={stage.width}
|
||||||
width={rect.width}
|
height={stage.height}
|
||||||
height={rect.width}
|
fill={backgroundRect.fill}
|
||||||
fill={rect.fill}
|
onTouchStart={deselectHandler}
|
||||||
|
onClick={deselectHandler}
|
||||||
|
id={backgroundId}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
})}
|
{items.map((item) => {
|
||||||
</Layer>
|
if (item.type === StageItemType.Image) {
|
||||||
{/* <Layer>
|
return (
|
||||||
<Text zIndex={10} x={30} y={200} fontSize={48} text="Hello" />
|
<TransformableImage
|
||||||
|
id={item.id}
|
||||||
|
onSelect={() => selectItem(item.id)}
|
||||||
|
isSelected={item.id === selectedItemId}
|
||||||
|
onChange={(newAttrs) => {
|
||||||
|
dispatch(updateImage({ id: item.id, ...newAttrs }));
|
||||||
|
}}
|
||||||
|
imageProps={item.params}
|
||||||
|
key={item.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (item.type === StageItemType.Text) {
|
||||||
|
return (
|
||||||
|
<TransformableText
|
||||||
|
onSelect={() => selectItem(item.id)}
|
||||||
|
isSelected={item.id === selectedItemId}
|
||||||
|
onChange={(newAttrs) => {
|
||||||
|
dispatch(updateText({ id: item.id, ...newAttrs }));
|
||||||
|
}}
|
||||||
|
textProps={item.params}
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Layer>
|
||||||
|
{/* <Layer>
|
||||||
|
<Rect
|
||||||
|
x={50}
|
||||||
|
y={220}
|
||||||
|
fontSize={48}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
fill="red"
|
||||||
|
/>
|
||||||
|
<Rect
|
||||||
|
x={30}
|
||||||
|
y={200}
|
||||||
|
fontSize={48}
|
||||||
|
width={100}
|
||||||
|
height={100}
|
||||||
|
fill="yellow"
|
||||||
|
/>
|
||||||
</Layer>*/}
|
</Layer>*/}
|
||||||
</Stage>
|
</Stage>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { deleteShape } from "@/store/app.slice";
|
import { deleteStageItem } from "@/store/app.slice";
|
||||||
import { useAppDispatch } from "@/hooks";
|
import { useAppDispatch } from "@/hooks";
|
||||||
import { BsTrash3 } from "react-icons/bs";
|
import { BsTrash3 } from "react-icons/bs";
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ type Props = {
|
|||||||
export const DeleteShapeButton = ({ selectedItemId }: Props) => {
|
export const DeleteShapeButton = ({ selectedItemId }: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const deleteTextHandler = (selectedItemId: string) => {
|
const deleteTextHandler = (selectedItemId: string) => {
|
||||||
dispatch(deleteShape(selectedItemId));
|
dispatch(deleteStageItem(selectedItemId));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ import React from "react";
|
|||||||
import { TbFlipHorizontal, TbFlipVertical } from "react-icons/tb";
|
import { TbFlipHorizontal, TbFlipVertical } from "react-icons/tb";
|
||||||
import { Toggle } from "@/components/ui/toggle";
|
import { Toggle } from "@/components/ui/toggle";
|
||||||
import { useAppDispatch } from "@/hooks";
|
import { useAppDispatch } from "@/hooks";
|
||||||
import { updateImage } from "@/store/app.slice";
|
import { type StageImageItem, updateImage } from "@/store/app.slice";
|
||||||
import ImageOpacityTool from "@/components/image-editing-tools/image-opacity-tool";
|
import ImageOpacityTool from "@/components/image-editing-tools/image-opacity-tool";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
export function ImageToolbar({ selectedItemId, currentImage }) {
|
type Props = {
|
||||||
|
selectedItemId: string;
|
||||||
|
currentImage: StageImageItem["params"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ImageToolbar({ selectedItemId, currentImage }: Props) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const flipImageVerticaly = () => {
|
const flipImageVerticaly = () => {
|
||||||
const currentScale = currentImage.scaleY;
|
const currentScale = currentImage.scaleY;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Props = {
|
|||||||
|
|
||||||
export const Layout = ({ children }: Props) => {
|
export const Layout = ({ children }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-[0.5rem] border bg-background shadow">
|
<div className="h-full overflow-hidden rounded-[0.5rem] border bg-background shadow">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="flex h-full ">
|
<div className="flex h-full ">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|||||||
@@ -7,8 +7,22 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { RxStack } from "react-icons/rx";
|
import { RxStack } from "react-icons/rx";
|
||||||
|
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||||
|
import { OrderDirection, updateItemOrder } from "@/store/app.slice";
|
||||||
|
|
||||||
export const Layers = () => {
|
export const Layers = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const items = useAppSelector((state) => state.app.items);
|
||||||
|
|
||||||
|
const updateImageLayer = (id: string, direction: OrderDirection) => {
|
||||||
|
dispatch(
|
||||||
|
updateItemOrder({
|
||||||
|
id,
|
||||||
|
direction,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -22,7 +36,23 @@ export const Layers = () => {
|
|||||||
<CardTitle className="text-center">Layer</CardTitle>
|
<CardTitle className="text-center">Layer</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-2">
|
<CardContent className="grid gap-2">
|
||||||
<div className="flex h-[2rem] flex-col items-center justify-between rounded-md border-2 border-muted bg-popover"></div>
|
{items.map((item) => (
|
||||||
|
<div key={item.id} className="flex items-center gap-2 ">
|
||||||
|
{item.id}
|
||||||
|
<Button
|
||||||
|
className="h-full"
|
||||||
|
onClick={() => updateImageLayer(item.id, OrderDirection.Up)}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="h-full"
|
||||||
|
onClick={() => updateImageLayer(item.id, OrderDirection.Down)}
|
||||||
|
>
|
||||||
|
-
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Layers } from "@/components/layout/sidebar/layers";
|
|||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
return (
|
return (
|
||||||
<div className="inline-flex h-full w-20 flex-col items-center justify-center gap-2 p-2">
|
<div className="flex h-full w-20 flex-col items-center gap-2 p-2 pt-4">
|
||||||
<SizeSelect />
|
<SizeSelect />
|
||||||
<TextInput />
|
<TextInput />
|
||||||
<ImageInput />
|
<ImageInput />
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const SizeSelect = () => {
|
|||||||
<PopoverContent side="right" className="mt-4">
|
<PopoverContent side="right" className="mt-4">
|
||||||
<Card className="p-3">
|
<Card className="p-3">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sélectionnez la taille d'étiquette</CardTitle>
|
<CardTitle>{`Sélectionnez la taille d'étiquette`}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
|||||||
@@ -5,15 +5,11 @@ import { FontStyle } from "./texte-editing-tools/text-style-selector";
|
|||||||
import { FontAlign } from "./texte-editing-tools/text-align-selector";
|
import { FontAlign } from "./texte-editing-tools/text-align-selector";
|
||||||
import { Separator } from "./ui/separator";
|
import { Separator } from "./ui/separator";
|
||||||
import { SpacingSettings } from "./texte-editing-tools/text-spacing-settings";
|
import { SpacingSettings } from "./texte-editing-tools/text-spacing-settings";
|
||||||
import { type TransformableTextProps } from "@/components/transformable-text";
|
import { type StageTextItem } from "@/store/app.slice";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { useAppDispatch } from "@/hooks";
|
|
||||||
import { deleteShape } from "@/store/app.slice";
|
|
||||||
import { DeleteShapeButton } from "@/components/delete-shape-button";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedItemId: string;
|
selectedItemId: string;
|
||||||
currentText: TransformableTextProps["textProps"];
|
currentText: StageTextItem["params"];
|
||||||
onTextColorChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
onTextColorChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||||
};
|
};
|
||||||
export const TextToolbar = ({
|
export const TextToolbar = ({
|
||||||
@@ -21,7 +17,6 @@ export const TextToolbar = ({
|
|||||||
selectedItemId,
|
selectedItemId,
|
||||||
onTextColorChange,
|
onTextColorChange,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
if (!currentText) return null;
|
if (!currentText) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function FontSizeSelector({ currentText, selectedItemId }: Props) {
|
|||||||
updateText({
|
updateText({
|
||||||
id: selectedItemId,
|
id: selectedItemId,
|
||||||
fontSize: e,
|
fontSize: e,
|
||||||
|
height: e,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,20 +2,18 @@ import React, { type ChangeEvent } from "react";
|
|||||||
import { ImageToolbar } from "@/components/image-toolbar";
|
import { ImageToolbar } from "@/components/image-toolbar";
|
||||||
import { TextToolbar } from "@/components/text-toolbar";
|
import { TextToolbar } from "@/components/text-toolbar";
|
||||||
import { useAppDispatch, useAppSelector } from "@/hooks";
|
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||||
import { updateText } from "@/store/app.slice";
|
import { StageItemType, updateText } from "@/store/app.slice";
|
||||||
import { DeleteShapeButton } from "@/components/delete-shape-button";
|
import { DeleteShapeButton } from "@/components/delete-shape-button";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
||||||
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 items = useAppSelector((state) => state.app.items);
|
||||||
const images = useAppSelector((state) => state.app.images);
|
|
||||||
|
|
||||||
const currentText = texts.find((t) => t.id === selectedItemId);
|
const currentItem = items.find((t) => t.id === selectedItemId);
|
||||||
const currentImage = images.find((img) => img.id === selectedItemId);
|
|
||||||
|
|
||||||
if (!selectedItemId) return null;
|
if (!currentItem || !selectedItemId) return null;
|
||||||
|
|
||||||
const handleTextColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleTextColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
if (!selectedItemId) return;
|
if (!selectedItemId) return;
|
||||||
@@ -28,17 +26,17 @@ export const Toolbar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" flex h-[5rem] w-full gap-6 border border-t-0 bg-white p-[1rem]">
|
<div className="inset absolute flex h-[5rem] w-full gap-6 border border-t-0 bg-white p-[1rem]">
|
||||||
{currentText && (
|
{currentItem.type === StageItemType.Text && (
|
||||||
<TextToolbar
|
<TextToolbar
|
||||||
currentText={currentText}
|
currentText={currentItem.params}
|
||||||
selectedItemId={selectedItemId}
|
selectedItemId={selectedItemId}
|
||||||
onTextColorChange={handleTextColorChange}
|
onTextColorChange={handleTextColorChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{currentImage && (
|
{currentItem.type === StageItemType.Image && (
|
||||||
<ImageToolbar
|
<ImageToolbar
|
||||||
currentImage={currentImage}
|
currentImage={currentItem.params}
|
||||||
selectedItemId={selectedItemId}
|
selectedItemId={selectedItemId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { Transformer, Image } from "react-konva";
|
|||||||
import useImage from "use-image";
|
import useImage from "use-image";
|
||||||
|
|
||||||
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
|
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
|
||||||
image?: ImageConfig["image"];
|
image?: HTMLImageElement;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
id: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TransformableImageProps = {
|
export type TransformableImageProps = {
|
||||||
imageProps: TransformableImageConfig;
|
imageProps: TransformableImageConfig;
|
||||||
|
id: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onChange: (newAttrs: TransformableImageConfig) => void;
|
onChange: (newAttrs: TransformableImageConfig) => void;
|
||||||
@@ -18,6 +18,7 @@ export type TransformableImageProps = {
|
|||||||
|
|
||||||
export const TransformableImage = ({
|
export const TransformableImage = ({
|
||||||
imageProps,
|
imageProps,
|
||||||
|
id,
|
||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -26,7 +27,6 @@ export const TransformableImage = ({
|
|||||||
const trRef = useRef<ElementRef<typeof Transformer>>(null);
|
const trRef = useRef<ElementRef<typeof Transformer>>(null);
|
||||||
const [image] = useImage(imageProps.imageUrl);
|
const [image] = useImage(imageProps.imageUrl);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!imageRef.current) return;
|
if (!imageRef.current) return;
|
||||||
|
|
||||||
@@ -40,6 +40,7 @@ export const TransformableImage = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
|
id={id}
|
||||||
alt={"canvas image"}
|
alt={"canvas image"}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
onTap={onSelect}
|
onTap={onSelect}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { TextConfig } from "konva/lib/shapes/Text";
|
import type { TextConfig } from "konva/lib/shapes/Text";
|
||||||
import { useRef, type ElementRef, useEffect } from "react";
|
import { type ElementRef, useEffect, useRef } 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?: string;
|
||||||
id: string;
|
fill?: string;
|
||||||
direction?: string;
|
direction?: string;
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
@@ -26,6 +25,7 @@ export type TransformableTextProps = {
|
|||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
onChange: (newAttrs: TransformableTextConfig) => void;
|
onChange: (newAttrs: TransformableTextConfig) => void;
|
||||||
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TransformableText = ({
|
const TransformableText = ({
|
||||||
@@ -33,6 +33,7 @@ const TransformableText = ({
|
|||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
onChange,
|
onChange,
|
||||||
|
id,
|
||||||
}: TransformableTextProps) => {
|
}: TransformableTextProps) => {
|
||||||
const textRef = useRef<ElementRef<typeof Text>>(null);
|
const textRef = useRef<ElementRef<typeof Text>>(null);
|
||||||
const trRef = useRef<ElementRef<typeof Transformer>>(null);
|
const trRef = useRef<ElementRef<typeof Transformer>>(null);
|
||||||
@@ -100,6 +101,7 @@ const TransformableText = ({
|
|||||||
/>
|
/>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Transformer
|
<Transformer
|
||||||
|
id={id}
|
||||||
ref={trRef}
|
ref={trRef}
|
||||||
boundBoxFunc={(oldBox, newBox) => {
|
boundBoxFunc={(oldBox, newBox) => {
|
||||||
return { ...newBox, height: oldBox.height };
|
return { ...newBox, height: oldBox.height };
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { DialogProps } from "@radix-ui/react-dialog"
|
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||||
import { Command as CommandPrimitive } from "cmdk"
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
import { Search } from "lucide-react"
|
import { Search } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||||
|
|
||||||
const Command = React.forwardRef<
|
const Command = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive>,
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
@@ -14,14 +14,14 @@ const Command = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
Command.displayName = CommandPrimitive.displayName
|
Command.displayName = CommandPrimitive.displayName;
|
||||||
|
|
||||||
interface CommandDialogProps extends DialogProps {}
|
type CommandDialogProps = DialogProps;
|
||||||
|
|
||||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||||
return (
|
return (
|
||||||
@@ -32,8 +32,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
|||||||
</Command>
|
</Command>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
const CommandInput = React.forwardRef<
|
const CommandInput = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
@@ -45,14 +45,14 @@ const CommandInput = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||||
|
|
||||||
const CommandList = React.forwardRef<
|
const CommandList = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.List>,
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
@@ -63,9 +63,9 @@ const CommandList = React.forwardRef<
|
|||||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandList.displayName = CommandPrimitive.List.displayName
|
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||||
|
|
||||||
const CommandEmpty = React.forwardRef<
|
const CommandEmpty = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
@@ -76,9 +76,9 @@ const CommandEmpty = React.forwardRef<
|
|||||||
className="py-6 text-center text-sm"
|
className="py-6 text-center text-sm"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||||
|
|
||||||
const CommandGroup = React.forwardRef<
|
const CommandGroup = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
@@ -88,13 +88,13 @@ const CommandGroup = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||||
|
|
||||||
const CommandSeparator = React.forwardRef<
|
const CommandSeparator = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
@@ -105,8 +105,8 @@ const CommandSeparator = React.forwardRef<
|
|||||||
className={cn("-mx-1 h-px bg-border", className)}
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||||
|
|
||||||
const CommandItem = React.forwardRef<
|
const CommandItem = React.forwardRef<
|
||||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
@@ -116,13 +116,13 @@ const CommandItem = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
));
|
||||||
|
|
||||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||||
|
|
||||||
const CommandShortcut = ({
|
const CommandShortcut = ({
|
||||||
className,
|
className,
|
||||||
@@ -132,13 +132,13 @@ const CommandShortcut = ({
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
CommandShortcut.displayName = "CommandShortcut"
|
CommandShortcut.displayName = "CommandShortcut";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Command,
|
Command,
|
||||||
@@ -150,4 +150,4 @@ export {
|
|||||||
CommandItem,
|
CommandItem,
|
||||||
CommandShortcut,
|
CommandShortcut,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface InputProps
|
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, type, ...props }, ref) => {
|
({ className, type, ...props }, ref) => {
|
||||||
@@ -12,14 +11,14 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
Input.displayName = "Input"
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export { Input }
|
export { Input };
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export interface TextareaProps
|
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
|
||||||
|
|
||||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ className, ...props }, ref) => {
|
({ className, ...props }, ref) => {
|
||||||
@@ -11,14 +10,14 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
<textarea
|
<textarea
|
||||||
className={cn(
|
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",
|
"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
|
className,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
},
|
||||||
)
|
);
|
||||||
Textarea.displayName = "Textarea"
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
export { Textarea }
|
export { Textarea };
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ export default function Home() {
|
|||||||
<title>Labbel Application</title>
|
<title>Labbel Application</title>
|
||||||
<link rel="icon" href="/logo.png" />
|
<link rel="icon" href="/logo.png" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex">
|
<Canvas />
|
||||||
<Canvas />
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,43 @@ import type { PayloadAction } from "@reduxjs/toolkit";
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import { v1 } from "uuid";
|
import { v1 } from "uuid";
|
||||||
import { type RectConfig } from "konva/lib/shapes/Rect";
|
import { type RectConfig } from "konva/lib/shapes/Rect";
|
||||||
|
import { type WritableDraft } from "immer/src/types/types-external";
|
||||||
|
|
||||||
|
export enum StageItemType {
|
||||||
|
Text = "text",
|
||||||
|
Image = "image",
|
||||||
|
}
|
||||||
|
|
||||||
|
type StageItemCommon = {
|
||||||
|
id: string;
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StageTextItem = {
|
||||||
|
type: StageItemType.Text;
|
||||||
|
params: TransformableTextProps["textProps"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StageImageItem = {
|
||||||
|
type: StageItemType.Image;
|
||||||
|
params: TransformableImageProps["imageProps"];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StageItemSpecific = StageTextItem | StageImageItem;
|
||||||
|
|
||||||
|
type StageItem = StageItemCommon & StageItemSpecific;
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
stage: { width: 500, height: 500 },
|
stage: { width: 500, height: 500, scale: 1, x: 0, y: 0 },
|
||||||
|
items: [] as StageItem[],
|
||||||
selectedItemId: null as string | null,
|
selectedItemId: null as string | null,
|
||||||
images: [] as TransformableImageProps["imageProps"][],
|
background: null as RectConfig | null,
|
||||||
texts: [] as TransformableTextProps["textProps"][],
|
|
||||||
backgroundRects: [] as RectConfig[],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultTextConfig = {
|
const defaultTextConfig = {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
align: "center",
|
align: "center",
|
||||||
fontFamily: "Roboto",
|
fontFamily: "Roboto",
|
||||||
zIndex: 3,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultImageConfig = {
|
const defaultImageConfig = {
|
||||||
@@ -28,35 +51,41 @@ const defaultImageConfig = {
|
|||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
scaleX: 1,
|
scaleX: 1,
|
||||||
scaleY: 1,
|
scaleY: 1,
|
||||||
zIndex: 1,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export enum OrderDirection {
|
||||||
|
Up = "up",
|
||||||
|
Down = "down",
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddStageItemAction = PayloadAction<StageItem>;
|
||||||
|
|
||||||
export const appSlice = createSlice({
|
export const appSlice = createSlice({
|
||||||
name: "app",
|
name: "app",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
|
setStageScale: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ x: number; y: number; scale: number }>,
|
||||||
|
) => {
|
||||||
|
state.stage = {
|
||||||
|
...state.stage,
|
||||||
|
...action.payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
|
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
|
||||||
const textId = v1();
|
const textId = v1();
|
||||||
state.texts.push({
|
state.items.push({
|
||||||
text: action.payload.initialValue,
|
type: StageItemType.Text,
|
||||||
id: textId,
|
id: textId,
|
||||||
...defaultTextConfig,
|
order: state.items.length + 1,
|
||||||
|
params: {
|
||||||
|
text: action.payload.initialValue,
|
||||||
|
|
||||||
|
...defaultTextConfig,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
selectBackground: (state, action: PayloadAction<string>) => {
|
|
||||||
state.backgroundRects = [];
|
|
||||||
state.backgroundRects.push({
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
stroke: "black",
|
|
||||||
z_index: 0,
|
|
||||||
width: state.stage.width,
|
|
||||||
height: state.stage.height,
|
|
||||||
fill: action.payload,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
addImage: (
|
addImage: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
@@ -68,16 +97,102 @@ export const appSlice = createSlice({
|
|||||||
const file = action.payload;
|
const file = action.payload;
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const imageId = v1();
|
const imageId = v1();
|
||||||
|
state.items.push({
|
||||||
state.images.push({
|
type: StageItemType.Image,
|
||||||
imageUrl: action.payload.imageUrl,
|
|
||||||
id: imageId,
|
id: imageId,
|
||||||
width: action.payload.width,
|
order: state.items.length + 1,
|
||||||
height: action.payload.height,
|
params: {
|
||||||
...defaultImageConfig,
|
imageUrl: action.payload.imageUrl,
|
||||||
|
width: action.payload.width,
|
||||||
|
height: action.payload.height,
|
||||||
|
...defaultImageConfig,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateImageOrder: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ id: string; direction: OrderDirection }>,
|
||||||
|
) => {
|
||||||
|
const imageToUpdateIndex = state.items.findIndex(
|
||||||
|
(img) => img.id === action.payload.id,
|
||||||
|
);
|
||||||
|
const imageToUpdate = state.items[imageToUpdateIndex];
|
||||||
|
|
||||||
|
if (!imageToUpdate) return;
|
||||||
|
const isLast = imageToUpdate.order === state.items.length;
|
||||||
|
const isFirst = imageToUpdate.order === 1;
|
||||||
|
|
||||||
|
let newOrder = imageToUpdate.order;
|
||||||
|
if (action.payload.direction === OrderDirection.Up && !isLast) {
|
||||||
|
newOrder = imageToUpdate.order + 1;
|
||||||
|
}
|
||||||
|
if (action.payload.direction === OrderDirection.Down && !isFirst) {
|
||||||
|
newOrder = imageToUpdate.order - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.items[imageToUpdateIndex] = {
|
||||||
|
...imageToUpdate,
|
||||||
|
order: newOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.items.sort((a, b) => {
|
||||||
|
if (a === b) return 0;
|
||||||
|
if (a < b) return 1;
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateItemOrder: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ id: string; direction: OrderDirection }>,
|
||||||
|
) => {
|
||||||
|
const itemToUpdateIndex = state.items.findIndex(
|
||||||
|
(item) => item.id === action.payload.id,
|
||||||
|
);
|
||||||
|
const itemToUpdate = state.items[itemToUpdateIndex];
|
||||||
|
|
||||||
|
if (!itemToUpdate) return;
|
||||||
|
const isLast = itemToUpdate.order === state.items.length;
|
||||||
|
const isFirst = itemToUpdate.order === 1;
|
||||||
|
|
||||||
|
let newOrder = itemToUpdate.order;
|
||||||
|
if (action.payload.direction === OrderDirection.Up && !isLast) {
|
||||||
|
newOrder = itemToUpdate.order + 1;
|
||||||
|
}
|
||||||
|
if (action.payload.direction === OrderDirection.Down && !isFirst) {
|
||||||
|
newOrder = itemToUpdate.order - 1;
|
||||||
|
}
|
||||||
|
const prevItemIndex = state.items.findIndex(
|
||||||
|
(item) => item.order === newOrder,
|
||||||
|
);
|
||||||
|
const prevItem = state.items[prevItemIndex];
|
||||||
|
|
||||||
|
if (!prevItem) return;
|
||||||
|
|
||||||
|
state.items[prevItemIndex] = {
|
||||||
|
...prevItem,
|
||||||
|
order: itemToUpdate.order,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.items[itemToUpdateIndex] = {
|
||||||
|
...itemToUpdate,
|
||||||
|
order: newOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
state.items.sort((a, b) => {
|
||||||
|
if (a === b) return 0;
|
||||||
|
if (a < b) return 1;
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
selectBackground: (state, action: PayloadAction<string>) => {
|
||||||
|
state.background = {
|
||||||
|
fill: action.payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
selectItem: (state, action: PayloadAction<string>) => {
|
selectItem: (state, action: PayloadAction<string>) => {
|
||||||
state.selectedItemId = action.payload;
|
state.selectedItemId = action.payload;
|
||||||
},
|
},
|
||||||
@@ -95,56 +210,61 @@ export const appSlice = createSlice({
|
|||||||
|
|
||||||
updateText: (
|
updateText: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ id: string } & Partial<TransformableTextProps>>,
|
action: PayloadAction<{ id: string } & Partial<StageTextItem["params"]>>,
|
||||||
) => {
|
) => {
|
||||||
const textToUpdateIndex = state.texts.findIndex(
|
const { id, ...params } = action.payload;
|
||||||
(t) => t.id === action.payload.id,
|
const textToUpdateIndex = state.items.findIndex((t) => t.id === id);
|
||||||
);
|
const textToUpdate = state.items[textToUpdateIndex];
|
||||||
const textToUpdate = state.texts[textToUpdateIndex];
|
|
||||||
|
|
||||||
if (!textToUpdate) return;
|
if (!textToUpdate || textToUpdate.type !== StageItemType.Text) return;
|
||||||
|
|
||||||
state.texts[textToUpdateIndex] = {
|
state.items[textToUpdateIndex] = {
|
||||||
...textToUpdate,
|
...textToUpdate,
|
||||||
...action.payload,
|
params: {
|
||||||
|
...textToUpdate.params,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
updateImage: (
|
updateImage: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{ id: string } & Partial<TransformableImageProps>>,
|
action: PayloadAction<{ id: string } & Partial<StageImageItem["params"]>>,
|
||||||
) => {
|
) => {
|
||||||
const imageToUpdateIndex = state.images.findIndex(
|
const { id, ...params } = action.payload;
|
||||||
(img) => img.id === action.payload.id,
|
const imageToUpdateIndex = state.items.findIndex((img) => img.id === id);
|
||||||
);
|
const imageToUpdate = state.items[imageToUpdateIndex];
|
||||||
const imageToUpdate = state.images[imageToUpdateIndex];
|
|
||||||
|
|
||||||
if (!imageToUpdate) return;
|
if (!imageToUpdate || imageToUpdate.type !== StageItemType.Image) return;
|
||||||
|
|
||||||
state.images[imageToUpdateIndex] = {
|
(state.items[imageToUpdateIndex] as WritableDraft<StageImageItem>) = {
|
||||||
...imageToUpdate,
|
...imageToUpdate,
|
||||||
...action.payload,
|
params: {
|
||||||
|
...imageToUpdate.params,
|
||||||
|
...params,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteShape: (state, action: PayloadAction<string>) => {
|
deleteStageItem: (state, action: PayloadAction<string>) => {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
texts: state.texts.filter((shape) => shape.id !== action.payload),
|
items: state.items.filter((item) => item.id !== action.payload),
|
||||||
images: state.images.filter((shape) => shape.id !== action.payload),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
setStageScale,
|
||||||
addImage,
|
addImage,
|
||||||
addText,
|
addText,
|
||||||
selectItem,
|
selectItem,
|
||||||
deselectItem,
|
deselectItem,
|
||||||
updateText,
|
updateText,
|
||||||
updateImage,
|
updateImage,
|
||||||
deleteShape,
|
deleteStageItem,
|
||||||
updateStage,
|
updateStage,
|
||||||
selectBackground,
|
selectBackground,
|
||||||
|
updateItemOrder,
|
||||||
} = appSlice.actions;
|
} = appSlice.actions;
|
||||||
|
|||||||
Reference in New Issue
Block a user