dpage
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
VITE_API_URL = 'http://188.121.103.8:4141'
|
VITE_API_URL = 'http://89.32.249.26:4010'
|
||||||
# VITE_API_URL = 'http://188.121.103.8:4141'
|
# VITE_API_URL = 'http://192.168.99.131:4000'
|
||||||
VITE_TOKEN_NAME = 'dpage-editor-t'
|
VITE_TOKEN_NAME = 'dpage-editor-t'
|
||||||
VITE_REFRESH_TOKEN_NAME = 'dpage-editor-refresh-t'
|
VITE_REFRESH_TOKEN_NAME = 'dpage-editor-refresh-t'
|
||||||
@@ -23,6 +23,11 @@ import ZoomControls from "./ZoomControls";
|
|||||||
import EditorCanvasToolbar from "./EditorCanvasToolbar";
|
import EditorCanvasToolbar from "./EditorCanvasToolbar";
|
||||||
import { createScopedId } from "../store/editorStore.helpers";
|
import { createScopedId } from "../store/editorStore.helpers";
|
||||||
import { getKonvaGradientProps } from "../utils/gradient";
|
import { getKonvaGradientProps } from "../utils/gradient";
|
||||||
|
import { STICKER_DRAG_MIME } from "../types/IconTypes";
|
||||||
|
import {
|
||||||
|
createStickerObject,
|
||||||
|
scaleStickerDimensions,
|
||||||
|
} from "../utils/stickerUtils";
|
||||||
|
|
||||||
type EditorCanvasProps = {
|
type EditorCanvasProps = {
|
||||||
catalogSize?: string;
|
catalogSize?: string;
|
||||||
@@ -50,6 +55,9 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
setLayerRef,
|
setLayerRef,
|
||||||
setGuides,
|
setGuides,
|
||||||
zoom,
|
zoom,
|
||||||
|
addObject,
|
||||||
|
setSelectedObjectId,
|
||||||
|
setTool,
|
||||||
} = useEditorStore();
|
} = useEditorStore();
|
||||||
const isSmartGuideEnabled = documentSettings.smartGuide;
|
const isSmartGuideEnabled = documentSettings.smartGuide;
|
||||||
const currentPage = pages.find((page) => page.id === currentPageId);
|
const currentPage = pages.find((page) => page.id === currentPageId);
|
||||||
@@ -360,6 +368,39 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
|
|
||||||
const scaledW = stageSize.width * finalScale;
|
const scaledW = stageSize.width * finalScale;
|
||||||
const scaledH = stageSize.height * 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(
|
const backgroundGradientProps = getKonvaGradientProps(
|
||||||
currentPage?.backgroundType === "gradient" ? "gradient" : "solid",
|
currentPage?.backgroundType === "gradient" ? "gradient" : "solid",
|
||||||
bgGradient,
|
bgGradient,
|
||||||
@@ -410,7 +451,12 @@ const EditorCanvas = ({ catalogSize }: EditorCanvasProps) => {
|
|||||||
title="Drag to create horizontal guide"
|
title="Drag to create horizontal guide"
|
||||||
/>
|
/>
|
||||||
<div className="absolute -top-6 -left-6 w-6 h-6 bg-slate-200 border border-slate-300" />
|
<div className="absolute -top-6 -left-6 w-6 h-6 bg-slate-200 border border-slate-300" />
|
||||||
<div ref={stageWrapRef} className="shadow-lg">
|
<div
|
||||||
|
ref={stageWrapRef}
|
||||||
|
className="shadow-lg"
|
||||||
|
onDragOver={handleStickerDragOver}
|
||||||
|
onDrop={handleStickerDrop}
|
||||||
|
>
|
||||||
<Stage
|
<Stage
|
||||||
ref={stageRef}
|
ref={stageRef}
|
||||||
width={stageSize.width * finalScale}
|
width={stageSize.width * finalScale}
|
||||||
|
|||||||
@@ -218,6 +218,9 @@ const ObjectRenderer = ({
|
|||||||
case "document":
|
case "document":
|
||||||
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
||||||
break;
|
break;
|
||||||
|
case "sticker":
|
||||||
|
shapeElement = <ImageObject key={obj.id} {...commonProps} />;
|
||||||
|
break;
|
||||||
case "grid":
|
case "grid":
|
||||||
shapeElement = (
|
shapeElement = (
|
||||||
<Table
|
<Table
|
||||||
|
|||||||
@@ -1,9 +1,59 @@
|
|||||||
const StickerInstruction = () => (
|
import { useMemo } from "react";
|
||||||
<div className="text-sm text-gray-600">
|
import { useGetIconGroups } from "@/pages/editor/hooks/useIconData";
|
||||||
<p>برای افزودن استیکر، روی کانوس کلیک کنید</p>
|
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<HTMLImageElement>, url: string) => {
|
||||||
|
e.dataTransfer.setData(STICKER_DRAG_MIME, url);
|
||||||
|
e.dataTransfer.effectAllowed = "copy";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-base font-bold">استیکر</div>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
استیکر را بکشید و روی صفحه رها کنید
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<p className="text-sm text-gray-500">در حال بارگذاری استیکرها...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<p className="text-sm text-red-500">خطا در بارگذاری استیکرها</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isLoading && !isError && stickers.length === 0 && (
|
||||||
|
<p className="text-sm text-gray-500">استیکری یافت نشد</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stickers.length > 0 && (
|
||||||
|
<div className="grid grid-cols-3 gap-2 max-h-[320px] overflow-y-auto">
|
||||||
|
{stickers.map((icon) => (
|
||||||
|
<div
|
||||||
|
key={icon.id}
|
||||||
|
className="flex aspect-square items-center justify-center rounded-lg border border-gray-200 bg-gray-50 p-1 hover:border-blue-300 hover:bg-blue-50 transition-colors"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={icon.url}
|
||||||
|
alt=""
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => handleDragStart(e, icon.url)}
|
||||||
|
className="max-h-full max-w-full object-contain cursor-grab active:cursor-grabbing"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default StickerInstruction;
|
export default StickerInstruction;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
import type { IconGroupsResponse } from "../types/IconTypes";
|
||||||
|
|
||||||
|
export const getIconGroups = async () => {
|
||||||
|
const { data } = await axios.get<IconGroupsResponse>("/admin/groups/icons");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -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<IconGroup[]>;
|
||||||
|
|
||||||
|
export const STICKER_DRAG_MIME = "application/x-dpage-sticker";
|
||||||
@@ -117,7 +117,7 @@ const drawObject = async (
|
|||||||
ctx.scale(object.scaleX || 1, object.scaleY || 1);
|
ctx.scale(object.scaleX || 1, object.scaleY || 1);
|
||||||
|
|
||||||
// برای image objects
|
// برای image objects
|
||||||
if (object.type === "image" || object.type === "document") {
|
if (object.type === "image" || object.type === "document" || object.type === "sticker") {
|
||||||
if (object.imageUrl) {
|
if (object.imageUrl) {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = "anonymous";
|
img.crossOrigin = "anonymous";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
});
|
||||||
@@ -172,6 +172,7 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'image':
|
case 'image':
|
||||||
|
case 'sticker':
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
key={obj.id || index}
|
key={obj.id || index}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
|||||||
baseObject.borderRadius = obj.borderRadius;
|
baseObject.borderRadius = obj.borderRadius;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.type === "image" && obj.imageUrl) {
|
if ((obj.type === "image" || obj.type === "sticker") && obj.imageUrl) {
|
||||||
baseObject.imageUrl = obj.imageUrl;
|
baseObject.imageUrl = obj.imageUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user