base load viewer
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"id": "page-1",
|
||||
"name": "صفحه 1",
|
||||
"objects": [
|
||||
{
|
||||
"id": "text-1",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"text": "خوش آمدید به صفحه اول",
|
||||
"fontSize": 36,
|
||||
"fontFamily": "irancell",
|
||||
"fontWeight": "bold",
|
||||
"fill": "#1a1a1a",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "image-1",
|
||||
"type": "image",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"width": 300,
|
||||
"height": 300,
|
||||
"imageUrl": "https://picsum.photos/200/300",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "rectangle-1",
|
||||
"type": "rectangle",
|
||||
"x": 450,
|
||||
"y": 200,
|
||||
"width": 200,
|
||||
"height": 150,
|
||||
"fill": "#3b82f6",
|
||||
"stroke": "#1e40af",
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "square",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "circle-1",
|
||||
"type": "rectangle",
|
||||
"x": 500,
|
||||
"y": 400,
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"fill": "#ef4444",
|
||||
"stroke": "#dc2626",
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "circle",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "video-1",
|
||||
"type": "video",
|
||||
"x": 100,
|
||||
"y": 550,
|
||||
"width": 400,
|
||||
"height": 300,
|
||||
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
|
||||
"opacity": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page-2",
|
||||
"name": "صفحه 2",
|
||||
"objects": [
|
||||
{
|
||||
"id": "text-2",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"text": "فصل اول: مقدمه",
|
||||
"fontSize": 32,
|
||||
"fontFamily": "irancell",
|
||||
"fontWeight": "bold",
|
||||
"fill": "#000000",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "line-1",
|
||||
"type": "line",
|
||||
"x": 100,
|
||||
"y": 180,
|
||||
"width": 500,
|
||||
"height": 2,
|
||||
"stroke": "#64748b",
|
||||
"strokeWidth": 3,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "arrow-1",
|
||||
"type": "arrow",
|
||||
"x": 200,
|
||||
"y": 250,
|
||||
"width": 300,
|
||||
"height": 100,
|
||||
"stroke": "#f59e0b",
|
||||
"strokeWidth": 4,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "triangle-1",
|
||||
"type": "rectangle",
|
||||
"x": 550,
|
||||
"y": 250,
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"fill": "#10b981",
|
||||
"stroke": "#059669",
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "triangle",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "link-1",
|
||||
"type": "link",
|
||||
"x": 100,
|
||||
"y": 400,
|
||||
"width": 200,
|
||||
"height": 50,
|
||||
"text": "لینک به صفحه وب",
|
||||
"fontSize": 18,
|
||||
"linkUrl": "https://example.com",
|
||||
"fill": "#3b82f6",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "image-2",
|
||||
"type": "image",
|
||||
"x": 350,
|
||||
"y": 400,
|
||||
"width": 250,
|
||||
"height": 200,
|
||||
"imageUrl": "https://picsum.photos/200/300",
|
||||
"opacity": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page-3",
|
||||
"name": "صفحه 3",
|
||||
"objects": [
|
||||
{
|
||||
"id": "text-3",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"text": "صفحه سوم\nاین صفحه شامل جدول است",
|
||||
"fontSize": 28,
|
||||
"fontFamily": "irancell",
|
||||
"fontWeight": "normal",
|
||||
"fill": "#1a1a1a",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "grid-1",
|
||||
"type": "grid",
|
||||
"x": 100,
|
||||
"y": 250,
|
||||
"width": 500,
|
||||
"height": 300,
|
||||
"tableData": {
|
||||
"id": "table-1",
|
||||
"rows": 4,
|
||||
"cols": 5,
|
||||
"cellWidth": 100,
|
||||
"cellHeight": 75,
|
||||
"cells": {
|
||||
"cell-0-0": {
|
||||
"id": "cell-0-0",
|
||||
"text": "ردیف 1",
|
||||
"background": "#f3f4f6",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-0-1": {
|
||||
"id": "cell-0-1",
|
||||
"text": "ستون 1",
|
||||
"background": "#e5e7eb",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-0-2": {
|
||||
"id": "cell-0-2",
|
||||
"text": "ستون 2",
|
||||
"background": "#e5e7eb",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-0-3": {
|
||||
"id": "cell-0-3",
|
||||
"text": "ستون 3",
|
||||
"background": "#e5e7eb",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-0-4": {
|
||||
"id": "cell-0-4",
|
||||
"text": "ستون 4",
|
||||
"background": "#e5e7eb",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-1-0": {
|
||||
"id": "cell-1-0",
|
||||
"text": "ردیف 2",
|
||||
"background": "#f3f4f6",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-1-1": {
|
||||
"id": "cell-1-1",
|
||||
"text": "داده 1",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-1-2": {
|
||||
"id": "cell-1-2",
|
||||
"text": "داده 2",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-1-3": {
|
||||
"id": "cell-1-3",
|
||||
"text": "داده 3",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-1-4": {
|
||||
"id": "cell-1-4",
|
||||
"text": "داده 4",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-2-0": {
|
||||
"id": "cell-2-0",
|
||||
"text": "ردیف 3",
|
||||
"background": "#f3f4f6",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-2-1": {
|
||||
"id": "cell-2-1",
|
||||
"text": "داده 5",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-2-2": {
|
||||
"id": "cell-2-2",
|
||||
"text": "داده 6",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-2-3": {
|
||||
"id": "cell-2-3",
|
||||
"text": "داده 7",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-2-4": {
|
||||
"id": "cell-2-4",
|
||||
"text": "داده 8",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-3-0": {
|
||||
"id": "cell-3-0",
|
||||
"text": "ردیف 4",
|
||||
"background": "#f3f4f6",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-3-1": {
|
||||
"id": "cell-3-1",
|
||||
"text": "داده 9",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-3-2": {
|
||||
"id": "cell-3-2",
|
||||
"text": "داده 10",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-3-3": {
|
||||
"id": "cell-3-3",
|
||||
"text": "داده 11",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
},
|
||||
"cell-3-4": {
|
||||
"id": "cell-3-4",
|
||||
"text": "داده 12",
|
||||
"background": "#ffffff",
|
||||
"width": 100,
|
||||
"height": 75
|
||||
}
|
||||
},
|
||||
"stroke": "#808080",
|
||||
"strokeWidth": 1
|
||||
},
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "abstract-1",
|
||||
"type": "rectangle",
|
||||
"x": 650,
|
||||
"y": 250,
|
||||
"width": 100,
|
||||
"height": 200,
|
||||
"fill": "#8b5cf6",
|
||||
"stroke": "#7c3aed",
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "abstract",
|
||||
"opacity": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page-4",
|
||||
"name": "صفحه 4",
|
||||
"objects": [
|
||||
{
|
||||
"id": "text-4",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"text": "صفحه چهارم\nشامل ویدیو و تصویر",
|
||||
"fontSize": 30,
|
||||
"fontFamily": "irancell",
|
||||
"fontWeight": "bold",
|
||||
"fill": "#1a1a1a",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "video-2",
|
||||
"type": "video",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"width": 350,
|
||||
"height": 250,
|
||||
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "image-3",
|
||||
"type": "image",
|
||||
"x": 500,
|
||||
"y": 200,
|
||||
"width": 250,
|
||||
"height": 250,
|
||||
"imageUrl": "https://picsum.photos/200/300",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "line-2",
|
||||
"type": "line",
|
||||
"x": 100,
|
||||
"y": 500,
|
||||
"width": 600,
|
||||
"height": 2,
|
||||
"stroke": "#94a3b8",
|
||||
"strokeWidth": 2,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "arrow-2",
|
||||
"type": "arrow",
|
||||
"x": 200,
|
||||
"y": 550,
|
||||
"width": 400,
|
||||
"height": 80,
|
||||
"stroke": "#ec4899",
|
||||
"strokeWidth": 5,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "circle-2",
|
||||
"type": "rectangle",
|
||||
"x": 650,
|
||||
"y": 550,
|
||||
"width": 100,
|
||||
"height": 100,
|
||||
"fill": "#f59e0b",
|
||||
"stroke": "#d97706",
|
||||
"strokeWidth": 3,
|
||||
"shapeType": "circle",
|
||||
"opacity": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "page-5",
|
||||
"name": "صفحه 5",
|
||||
"objects": [
|
||||
{
|
||||
"id": "text-5",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"text": "صفحه آخر\nنمایش همه انواع object ها",
|
||||
"fontSize": 32,
|
||||
"fontFamily": "irancell",
|
||||
"fontWeight": "bold",
|
||||
"fill": "#1a1a1a",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "rectangle-2",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 200,
|
||||
"width": 150,
|
||||
"height": 120,
|
||||
"fill": "#06b6d4",
|
||||
"stroke": "#0891b2",
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "square",
|
||||
"rotation": 15,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "triangle-2",
|
||||
"type": "rectangle",
|
||||
"x": 300,
|
||||
"y": 200,
|
||||
"width": 120,
|
||||
"height": 120,
|
||||
"fill": "#14b8a6",
|
||||
"stroke": "#0d9488",
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "triangle",
|
||||
"rotation": 30,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "abstract-2",
|
||||
"type": "rectangle",
|
||||
"x": 470,
|
||||
"y": 200,
|
||||
"width": 100,
|
||||
"height": 150,
|
||||
"fill": "#a855f7",
|
||||
"stroke": "#9333ea",
|
||||
"strokeWidth": 2,
|
||||
"shapeType": "abstract",
|
||||
"rotation": -20,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "image-4",
|
||||
"type": "image",
|
||||
"x": 620,
|
||||
"y": 200,
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"imageUrl": "https://picsum.photos/200/300",
|
||||
"rotation": 10,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "video-3",
|
||||
"type": "video",
|
||||
"x": 100,
|
||||
"y": 380,
|
||||
"width": 300,
|
||||
"height": 200,
|
||||
"videoUrl": "https://loremipsum.video/vt/powerpoint-1.mp4",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "link-2",
|
||||
"type": "link",
|
||||
"x": 450,
|
||||
"y": 380,
|
||||
"width": 250,
|
||||
"height": 60,
|
||||
"text": "لینک به صفحه دیگر",
|
||||
"fontSize": 20,
|
||||
"linkUrl": "https://example.com",
|
||||
"fill": "#6366f1",
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "line-3",
|
||||
"type": "line",
|
||||
"x": 100,
|
||||
"y": 620,
|
||||
"width": 200,
|
||||
"height": 3,
|
||||
"stroke": "#f97316",
|
||||
"strokeWidth": 4,
|
||||
"rotation": 45,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "arrow-3",
|
||||
"type": "arrow",
|
||||
"x": 350,
|
||||
"y": 620,
|
||||
"width": 250,
|
||||
"height": 100,
|
||||
"stroke": "#84cc16",
|
||||
"strokeWidth": 6,
|
||||
"rotation": -30,
|
||||
"opacity": 1
|
||||
},
|
||||
{
|
||||
"id": "text-6",
|
||||
"type": "text",
|
||||
"x": 100,
|
||||
"y": 750,
|
||||
"text": "این یک متن نمونه است\nبا چند خط\nبرای نمایش بهتر",
|
||||
"fontSize": 22,
|
||||
"fontFamily": "irancell",
|
||||
"fontWeight": "normal",
|
||||
"fill": "#475569",
|
||||
"letterSpacing": 1,
|
||||
"opacity": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import Logo from "@/assets/images/logo.svg";
|
||||
import { clx } from "@/helpers/utils";
|
||||
import { useSharedStore } from "@/shared/store/sharedStore";
|
||||
import { CloseSquare } from "iconsax-react";
|
||||
import { CloseSquare, DocumentUpload } from "iconsax-react";
|
||||
import { useEditorStore, type ToolType } from "../store/editorStore";
|
||||
import { ToolsBar, ObjectSettings, ToolInstructions } from "./sidebar";
|
||||
|
||||
const EditorSidebar = () => {
|
||||
const { openSidebar, setOpenSidebar } = useSharedStore();
|
||||
const { tool, setTool, selectedObjectId, objects, updateObject, deleteObject } = useEditorStore();
|
||||
const { tool, setTool, selectedObjectId, objects, updateObject, deleteObject, loadPages } = useEditorStore();
|
||||
|
||||
const selectedObject = objects.find((obj) => obj.id === selectedObjectId);
|
||||
|
||||
@@ -15,6 +15,19 @@ const EditorSidebar = () => {
|
||||
setTool(selectedTool);
|
||||
};
|
||||
|
||||
const handleLoadJSON = async () => {
|
||||
try {
|
||||
const response = await fetch('/viewer-data.json');
|
||||
const data = await response.json();
|
||||
if (data.pages && Array.isArray(data.pages)) {
|
||||
loadPages(data.pages);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('خطا در لود JSON:', error);
|
||||
alert('خطا در لود فایل JSON');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -29,6 +42,14 @@ const EditorSidebar = () => {
|
||||
>
|
||||
<div className="mb-10 flex items-center justify-between">
|
||||
<img src={Logo} alt="لوگو دانک" className="h-9" />
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleLoadJSON}
|
||||
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
title="لود JSON"
|
||||
>
|
||||
<DocumentUpload color="black" size={24} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setOpenSidebar(false)}
|
||||
className="xl:hidden p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||
@@ -36,6 +57,7 @@ const EditorSidebar = () => {
|
||||
<CloseSquare color="black" size={24} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-8 flex-1 min-h-0">
|
||||
<ToolsBar tool={tool} onToolClick={handleToolClick} />
|
||||
|
||||
@@ -99,10 +99,6 @@ const VideoShape = ({
|
||||
}
|
||||
};
|
||||
|
||||
if (!image) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Group
|
||||
@@ -130,6 +126,7 @@ const VideoShape = ({
|
||||
strokeWidth={isSelected ? 3 : (obj.strokeWidth ?? 0)}
|
||||
onClick={handleGroupClick}
|
||||
/>
|
||||
{image ? (
|
||||
<KonvaImage
|
||||
x={0}
|
||||
y={0}
|
||||
@@ -138,6 +135,16 @@ const VideoShape = ({
|
||||
image={image}
|
||||
onClick={handleGroupClick}
|
||||
/>
|
||||
) : (
|
||||
<Rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={containerWidth}
|
||||
height={containerHeight}
|
||||
fill="#1a1a1a"
|
||||
onClick={handleGroupClick}
|
||||
/>
|
||||
)}
|
||||
<Group
|
||||
name="playButton"
|
||||
onClick={handlePlayClick}
|
||||
|
||||
@@ -146,6 +146,7 @@ type EditorStoreType = {
|
||||
toggleObjectVisibility: (id: string) => void;
|
||||
groupObjects: (objectIds: string[]) => void;
|
||||
ungroupObjects: (groupId: string) => void;
|
||||
loadPages: (pages: Page[]) => void;
|
||||
};
|
||||
|
||||
const createTableCellId = (row: number, col: number) => `cell-${row}-${col}`;
|
||||
@@ -904,5 +905,16 @@ export const useEditorStore = create<EditorStoreType>((set, get) => {
|
||||
const state = get();
|
||||
return state.objects.filter((obj) => obj.groupId === groupId);
|
||||
},
|
||||
loadPages: (pages) => {
|
||||
if (pages.length === 0) return;
|
||||
const firstPage = pages[0];
|
||||
set({
|
||||
pages,
|
||||
currentPageId: firstPage.id,
|
||||
objects: firstPage.objects,
|
||||
selectedObjectId: null,
|
||||
selectedObjectIds: [],
|
||||
});
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,11 +1,63 @@
|
||||
import { type FC } from 'react';
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import BookViewer from './components/BookViewer';
|
||||
import { mockPages } from './components/mockData';
|
||||
import { transformViewerDataToPages } from './utils/dataTransformer';
|
||||
import type { PageData } from './types';
|
||||
|
||||
const Viewer: FC = () => {
|
||||
const [pages, setPages] = useState<PageData[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadViewerData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('/viewer-data.json');
|
||||
if (!response.ok) {
|
||||
throw new Error('خطا در بارگذاری دادهها');
|
||||
}
|
||||
const data = await response.json();
|
||||
const transformedPages = transformViewerDataToPages(data);
|
||||
setPages(transformedPages);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'خطای نامشخص');
|
||||
console.error('خطا در بارگذاری viewer-data.json:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadViewerData();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="text-gray-600">در حال بارگذاری...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="text-red-600">خطا: {error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (pages.length === 0) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="text-gray-600">هیچ صفحهای یافت نشد</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<BookViewer pages={mockPages} />
|
||||
<BookViewer pages={pages} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { forwardRef } from 'react';
|
||||
import { type PageData } from '../types';
|
||||
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||
|
||||
type BookPageProps = {
|
||||
page: PageData;
|
||||
scale?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -12,7 +14,323 @@ type BookPageProps = {
|
||||
* این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
|
||||
* نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
|
||||
*/
|
||||
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page }, ref) => {
|
||||
const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page, scale = 1 }, ref) => {
|
||||
const renderObject = (obj: EditorObject, index: number) => {
|
||||
const baseStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
left: `${(obj.x || 0) * scale}px`,
|
||||
top: `${(obj.y || 0) * scale}px`,
|
||||
opacity: obj.opacity ?? 1,
|
||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
};
|
||||
|
||||
if (obj.visible === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (obj.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={{
|
||||
...baseStyle,
|
||||
fontSize: `${(obj.fontSize || 16) * scale}px`,
|
||||
fontFamily: obj.fontFamily || 'irancell, sans-serif',
|
||||
fontWeight: obj.fontWeight || 'normal',
|
||||
color: obj.fill || '#000000',
|
||||
whiteSpace: 'pre-wrap',
|
||||
letterSpacing: obj.letterSpacing ? `${obj.letterSpacing * scale}px` : undefined,
|
||||
}}
|
||||
>
|
||||
{obj.text || ''}
|
||||
</div>
|
||||
);
|
||||
|
||||
case 'image':
|
||||
return (
|
||||
<img
|
||||
key={obj.id || index}
|
||||
src={obj.imageUrl || ''}
|
||||
alt=""
|
||||
style={{
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'video':
|
||||
return (
|
||||
<video
|
||||
key={obj.id || index}
|
||||
src={obj.videoUrl || ''}
|
||||
controls
|
||||
style={{
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'link':
|
||||
return (
|
||||
<a
|
||||
key={obj.id || index}
|
||||
href={obj.linkUrl || '#'}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
...baseStyle,
|
||||
width: obj.width ? `${obj.width * scale}px` : 'auto',
|
||||
height: obj.height ? `${obj.height * scale}px` : 'auto',
|
||||
color: obj.fill || '#3b82f6',
|
||||
fontSize: `${(obj.fontSize || 16) * scale}px`,
|
||||
fontFamily: obj.fontFamily || 'irancell, sans-serif',
|
||||
textDecoration: 'underline',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{obj.text || obj.linkUrl || ''}
|
||||
</a>
|
||||
);
|
||||
|
||||
case 'rectangle':
|
||||
if (obj.shapeType === 'circle') {
|
||||
// در editor، circle با مرکز در x, y رندر میشود (radius = width/2)
|
||||
// اما در viewer-data.json ممکن است x, y به عنوان گوشه بالا-چپ bounding box باشد
|
||||
// برای سازگاری با editor، فرض میکنیم x, y مرکز است
|
||||
const radius = ((obj.width || 100) / 2) * scale;
|
||||
const centerX = (obj.x || 0) * scale;
|
||||
const centerY = (obj.y || 0) * scale;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${centerX - radius}px`,
|
||||
top: `${centerY - radius}px`,
|
||||
width: `${radius * 2}px`,
|
||||
height: `${radius * 2}px`,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: obj.fill || 'transparent',
|
||||
border: obj.stroke
|
||||
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
||||
: 'none',
|
||||
opacity: obj.opacity ?? 1,
|
||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (obj.shapeType === 'triangle') {
|
||||
// در editor، triangle با مرکز در x + width/2, y + height/2 رندر میشود
|
||||
const width = (obj.width || 100) * scale;
|
||||
const height = (obj.height || 100) * scale;
|
||||
const radius = Math.min(width, height) / 2;
|
||||
const centerX = (obj.x || 0) * scale + width / 2;
|
||||
const centerY = (obj.y || 0) * scale + height / 2;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${centerX - radius}px`,
|
||||
top: `${centerY - radius}px`,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderLeft: `${radius}px solid transparent`,
|
||||
borderRight: `${radius}px solid transparent`,
|
||||
borderBottom: `${radius * 2}px solid ${obj.fill || '#000000'}`,
|
||||
backgroundColor: 'transparent',
|
||||
borderTop: 'none',
|
||||
opacity: obj.opacity ?? 1,
|
||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (obj.shapeType === 'abstract') {
|
||||
// در editor، abstract (star) با مرکز در x + width/2, y + height/2 رندر میشود
|
||||
const width = (obj.width || 100) * scale;
|
||||
const height = (obj.height || 100) * scale;
|
||||
const baseRadius = Math.min(width, height) / 2;
|
||||
const abstractScale = 0.85;
|
||||
const radius = baseRadius * abstractScale;
|
||||
const centerX = (obj.x || 0) * scale + width / 2;
|
||||
const centerY = (obj.y || 0) * scale + height / 2;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${centerX - radius}px`,
|
||||
top: `${centerY - radius}px`,
|
||||
width: `${radius * 2}px`,
|
||||
height: `${radius * 2}px`,
|
||||
clipPath: 'polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%)',
|
||||
backgroundColor: obj.fill || 'transparent',
|
||||
border: obj.stroke
|
||||
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
||||
: 'none',
|
||||
opacity: obj.opacity ?? 1,
|
||||
transform: obj.rotation ? `rotate(${obj.rotation}deg)` : undefined,
|
||||
transformOrigin: 'center center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Rectangle عادی
|
||||
const shapeStyle: React.CSSProperties = {
|
||||
...baseStyle,
|
||||
width: `${(obj.width || 100) * scale}px`,
|
||||
height: `${(obj.height || 100) * scale}px`,
|
||||
backgroundColor: obj.fill || 'transparent',
|
||||
border: obj.stroke
|
||||
? `${(obj.strokeWidth || 1) * scale}px solid ${obj.stroke}`
|
||||
: 'none',
|
||||
};
|
||||
|
||||
return <div key={obj.id || index} style={shapeStyle} />;
|
||||
|
||||
case 'line': {
|
||||
// در EditorObject، width و height به عنوان مختصات انتهایی استفاده میشوند
|
||||
// در Konva Line، points به صورت [0, 0, endX - startX, endY - startY] است
|
||||
const startX = (obj.x || 0) * scale;
|
||||
const startY = (obj.y || 0) * scale;
|
||||
const endX = (obj.width ?? obj.x ?? 0) * scale;
|
||||
const endY = (obj.height ?? obj.y ?? 0) * scale;
|
||||
const dx = endX - startX;
|
||||
const dy = endY - startY;
|
||||
const lineLength = Math.sqrt(dx * dx + dy * dy);
|
||||
const lineAngle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${startX}px`,
|
||||
top: `${startY}px`,
|
||||
width: `${lineLength}px`,
|
||||
height: `${(obj.strokeWidth || 2) * scale}px`,
|
||||
backgroundColor: obj.stroke || '#000000',
|
||||
opacity: obj.opacity ?? 1,
|
||||
transform: `rotate(${lineAngle}deg)`,
|
||||
transformOrigin: 'left center',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'arrow': {
|
||||
// در EditorObject، width و height به عنوان مختصات انتهایی استفاده میشوند
|
||||
// در Konva Arrow، points به صورت [0, 0, endX - startX, endY - startY] است
|
||||
const startX = (obj.x || 0) * scale;
|
||||
const startY = (obj.y || 0) * scale;
|
||||
const endX = (obj.width ?? obj.x ?? 0) * scale;
|
||||
const endY = (obj.height ?? obj.y ?? 0) * scale;
|
||||
const dx = endX - startX;
|
||||
const dy = endY - startY;
|
||||
const arrowLength = Math.sqrt(dx * dx + dy * dy);
|
||||
const arrowAngle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||
const strokeWidth = (obj.strokeWidth || 4) * scale;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${startX}px`,
|
||||
top: `${startY}px`,
|
||||
width: `${arrowLength}px`,
|
||||
height: `${strokeWidth}px`,
|
||||
backgroundColor: obj.stroke || '#000000',
|
||||
opacity: obj.opacity ?? 1,
|
||||
transform: `rotate(${arrowAngle}deg)`,
|
||||
transformOrigin: 'left center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: `-${strokeWidth * 2}px`,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTop: `${strokeWidth * 2}px solid transparent`,
|
||||
borderBottom: `${strokeWidth * 2}px solid transparent`,
|
||||
borderLeft: `${strokeWidth * 2}px solid ${obj.stroke || '#000000'}`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
case 'grid':
|
||||
if (!obj.tableData) return null;
|
||||
|
||||
const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } =
|
||||
obj.tableData;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={obj.id || index}
|
||||
style={{
|
||||
...baseStyle,
|
||||
display: 'grid',
|
||||
gridTemplateRows: `repeat(${rows}, ${cellHeight * scale}px)`,
|
||||
gridTemplateColumns: `repeat(${cols}, ${cellWidth * scale}px)`,
|
||||
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : 'none',
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: rows }).map((_, rowIndex) =>
|
||||
Array.from({ length: cols }).map((_, colIndex) => {
|
||||
const cellId = `cell-${rowIndex}-${colIndex}`;
|
||||
const cell = cells[cellId];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={cellId}
|
||||
style={{
|
||||
border: stroke ? `${(strokeWidth || 1) * scale}px solid ${stroke}` : `${1 * scale}px solid #ccc`,
|
||||
backgroundColor: cell?.background || '#ffffff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: `${4 * scale}px`,
|
||||
fontSize: `${14 * scale}px`,
|
||||
}}
|
||||
>
|
||||
{cell?.text || ''}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -22,52 +340,12 @@ const BookPage = forwardRef<HTMLDivElement, BookPageProps>(({ page }, ref) => {
|
||||
position: 'relative',
|
||||
backgroundColor: '#ffffff',
|
||||
overflow: 'hidden',
|
||||
padding: '40px',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: `${794 * scale}px`,
|
||||
height: `${1123 * scale}px`,
|
||||
}}
|
||||
>
|
||||
{page.elements.map((element, index) => {
|
||||
if (element.type === 'text') {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${element.x}px`,
|
||||
top: `${element.y}px`,
|
||||
fontSize: `${element.fontSize}px`,
|
||||
color: element.color,
|
||||
fontFamily: 'inherit',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{element.text}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (element.type === 'image') {
|
||||
return (
|
||||
<img
|
||||
key={index}
|
||||
src={element.src}
|
||||
alt=""
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${element.x}px`,
|
||||
top: `${element.y}px`,
|
||||
width: `${element.width}px`,
|
||||
height: `${element.height}px`,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
{page.elements.map((element, index) => renderObject(element, index))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -56,18 +56,18 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
||||
/>
|
||||
<HTMLFlipBook
|
||||
ref={bookRef}
|
||||
width={794}
|
||||
height={1123}
|
||||
size="stretch"
|
||||
minWidth={315}
|
||||
minHeight={400}
|
||||
maxWidth={1000}
|
||||
maxHeight={1533}
|
||||
width={397}
|
||||
height={561}
|
||||
size="fixed"
|
||||
minWidth={397}
|
||||
minHeight={561}
|
||||
maxWidth={397}
|
||||
maxHeight={561}
|
||||
drawShadow={true}
|
||||
flippingTime={800}
|
||||
usePortrait={true}
|
||||
usePortrait={false}
|
||||
startPage={0}
|
||||
autoSize={true}
|
||||
autoSize={false}
|
||||
maxShadowOpacity={0.5}
|
||||
showCover={true}
|
||||
mobileScrollSupport={true}
|
||||
@@ -84,7 +84,7 @@ const BookViewer: FC<BookViewerProps> = ({ pages }) => {
|
||||
onFlip={handlePageFlip}
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<BookPage key={page.id} page={page} />
|
||||
<BookPage key={page.id} page={page} scale={0.5} />
|
||||
))}
|
||||
</HTMLFlipBook>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
export type PageElement =
|
||||
| { type: "text"; x: number; y: number; text: string; fontSize: number; color: string }
|
||||
| { type: "image"; x: number; y: number; src: string; width: number; height: number };
|
||||
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||
|
||||
export type PageData = {
|
||||
id: number;
|
||||
width: number;
|
||||
height: number;
|
||||
elements: PageElement[];
|
||||
elements: EditorObject[];
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { EditorObject } from '@/pages/editor/store/editorStore';
|
||||
import type { PageData } from '../types';
|
||||
|
||||
type ViewerDataPage = {
|
||||
id: string;
|
||||
name: string;
|
||||
objects: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
text?: string;
|
||||
fontSize?: number;
|
||||
fontFamily?: string;
|
||||
fontWeight?: string;
|
||||
fill?: string;
|
||||
stroke?: string;
|
||||
strokeWidth?: number;
|
||||
shapeType?: string;
|
||||
imageUrl?: string;
|
||||
videoUrl?: string;
|
||||
linkUrl?: string;
|
||||
rotation?: number;
|
||||
opacity?: number;
|
||||
letterSpacing?: number;
|
||||
tableData?: any;
|
||||
}>;
|
||||
};
|
||||
|
||||
type ViewerData = {
|
||||
pages: ViewerDataPage[];
|
||||
};
|
||||
|
||||
/**
|
||||
* تبدیل دادههای viewer-data.json به فرمت PageData
|
||||
*/
|
||||
export function transformViewerDataToPages(data: ViewerData): PageData[] {
|
||||
return data.pages.map((page, index) => {
|
||||
const objects: EditorObject[] = page.objects.map((obj) => {
|
||||
const baseObject: EditorObject = {
|
||||
id: obj.id,
|
||||
type: obj.type as EditorObject['type'],
|
||||
x: obj.x,
|
||||
y: obj.y,
|
||||
opacity: obj.opacity ?? 1,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
// برای line و arrow، width و height در viewer-data.json به عنوان اندازه هستند
|
||||
// اما در EditorObject به عنوان مختصات انتهایی استفاده میشوند
|
||||
if (obj.type === 'line' || obj.type === 'arrow') {
|
||||
if (obj.width !== undefined) {
|
||||
baseObject.width = obj.x + obj.width;
|
||||
}
|
||||
if (obj.height !== undefined) {
|
||||
// برای line، height ممکن است ضخامت باشد، اما برای arrow طول عمودی است
|
||||
baseObject.height = obj.y + obj.height;
|
||||
}
|
||||
} else {
|
||||
if (obj.width !== undefined) baseObject.width = obj.width;
|
||||
if (obj.height !== undefined) baseObject.height = obj.height;
|
||||
}
|
||||
|
||||
// برای circle در viewer-data.json، x, y به عنوان گوشه بالا-چپ bounding box است
|
||||
// اما در editor، circle با مرکز در x, y ذخیره میشود
|
||||
// پس باید تبدیل کنیم
|
||||
if (obj.type === 'rectangle' && obj.shapeType === 'circle') {
|
||||
// تبدیل از گوشه بالا-چپ به مرکز
|
||||
baseObject.x = obj.x + (obj.width || 100) / 2;
|
||||
baseObject.y = obj.y + (obj.height || 100) / 2;
|
||||
}
|
||||
if (obj.fill !== undefined) baseObject.fill = obj.fill;
|
||||
if (obj.stroke !== undefined) baseObject.stroke = obj.stroke;
|
||||
if (obj.strokeWidth !== undefined) baseObject.strokeWidth = obj.strokeWidth;
|
||||
if (obj.rotation !== undefined) baseObject.rotation = obj.rotation;
|
||||
|
||||
// ویژگیهای متنی
|
||||
if (obj.text !== undefined) baseObject.text = obj.text;
|
||||
if (obj.fontSize !== undefined) baseObject.fontSize = obj.fontSize;
|
||||
if (obj.fontFamily !== undefined) baseObject.fontFamily = obj.fontFamily;
|
||||
if (obj.fontWeight !== undefined) baseObject.fontWeight = obj.fontWeight;
|
||||
if (obj.letterSpacing !== undefined) baseObject.letterSpacing = obj.letterSpacing;
|
||||
|
||||
// انواع خاص objectها
|
||||
if (obj.type === 'rectangle' && obj.shapeType) {
|
||||
baseObject.shapeType = obj.shapeType as EditorObject['shapeType'];
|
||||
}
|
||||
|
||||
if (obj.type === 'image' && obj.imageUrl) {
|
||||
baseObject.imageUrl = obj.imageUrl;
|
||||
}
|
||||
|
||||
if (obj.type === 'video' && obj.videoUrl) {
|
||||
baseObject.videoUrl = obj.videoUrl;
|
||||
}
|
||||
|
||||
if (obj.type === 'link' && obj.linkUrl) {
|
||||
baseObject.linkUrl = obj.linkUrl;
|
||||
}
|
||||
|
||||
if (obj.type === 'grid' && obj.tableData) {
|
||||
baseObject.tableData = obj.tableData;
|
||||
}
|
||||
|
||||
return baseObject;
|
||||
});
|
||||
|
||||
return {
|
||||
id: index + 1,
|
||||
width: 794,
|
||||
height: 1123,
|
||||
elements: objects,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user