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، صفحه سمت راست) */} + +
+
+ ); +}; + +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 = () => { } /> } /> + } />