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.
|
||||
"@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",
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
7
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -21,6 +21,7 @@ export function FontSizeSelector({ currentText, selectedItemId }: Props) {
|
||||
updateText({
|
||||
id: selectedItemId,
|
||||
fontSize: e,
|
||||
height: e,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -10,9 +10,7 @@ export default function Home() {
|
||||
<title>Labbel Application</title>
|
||||
<link rel="icon" href="/logo.png" />
|
||||
</Head>
|
||||
<div className="flex">
|
||||
<Canvas />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user