added css finitions, background bug corrected, added item state

This commit is contained in:
Artur AGH
2023-11-09 15:58:40 +01:00
parent 88feafddc3
commit 6e91fb6447
22 changed files with 440 additions and 197 deletions

View File

@@ -15,6 +15,10 @@ const config = {
// Feel free to reconfigure them to your own preference.
"@typescript-eslint/array-type": "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": [
"warn",

View File

@@ -7,7 +7,13 @@ await import("./src/env.mjs");
/** @type {import("next").NextConfig} */
const config = {
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.
*

View File

@@ -8,7 +8,7 @@
"dev": "next dev",
"postinstall": "prisma generate",
"lint": "next lint",
"start": "next start"
"start": "next start --port 3333"
},
"dependencies": {
"@headlessui/react": "^1.7.17",
@@ -43,6 +43,7 @@
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"font-picker-react": "^3.5.2",
"immer": "^10.0.3",
"konva": "^9.2.0",
"lucide-react": "^0.279.0",
"next": "^13.4.19",

7
pnpm-lock.yaml generated
View File

@@ -101,6 +101,9 @@ dependencies:
font-picker-react:
specifier: ^3.5.2
version: 3.5.2(@types/react@18.2.31)(react@18.2.0)
immer:
specifier: ^10.0.3
version: 10.0.3
konva:
specifier: ^9.2.0
version: 9.2.2
@@ -3320,6 +3323,10 @@ packages:
engines: {node: '>= 4'}
dev: true
/immer@10.0.3:
resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==}
dev: false
/immer@9.0.21:
resolution: {integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==}
dev: false

View File

@@ -1,28 +1,41 @@
import { TransformableImage } from "@/components/transformable-image";
import type { KonvaEventObject } from "konva/lib/Node";
import { Layer, Rect, Stage } from "react-konva";
import TransformableText from "./transformable-text";
import { useAppDispatch, useAppSelector } from "@/hooks";
import { appSlice, deselectItem } from "@/store/app.slice";
import { useEffect, useState } from "react";
import {
appSlice,
deselectItem,
setStageScale,
StageItemType,
updateImage,
updateText,
} from "@/store/app.slice";
import { useEffect, useState, type MouseEvent } from "react";
import { Toolbar } from "@/components/toolbar";
import type Konva from "konva";
import TransformableText from "@/components/transformable-text";
const Canvas = () => {
const dispatch = useAppDispatch();
const stage = useAppSelector((state) => state.app.stage);
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
const selectItem = (id: string) => dispatch(appSlice.actions.selectItem(id));
const texts = useAppSelector((state) => state.app.texts);
const images = useAppSelector((state) => state.app.images);
const backgroundRects = useAppSelector((state) => state.app.backgroundRects);
const items = useAppSelector((state) => state.app.items);
const backgroundRect = useAppSelector((state) => state.app.background);
const backgroundId = "background";
const deselectHandler = (
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) {
return;
}
console.log(target);
dispatch(deselectItem());
};
@@ -48,61 +61,124 @@ const Canvas = () => {
setSelected(!isTransforming);
}
return (
<div className="flex h-screen w-full flex-col items-center">
<Toolbar />
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 (
<div className="relative flex h-full w-full flex-col items-center">
<Toolbar />
<div
className={"flex h-full w-full items-center justify-center"}
onClick={(e) => {
if (e.target !== e.currentTarget) {
return;
}
dispatch(deselectItem());
}}
>
<Stage
className="m-[3rem] bg-white"
className="bg-white"
width={stage.width}
height={stage.height}
onTouchStart={deselectHandler}
onMouseDown={deselectHandler}
x={stage.x}
y={stage.y}
scaleX={stage.scale}
scaleY={stage.scale}
// onWheel={handleWheel}
>
<Layer>
{images.map((image) => {
{!!backgroundRect && (
<Rect
width={stage.width}
height={stage.height}
fill={backgroundRect.fill}
onTouchStart={deselectHandler}
onClick={deselectHandler}
id={backgroundId}
/>
)}
{items.map((item) => {
if (item.type === StageItemType.Image) {
return (
<TransformableImage
onSelect={() => selectItem(image.id)}
isSelected={image.id === selectedItemId}
id={item.id}
onSelect={() => selectItem(item.id)}
isSelected={item.id === selectedItemId}
onChange={(newAttrs) => {
images.map((i) => (i.id === image.id ? newAttrs : i));
dispatch(updateImage({ id: item.id, ...newAttrs }));
}}
imageProps={image}
key={image.id}
imageProps={item.params}
key={item.id}
/>
);
})}
{texts.map((text) => {
}
if (item.type === StageItemType.Text) {
return (
<TransformableText
onSelect={() => selectItem(text.id)}
isSelected={text.id === selectedItemId}
onSelect={() => selectItem(item.id)}
isSelected={item.id === selectedItemId}
onChange={(newAttrs) => {
texts.map((t) => (t.id === text.id ? newAttrs : t));
dispatch(updateText({ id: item.id, ...newAttrs }));
}}
textProps={text}
key={text.id}
/>
);
})}
{backgroundRects.map((rect, i) => {
return (
<Rect
key={i}
width={rect.width}
height={rect.width}
fill={rect.fill}
textProps={item.params}
key={item.id}
id={item.id}
/>
);
}
})}
</Layer>
{/* <Layer>
<Text zIndex={10} x={30} y={200} fontSize={48} text="Hello" />
<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>*/}
</Stage>
</div>
</div>
);
};

View File

@@ -1,6 +1,6 @@
import React from "react";
import { Button } from "@/components/ui/button";
import { deleteShape } from "@/store/app.slice";
import { deleteStageItem } from "@/store/app.slice";
import { useAppDispatch } from "@/hooks";
import { BsTrash3 } from "react-icons/bs";
@@ -11,7 +11,7 @@ type Props = {
export const DeleteShapeButton = ({ selectedItemId }: Props) => {
const dispatch = useAppDispatch();
const deleteTextHandler = (selectedItemId: string) => {
dispatch(deleteShape(selectedItemId));
dispatch(deleteStageItem(selectedItemId));
};
return (
<Button

View File

@@ -2,11 +2,16 @@ import React from "react";
import { TbFlipHorizontal, TbFlipVertical } from "react-icons/tb";
import { Toggle } from "@/components/ui/toggle";
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 { 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 flipImageVerticaly = () => {
const currentScale = currentImage.scaleY;

View File

@@ -8,7 +8,7 @@ type Props = {
export const Layout = ({ children }: Props) => {
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 />
<div className="flex h-full ">
<Sidebar />

View File

@@ -7,8 +7,22 @@ import {
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { RxStack } from "react-icons/rx";
import { useAppDispatch, useAppSelector } from "@/hooks";
import { OrderDirection, updateItemOrder } from "@/store/app.slice";
export const Layers = () => {
const dispatch = useAppDispatch();
const items = useAppSelector((state) => state.app.items);
const updateImageLayer = (id: string, direction: OrderDirection) => {
dispatch(
updateItemOrder({
id,
direction,
}),
);
};
return (
<Popover>
<PopoverTrigger asChild>
@@ -22,7 +36,23 @@ export const Layers = () => {
<CardTitle className="text-center">Layer</CardTitle>
</CardHeader>
<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>
</Card>
</PopoverContent>

View File

@@ -6,7 +6,7 @@ import { Layers } from "@/components/layout/sidebar/layers";
export function Sidebar() {
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 />
<TextInput />
<ImageInput />

View File

@@ -38,7 +38,7 @@ const SizeSelect = () => {
<PopoverContent side="right" className="mt-4">
<Card className="p-3">
<CardHeader>
<CardTitle>Sélectionnez la taille d'étiquette</CardTitle>
<CardTitle>{`Sélectionnez la taille d'étiquette`}</CardTitle>
</CardHeader>
<div className="flex flex-col gap-2">

View File

@@ -5,15 +5,11 @@ 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";
import { DeleteShapeButton } from "@/components/delete-shape-button";
import { type StageTextItem } from "@/store/app.slice";
type Props = {
selectedItemId: string;
currentText: TransformableTextProps["textProps"];
currentText: StageTextItem["params"];
onTextColorChange: (e: ChangeEvent<HTMLInputElement>) => void;
};
export const TextToolbar = ({
@@ -21,7 +17,6 @@ export const TextToolbar = ({
selectedItemId,
onTextColorChange,
}: Props) => {
const dispatch = useAppDispatch();
if (!currentText) return null;
return (

View File

@@ -21,6 +21,7 @@ export function FontSizeSelector({ currentText, selectedItemId }: Props) {
updateText({
id: selectedItemId,
fontSize: e,
height: e,
}),
);
};

View File

@@ -2,20 +2,18 @@ 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 { StageItemType, updateText } from "@/store/app.slice";
import { DeleteShapeButton } from "@/components/delete-shape-button";
import { Separator } from "@/components/ui/separator";
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 items = useAppSelector((state) => state.app.items);
const currentText = texts.find((t) => t.id === selectedItemId);
const currentImage = images.find((img) => img.id === selectedItemId);
const currentItem = items.find((t) => t.id === selectedItemId);
if (!selectedItemId) return null;
if (!currentItem || !selectedItemId) return null;
const handleTextColorChange = (e: ChangeEvent<HTMLInputElement>) => {
if (!selectedItemId) return;
@@ -28,17 +26,17 @@ export const Toolbar = () => {
};
return (
<div className=" flex h-[5rem] w-full gap-6 border border-t-0 bg-white p-[1rem]">
{currentText && (
<div className="inset absolute flex h-[5rem] w-full gap-6 border border-t-0 bg-white p-[1rem]">
{currentItem.type === StageItemType.Text && (
<TextToolbar
currentText={currentText}
currentText={currentItem.params}
selectedItemId={selectedItemId}
onTextColorChange={handleTextColorChange}
/>
)}
{currentImage && (
{currentItem.type === StageItemType.Image && (
<ImageToolbar
currentImage={currentImage}
currentImage={currentItem.params}
selectedItemId={selectedItemId}
/>
)}

View File

@@ -4,13 +4,13 @@ import { Transformer, Image } from "react-konva";
import useImage from "use-image";
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
image?: ImageConfig["image"];
image?: HTMLImageElement;
imageUrl: string;
id: string;
};
export type TransformableImageProps = {
imageProps: TransformableImageConfig;
id: string;
isSelected: boolean;
onSelect: () => void;
onChange: (newAttrs: TransformableImageConfig) => void;
@@ -18,6 +18,7 @@ export type TransformableImageProps = {
export const TransformableImage = ({
imageProps,
id,
isSelected,
onSelect,
onChange,
@@ -26,7 +27,6 @@ export const TransformableImage = ({
const trRef = useRef<ElementRef<typeof Transformer>>(null);
const [image] = useImage(imageProps.imageUrl);
useEffect(() => {
if (!imageRef.current) return;
@@ -40,6 +40,7 @@ export const TransformableImage = ({
return (
<>
<Image
id={id}
alt={"canvas image"}
onClick={onSelect}
onTap={onSelect}

View File

@@ -1,11 +1,10 @@
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 * as console from "console";
type TransformableTextConfig = Omit<TextConfig, "text"> & {
text?: TextConfig["text"];
id: string;
text?: string;
fill?: string;
direction?: string;
fontFamily: string;
fontSize?: number;
@@ -26,6 +25,7 @@ export type TransformableTextProps = {
isSelected: boolean;
onSelect: () => void;
onChange: (newAttrs: TransformableTextConfig) => void;
id: string;
};
const TransformableText = ({
@@ -33,6 +33,7 @@ const TransformableText = ({
isSelected,
onSelect,
onChange,
id,
}: TransformableTextProps) => {
const textRef = useRef<ElementRef<typeof Text>>(null);
const trRef = useRef<ElementRef<typeof Transformer>>(null);
@@ -100,6 +101,7 @@ const TransformableText = ({
/>
{isSelected && (
<Transformer
id={id}
ref={trRef}
boundBoxFunc={(oldBox, newBox) => {
return { ...newBox, height: oldBox.height };

View File

@@ -1,10 +1,10 @@
import * as React from "react"
import { DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
@@ -14,14 +14,14 @@ const Command = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
className,
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
type CommandDialogProps = DialogProps;
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
@@ -32,8 +32,8 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
</Command>
</DialogContent>
</Dialog>
)
}
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
@@ -45,14 +45,14 @@ const CommandInput = React.forwardRef<
ref={ref}
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",
className
className,
)}
{...props}
/>
</div>
))
));
CommandInput.displayName = CommandPrimitive.Input.displayName
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
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)}
{...props}
/>
))
));
CommandList.displayName = CommandPrimitive.List.displayName
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
@@ -76,9 +76,9 @@ const CommandEmpty = React.forwardRef<
className="py-6 text-center text-sm"
{...props}
/>
))
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
@@ -88,13 +88,13 @@ const CommandGroup = React.forwardRef<
ref={ref}
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",
className
className,
)}
{...props}
/>
))
));
CommandGroup.displayName = CommandPrimitive.Group.displayName
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
@@ -105,8 +105,8 @@ const CommandSeparator = React.forwardRef<
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
@@ -116,13 +116,13 @@ const CommandItem = React.forwardRef<
ref={ref}
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",
className
className,
)}
{...props}
/>
))
));
CommandItem.displayName = CommandPrimitive.Item.displayName
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
@@ -132,13 +132,13 @@ const CommandShortcut = ({
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
className,
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
@@ -150,4 +150,4 @@ export {
CommandItem,
CommandShortcut,
CommandSeparator,
}
};

View File

@@ -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
extends React.InputHTMLAttributes<HTMLInputElement> {}
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
@@ -12,14 +11,14 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
type={type}
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",
className
className,
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
);
},
);
Input.displayName = "Input";
export { Input }
export { Input };

View File

@@ -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
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
@@ -11,14 +10,14 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
<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
className,
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
);
},
);
Textarea.displayName = "Textarea";
export { Textarea }
export { Textarea };

View File

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

View File

@@ -4,20 +4,43 @@ import type { PayloadAction } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";
import { v1 } from "uuid";
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 = {
stage: { width: 500, height: 500 },
stage: { width: 500, height: 500, scale: 1, x: 0, y: 0 },
items: [] as StageItem[],
selectedItemId: null as string | null,
images: [] as TransformableImageProps["imageProps"][],
texts: [] as TransformableTextProps["textProps"][],
backgroundRects: [] as RectConfig[],
background: null as RectConfig | null,
};
const defaultTextConfig = {
fontSize: 16,
align: "center",
fontFamily: "Roboto",
zIndex: 3,
};
const defaultImageConfig = {
@@ -28,35 +51,41 @@ const defaultImageConfig = {
offsetY: 0,
scaleX: 1,
scaleY: 1,
zIndex: 1,
};
export enum OrderDirection {
Up = "up",
Down = "down",
}
type AddStageItemAction = PayloadAction<StageItem>;
export const appSlice = createSlice({
name: "app",
initialState,
reducers: {
setStageScale: (
state,
action: PayloadAction<{ x: number; y: number; scale: number }>,
) => {
state.stage = {
...state.stage,
...action.payload,
};
},
addText: (state, action: PayloadAction<{ initialValue: string }>) => {
const textId = v1();
state.texts.push({
text: action.payload.initialValue,
state.items.push({
type: StageItemType.Text,
id: textId,
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: (
state,
action: PayloadAction<{
@@ -68,16 +97,102 @@ export const appSlice = createSlice({
const file = action.payload;
if (!file) return;
const imageId = v1();
state.images.push({
imageUrl: action.payload.imageUrl,
state.items.push({
type: StageItemType.Image,
id: imageId,
order: state.items.length + 1,
params: {
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>) => {
state.selectedItemId = action.payload;
},
@@ -95,56 +210,61 @@ export const appSlice = createSlice({
updateText: (
state,
action: PayloadAction<{ id: string } & Partial<TransformableTextProps>>,
action: PayloadAction<{ id: string } & Partial<StageTextItem["params"]>>,
) => {
const textToUpdateIndex = state.texts.findIndex(
(t) => t.id === action.payload.id,
);
const textToUpdate = state.texts[textToUpdateIndex];
const { id, ...params } = action.payload;
const textToUpdateIndex = state.items.findIndex((t) => t.id === id);
const textToUpdate = state.items[textToUpdateIndex];
if (!textToUpdate) return;
if (!textToUpdate || textToUpdate.type !== StageItemType.Text) return;
state.texts[textToUpdateIndex] = {
state.items[textToUpdateIndex] = {
...textToUpdate,
...action.payload,
params: {
...textToUpdate.params,
...params,
},
};
},
updateImage: (
state,
action: PayloadAction<{ id: string } & Partial<TransformableImageProps>>,
action: PayloadAction<{ id: string } & Partial<StageImageItem["params"]>>,
) => {
const imageToUpdateIndex = state.images.findIndex(
(img) => img.id === action.payload.id,
);
const imageToUpdate = state.images[imageToUpdateIndex];
const { id, ...params } = action.payload;
const imageToUpdateIndex = state.items.findIndex((img) => img.id === id);
const imageToUpdate = state.items[imageToUpdateIndex];
if (!imageToUpdate) return;
if (!imageToUpdate || imageToUpdate.type !== StageItemType.Image) return;
state.images[imageToUpdateIndex] = {
(state.items[imageToUpdateIndex] as WritableDraft<StageImageItem>) = {
...imageToUpdate,
...action.payload,
params: {
...imageToUpdate.params,
...params,
},
};
},
deleteShape: (state, action: PayloadAction<string>) => {
deleteStageItem: (state, action: PayloadAction<string>) => {
return {
...state,
texts: state.texts.filter((shape) => shape.id !== action.payload),
images: state.images.filter((shape) => shape.id !== action.payload),
items: state.items.filter((item) => item.id !== action.payload),
};
},
},
});
export const {
setStageScale,
addImage,
addText,
selectItem,
deselectItem,
updateText,
updateImage,
deleteShape,
deleteStageItem,
updateStage,
selectBackground,
updateItemOrder,
} = appSlice.actions;

View File

@@ -1,3 +1,4 @@
Change Text
Delete Text
TextToolbar - text ? text : image
- text transform box doesn't update it's size on font size change
- layout order doesn't work correctly with > 2 objects