diff --git a/.env b/.env
index e11c74f..2aa0616 100644
--- a/.env
+++ b/.env
@@ -1,4 +1,4 @@
-VITE_API_URL = 'http://188.121.103.8:4141'
-# VITE_API_URL = 'http://188.121.103.8:4141'
+VITE_API_URL = 'http://89.32.249.26:4010'
+# VITE_API_URL = 'http://192.168.99.131:4000'
VITE_TOKEN_NAME = 'dpage-editor-t'
VITE_REFRESH_TOKEN_NAME = 'dpage-editor-refresh-t'
\ No newline at end of file
diff --git a/src/pages/editor/components/EditorCanvas.tsx b/src/pages/editor/components/EditorCanvas.tsx
index 89d9f0a..d5a4f07 100644
--- a/src/pages/editor/components/EditorCanvas.tsx
+++ b/src/pages/editor/components/EditorCanvas.tsx
@@ -23,6 +23,11 @@ import ZoomControls from "./ZoomControls";
import EditorCanvasToolbar from "./EditorCanvasToolbar";
import { createScopedId } from "../store/editorStore.helpers";
import { getKonvaGradientProps } from "../utils/gradient";
+import { STICKER_DRAG_MIME } from "../types/IconTypes";
+import {
+ createStickerObject,
+ scaleStickerDimensions,
+} from "../utils/stickerUtils";
type EditorCanvasProps = {
catalogSize?: string;
@@ -50,6 +55,9 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
setLayerRef,
setGuides,
zoom,
+ addObject,
+ setSelectedObjectId,
+ setTool,
} = useEditorStore();
const isSmartGuideEnabled = documentSettings.smartGuide;
const currentPage = pages.find((page) => page.id === currentPageId);
@@ -360,6 +368,39 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
const scaledW = stageSize.width * finalScale;
const scaledH = stageSize.height * finalScale;
+
+ const handleStickerDragOver = (e: React.DragEvent) => {
+ if (!e.dataTransfer.types.includes(STICKER_DRAG_MIME)) return;
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "copy";
+ };
+
+ const placeStickerAt = (imageUrl: string, stageX: number, stageY: number, width: number, height: number) => {
+ const sticker = createStickerObject(imageUrl, stageX, stageY, width, height);
+ addObject(sticker);
+ setSelectedObjectId(sticker.id);
+ setTool("select");
+ };
+
+ const handleStickerDrop = (e: React.DragEvent) => {
+ const imageUrl = e.dataTransfer.getData(STICKER_DRAG_MIME);
+ if (!imageUrl || !stageWrapRef.current) return;
+ e.preventDefault();
+
+ const rect = stageWrapRef.current.getBoundingClientRect();
+ const stageX = (e.clientX - rect.left) / finalScale;
+ const stageY = (e.clientY - rect.top) / finalScale;
+
+ const img = new Image();
+ img.onload = () => {
+ const { width, height } = scaleStickerDimensions(img.naturalWidth, img.naturalHeight);
+ placeStickerAt(imageUrl, stageX, stageY, width, height);
+ };
+ img.onerror = () => {
+ placeStickerAt(imageUrl, stageX, stageY, 100, 100);
+ };
+ img.src = imageUrl;
+ };
const backgroundGradientProps = getKonvaGradientProps(
currentPage?.backgroundType === "gradient" ? "gradient" : "solid",
bgGradient,
@@ -410,7 +451,12 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
title="Drag to create horizontal guide"
/>
-
+
;
break;
+ case "sticker":
+ shapeElement =
;
+ break;
case "grid":
shapeElement = (
(
-
-
برای افزودن استیکر، روی کانوس کلیک کنید
+import { useMemo } from "react";
+import { useGetIconGroups } from "@/pages/editor/hooks/useIconData";
+import { STICKER_DRAG_MIME } from "@/pages/editor/types/IconTypes";
+
+const StickerInstruction = () => {
+ const { data, isLoading, isError } = useGetIconGroups();
+
+ const stickers = useMemo(() => {
+ const groups = data?.data ?? [];
+ return groups.flatMap((group) => group.icons);
+ }, [data]);
+
+ const handleDragStart = (e: React.DragEvent
, url: string) => {
+ e.dataTransfer.setData(STICKER_DRAG_MIME, url);
+ e.dataTransfer.effectAllowed = "copy";
+ };
+
+ return (
+
+
استیکر
+
+ استیکر را بکشید و روی صفحه رها کنید
+
+
+ {isLoading && (
+
در حال بارگذاری استیکرها...
+ )}
+
+ {isError && (
+
خطا در بارگذاری استیکرها
+ )}
+
+ {!isLoading && !isError && stickers.length === 0 && (
+
استیکری یافت نشد
+ )}
+
+ {stickers.length > 0 && (
+
+ {stickers.map((icon) => (
+
+

handleDragStart(e, icon.url)}
+ className="max-h-full max-w-full object-contain cursor-grab active:cursor-grabbing"
+ />
+
+ ))}
+
+ )}
-);
+ );
+};
export default StickerInstruction;
-
-
diff --git a/src/pages/editor/hooks/useIconData.ts b/src/pages/editor/hooks/useIconData.ts
new file mode 100644
index 0000000..5a4a827
--- /dev/null
+++ b/src/pages/editor/hooks/useIconData.ts
@@ -0,0 +1,10 @@
+import { useQuery } from "@tanstack/react-query";
+import * as api from "../service/IconService";
+
+export const useGetIconGroups = () => {
+ return useQuery({
+ queryKey: ["icon-groups"],
+ queryFn: api.getIconGroups,
+ staleTime: 5 * 60 * 1000,
+ });
+};
diff --git a/src/pages/editor/service/IconService.ts b/src/pages/editor/service/IconService.ts
new file mode 100644
index 0000000..3cb27fa
--- /dev/null
+++ b/src/pages/editor/service/IconService.ts
@@ -0,0 +1,7 @@
+import axios from "@/config/axios";
+import type { IconGroupsResponse } from "../types/IconTypes";
+
+export const getIconGroups = async () => {
+ const { data } = await axios.get("/admin/groups/icons");
+ return data;
+};
diff --git a/src/pages/editor/types/IconTypes.ts b/src/pages/editor/types/IconTypes.ts
new file mode 100644
index 0000000..649d29f
--- /dev/null
+++ b/src/pages/editor/types/IconTypes.ts
@@ -0,0 +1,23 @@
+import type { BaseResponse } from "@/shared/types/SharedTypes";
+
+export type IconItem = {
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ url: string;
+ group: string;
+};
+
+export type IconGroup = {
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ name: string;
+ icons: IconItem[];
+};
+
+export type IconGroupsResponse = BaseResponse;
+
+export const STICKER_DRAG_MIME = "application/x-dpage-sticker";
diff --git a/src/pages/editor/utils/exportUtils.ts b/src/pages/editor/utils/exportUtils.ts
index 7360418..d0750ad 100644
--- a/src/pages/editor/utils/exportUtils.ts
+++ b/src/pages/editor/utils/exportUtils.ts
@@ -117,7 +117,7 @@ const drawObject = async (
ctx.scale(object.scaleX || 1, object.scaleY || 1);
// برای image objects
- if (object.type === "image" || object.type === "document") {
+ if (object.type === "image" || object.type === "document" || object.type === "sticker") {
if (object.imageUrl) {
const img = new Image();
img.crossOrigin = "anonymous";
diff --git a/src/pages/editor/utils/stickerUtils.ts b/src/pages/editor/utils/stickerUtils.ts
new file mode 100644
index 0000000..03da14a
--- /dev/null
+++ b/src/pages/editor/utils/stickerUtils.ts
@@ -0,0 +1,36 @@
+import type { ToolType } from "../store/editorStore.types";
+
+const STICKER_MAX_SIZE = 120;
+
+export const scaleStickerDimensions = (
+ naturalWidth: number,
+ naturalHeight: number,
+ maxSize = STICKER_MAX_SIZE,
+) => {
+ let width = naturalWidth;
+ let height = naturalHeight;
+
+ if (width > maxSize || height > maxSize) {
+ const ratio = Math.min(maxSize / width, maxSize / height);
+ width = Math.round(width * ratio);
+ height = Math.round(height * ratio);
+ }
+
+ return { width, height };
+};
+
+export const createStickerObject = (
+ imageUrl: string,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+) => ({
+ id: `sticker-${Date.now()}`,
+ type: "sticker" as ToolType,
+ x: Math.round(x - width / 2),
+ y: Math.round(y - height / 2),
+ width,
+ height,
+ imageUrl,
+});
diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx
index d93a6f3..cc89b5d 100644
--- a/src/pages/viewer/components/BookPage.tsx
+++ b/src/pages/viewer/components/BookPage.tsx
@@ -172,6 +172,7 @@ const BookPage = forwardRef(
}
case 'image':
+ case 'sticker':
return (
![]()