diff --git a/package-lock.json b/package-lock.json
index 8bbfdff..d3c5cc5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,11 +15,13 @@
"i18next": "^25.6.2",
"iconsax-react": "^0.0.8",
"konva": "^10.0.9",
+ "page-flip": "^2.0.7",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-dropzone": "^14.3.8",
"react-i18next": "^16.3.0",
"react-konva": "^19.2.0",
+ "react-pageflip": "^2.0.3",
"react-router-dom": "^7.9.5",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
@@ -73,7 +75,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1847,7 +1848,6 @@
"integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1857,7 +1857,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -1868,7 +1867,6 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -1928,7 +1926,6 @@
"integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.4",
"@typescript-eslint/types": "8.46.4",
@@ -2181,7 +2178,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2319,7 +2315,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.25",
"caniuse-lite": "^1.0.30001754",
@@ -2591,7 +2586,6 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3001,7 +2995,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -3227,8 +3220,7 @@
"url": "https://github.com/sponsors/lavrton"
}
],
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/levn": {
"version": "0.4.1",
@@ -3682,6 +3674,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/page-flip": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/page-flip/-/page-flip-2.0.7.tgz",
+ "integrity": "sha512-96lQFUUz7r/LZzEUZJ3yBIMEKU9+m8HMFDzTvTdD6P7Ag/wXINjp9n0W7b4wanwnDbQETo4uNUoL3zMqpFxwGA==",
+ "license": "MIT"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3819,7 +3817,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3829,7 +3826,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3918,6 +3914,15 @@
"react-dom": "^19.2.0"
}
},
+ "node_modules/react-pageflip": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/react-pageflip/-/react-pageflip-2.0.3.tgz",
+ "integrity": "sha512-k81mHhRvUM52y8jyzTCh5t4O0lepkLhp+XGSUzq2C3uD+iW99Cv0jfRlqFCjZbD5N3jKkIFr7/3giucoXKDP3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "page-flip": "latest"
+ }
+ },
"node_modules/react-reconciler": {
"version": "0.33.0",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.33.0.tgz",
@@ -4214,7 +4219,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -4273,7 +4277,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4378,7 +4381,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
"integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -4470,7 +4472,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
diff --git a/package.json b/package.json
index a0df1bf..aee8892 100644
--- a/package.json
+++ b/package.json
@@ -17,11 +17,13 @@
"i18next": "^25.6.2",
"iconsax-react": "^0.0.8",
"konva": "^10.0.9",
+ "page-flip": "^2.0.7",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-dropzone": "^14.3.8",
"react-i18next": "^16.3.0",
"react-konva": "^19.2.0",
+ "react-pageflip": "^2.0.3",
"react-router-dom": "^7.9.5",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.17",
diff --git a/src/config/Paths.ts b/src/config/Paths.ts
index 4015b07..d696c76 100644
--- a/src/config/Paths.ts
+++ b/src/config/Paths.ts
@@ -1,4 +1,5 @@
export const Paths = {
home: "/",
editor: "/editor",
+ viewer: "/viewer",
};
diff --git a/src/index.css b/src/index.css
index 79228d0..c30e4c2 100644
--- a/src/index.css
+++ b/src/index.css
@@ -89,3 +89,34 @@ html {
.filterBlack {
filter: brightness(0) invert(0);
}
+
+/* استایل برای صفحات کتاب - بکگراند سفید */
+.stf__item.page,
+.stf__item[class*="page"],
+.page.stf__item,
+div.stf__item.page,
+.stf__block .stf__item,
+.stf__parent .stf__item,
+.stf__wrapper .stf__item {
+ background-color: #ffffff !important;
+ background: #ffffff !important;
+}
+
+/* برای اطمینان بیشتر - تمام صفحات با کلاس page */
+.page {
+ background-color: #ffffff !important;
+ background: #ffffff !important;
+}
+
+/* استایل برای ظاهر کتاب */
+.flipbook-container {
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+/* استایل برای صفحات داخل flipbook */
+.flipbook-container .stf__item,
+.flipbook-container .page {
+ border-radius: 0;
+}
diff --git a/src/pages/viewer/Viewer.tsx b/src/pages/viewer/Viewer.tsx
new file mode 100644
index 0000000..c6cf52b
--- /dev/null
+++ b/src/pages/viewer/Viewer.tsx
@@ -0,0 +1,13 @@
+import { type FC } from 'react';
+import BookViewer from './components/BookViewer';
+import { mockPages } from './components/mockData';
+
+const Viewer: FC = () => {
+ return (
+
+
+
+ );
+};
+
+export default Viewer;
\ No newline at end of file
diff --git a/src/pages/viewer/components/BookPage.tsx b/src/pages/viewer/components/BookPage.tsx
new file mode 100644
index 0000000..c1c0262
--- /dev/null
+++ b/src/pages/viewer/components/BookPage.tsx
@@ -0,0 +1,78 @@
+import { forwardRef } from 'react';
+import { type PageData } from '../types';
+
+type BookPageProps = {
+ page: PageData;
+};
+
+/**
+ * کامپوننت خالص برای نمایش محتوای یک صفحه کتاب
+ * تمام المنتها با absolute positioning رندر میشوند
+ * از HTML استفاده میشود (نه Canvas) برای سازگاری با Konva در آینده
+ * این کامپوننت به عنوان child برای HTMLFlipBook استفاده میشود
+ * نیاز به forwardRef دارد تا react-pageflip بتواند ref را مدیریت کند
+ */
+const BookPage = forwardRef(({ page }, ref) => {
+ return (
+
+ {page.elements.map((element, index) => {
+ if (element.type === 'text') {
+ return (
+
+ {element.text}
+
+ );
+ }
+
+ if (element.type === 'image') {
+ return (
+
+ );
+ }
+
+ return null;
+ })}
+
+ );
+});
+
+BookPage.displayName = 'BookPage';
+
+export default BookPage;
+
diff --git a/src/pages/viewer/components/BookViewer.tsx b/src/pages/viewer/components/BookViewer.tsx
new file mode 100644
index 0000000..5555917
--- /dev/null
+++ b/src/pages/viewer/components/BookViewer.tsx
@@ -0,0 +1,125 @@
+import { type FC, useRef, useCallback, useState } from 'react';
+import HTMLFlipBook from 'react-pageflip';
+import { type PageData } from '../types';
+import BookPage from './BookPage';
+import { ArrowLeft2, ArrowRight2 } from 'iconsax-react';
+
+type BookViewerProps = {
+ pages: PageData[];
+};
+
+interface PageFlipAPI {
+ flipNext: () => void;
+ flipPrev: () => void;
+}
+
+interface FlipBookInstance {
+ pageFlip: () => PageFlipAPI | undefined;
+}
+
+interface FlipEvent {
+ data: number;
+}
+
+/**
+ * کامپوننت اصلی کتابخوان با استفاده از react-pageflip
+ * این کامپوننت از HTMLFlipBook استفاده میکند که یک wrapper برای PageFlip است
+ * از HTML برای رندر صفحات استفاده میشود (نه Canvas) برای سازگاری با Konva در آینده
+ */
+const BookViewer: FC = ({ pages }) => {
+ const bookRef = useRef(null);
+ const [currentPage, setCurrentPage] = useState(0);
+
+ const handlePageFlip = useCallback((e: FlipEvent) => {
+ const pageNum = e.data;
+ setCurrentPage(pageNum);
+ }, []);
+
+ const goToNextPage = useCallback(() => {
+ bookRef.current?.pageFlip()?.flipNext();
+ }, []);
+
+ const goToPrevPage = useCallback(() => {
+ bookRef.current?.pageFlip()?.flipPrev();
+ }, []);
+
+ return (
+
+ {/* نمایش کتاب */}
+
+ {/* Border و Shadow عمودی در وسط کتاب */}
+
+
+ {pages.map((page) => (
+
+ ))}
+
+
+
+ {/* کنترلهای ناوبری */}
+
+ {/* دکمه صفحه بعد (در RTL، صفحه سمت چپ) */}
+
+
+
+
+ {/* شماره صفحه */}
+
+
+ صفحه {currentPage + 1} از {pages.length}
+
+
+
+ {/* دکمه صفحه قبل (در RTL، صفحه سمت راست) */}
+
= pages.length - 1}
+ className="p-2 md:p-2 rounded-full hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
+ aria-label="صفحه قبل"
+ >
+
+
+
+
+ );
+};
+
+export default BookViewer;
diff --git a/src/pages/viewer/components/mockData.ts b/src/pages/viewer/components/mockData.ts
new file mode 100644
index 0000000..70e8732
--- /dev/null
+++ b/src/pages/viewer/components/mockData.ts
@@ -0,0 +1,179 @@
+import { type PageData } from '../types';
+
+/**
+ * دادههای mock برای 6 صفحه کتاب
+ * این ساختار داده برای سازگاری با Konva در آینده طراحی شده است
+ */
+export const mockPages: PageData[] = [
+ {
+ id: 1,
+ width: 794,
+ height: 1123,
+ elements: [
+ {
+ type: 'text',
+ x: 100,
+ y: 150,
+ text: 'صفحه اول\nخوش آمدید',
+ fontSize: 48,
+ color: '#1a1a1a',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 250,
+ text: 'این یک کتاب نمایشی است\nبرای تست کتابخوان',
+ fontSize: 24,
+ color: '#333333',
+ },
+ ],
+ },
+ {
+ id: 2,
+ width: 794,
+ height: 1123,
+ elements: [
+ {
+ type: 'text',
+ x: 100,
+ y: 100,
+ text: 'فصل اول: مقدمه',
+ fontSize: 36,
+ color: '#000000',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 180,
+ text: 'این یک متن نمونه است که در صفحه دوم نمایش داده میشود.\nمتنهای بیشتر برای نمایش بهتر صفحه.',
+ fontSize: 20,
+ color: '#2c2c2c',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 320,
+ text: 'محتوا در اینجا ادامه دارد...',
+ fontSize: 18,
+ color: '#444444',
+ },
+ ],
+ },
+ {
+ id: 3,
+ width: 794,
+ height: 1123,
+ elements: [
+ {
+ type: 'text',
+ x: 100,
+ y: 100,
+ text: 'صفحه سوم',
+ fontSize: 32,
+ color: '#1a1a1a',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 200,
+ text: 'این صفحه شامل محتوای بیشتری است.\n\nمیتوانید متنهای مختلفی در اینجا اضافه کنید.',
+ fontSize: 22,
+ color: '#333333',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 400,
+ text: 'نکته: این یک نمایش نمونه است',
+ fontSize: 18,
+ color: '#666666',
+ },
+ ],
+ },
+ {
+ id: 4,
+ width: 794,
+ height: 1123,
+ elements: [
+ {
+ type: 'text',
+ x: 100,
+ y: 150,
+ text: 'فصل دوم: ادامه',
+ fontSize: 36,
+ color: '#000000',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 250,
+ text: 'در این بخش محتوای بیشتری اضافه میشود.\n\nاین کتاب نمونه برای نمایش قابلیتهای کتابخوان طراحی شده است.',
+ fontSize: 20,
+ color: '#2c2c2c',
+ },
+ ],
+ },
+ {
+ id: 5,
+ width: 794,
+ height: 1123,
+ elements: [
+ {
+ type: 'text',
+ x: 100,
+ y: 120,
+ text: 'صفحه پنجم',
+ fontSize: 40,
+ color: '#1a1a1a',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 220,
+ text: 'محتوا در صفحات مختلف توزیع شده است.\n\nهر صفحه میتواند شامل متن و تصویر باشد.',
+ fontSize: 22,
+ color: '#333333',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 400,
+ text: 'ادامه در صفحه بعد...',
+ fontSize: 18,
+ color: '#666666',
+ },
+ ],
+ },
+ {
+ id: 6,
+ width: 794,
+ height: 1123,
+ elements: [
+ {
+ type: 'text',
+ x: 100,
+ y: 200,
+ text: 'صفحه آخر',
+ fontSize: 36,
+ color: '#1a1a1a',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 300,
+ text: 'این آخرین صفحه کتاب است.\n\nاز کتابخوان استفاده کرده و صفحات را ورق بزنید!',
+ fontSize: 24,
+ color: '#333333',
+ },
+ {
+ type: 'text',
+ x: 100,
+ y: 500,
+ text: 'پایان',
+ fontSize: 32,
+ color: '#000000',
+ },
+ ],
+ },
+];
+
diff --git a/src/pages/viewer/types/index.ts b/src/pages/viewer/types/index.ts
new file mode 100644
index 0000000..dd0e942
--- /dev/null
+++ b/src/pages/viewer/types/index.ts
@@ -0,0 +1,11 @@
+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 };
+
+export type PageData = {
+ id: number;
+ width: number;
+ height: number;
+ elements: PageElement[];
+};
+
diff --git a/src/pages/viewer/types/page-flip.d.ts b/src/pages/viewer/types/page-flip.d.ts
new file mode 100644
index 0000000..92a74b3
--- /dev/null
+++ b/src/pages/viewer/types/page-flip.d.ts
@@ -0,0 +1,19 @@
+declare module 'page-flip' {
+ export interface PageFlipSettings {
+ width: number;
+ height: number;
+ size?: 'fixed' | 'stretch';
+ showCover?: boolean;
+ maxShadowOpacity?: number;
+ mobileScrollSupport?: boolean;
+ }
+
+ export class PageFlip {
+ constructor(block: HTMLElement, setting: PageFlipSettings);
+ loadFromHTML(pages: NodeListOf | Element[]): void;
+ flipNext(corner?: 'top' | 'bottom'): void;
+ flipPrev(corner?: 'top' | 'bottom'): void;
+ destroy(): void;
+ }
+}
+
diff --git a/src/router/MainRouter.tsx b/src/router/MainRouter.tsx
index f1b57bf..725d8aa 100644
--- a/src/router/MainRouter.tsx
+++ b/src/router/MainRouter.tsx
@@ -6,6 +6,7 @@ import { clx } from '@/helpers/utils'
import { useSharedStore } from '@/shared/store/sharedStore'
import Home from '@/pages/home/Home'
import Editor from '@/pages/editor/Editor'
+import Viewer from '@/pages/viewer/Viewer'
const MainRouter = () => {
const { hasSubMenu } = useSharedStore()
@@ -31,6 +32,7 @@ const MainRouter = () => {
} />
} />
+ } />