base flip

This commit is contained in:
hamid zarghami
2026-01-01 12:02:24 +03:30
parent 97f70eefae
commit 84f93463a3
11 changed files with 479 additions and 17 deletions
+13
View File
@@ -0,0 +1,13 @@
import { type FC } from 'react';
import BookViewer from './components/BookViewer';
import { mockPages } from './components/mockData';
const Viewer: FC = () => {
return (
<div className="w-full h-full">
<BookViewer pages={mockPages} />
</div>
);
};
export default Viewer;
+78
View File
@@ -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<HTMLDivElement, BookPageProps>(({ page }, ref) => {
return (
<div
ref={ref}
className="page"
data-density="soft"
style={{
position: 'relative',
backgroundColor: '#ffffff',
overflow: 'hidden',
padding: '40px',
boxSizing: 'border-box',
width: '100%',
height: '100%',
}}
>
{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;
})}
</div>
);
});
BookPage.displayName = 'BookPage';
export default BookPage;
+125
View File
@@ -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<BookViewerProps> = ({ pages }) => {
const bookRef = useRef<FlipBookInstance | null>(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 (
<div className="flex flex-col items-center gap-4 md:gap-6 w-full h-full" dir="rtl">
{/* نمایش کتاب */}
<div className="relative w-full flex justify-center flex-1 items-center">
{/* Border و Shadow عمودی در وسط کتاب */}
<div
className="absolute inset-y-0 left-1/2 -translate-x-1/2 w-px border-r border-gray-300 pointer-events-none z-50"
style={{
boxShadow: '-5px 0 15px rgba(0, 0, 0, 0.15), 5px 0 15px rgba(0, 0, 0, 0.15)'
}}
/>
<HTMLFlipBook
ref={bookRef}
width={794}
height={1123}
size="stretch"
minWidth={315}
minHeight={400}
maxWidth={1000}
maxHeight={1533}
drawShadow={true}
flippingTime={800}
usePortrait={true}
startPage={0}
autoSize={true}
maxShadowOpacity={0.5}
showCover={true}
mobileScrollSupport={true}
clickEventForward={true}
useMouseEvents={true}
swipeDistance={30}
startZIndex={1}
showPageCorners={true}
disableFlipByClick={false}
className="flipbook-container mx-auto"
style={{
direction: 'rtl',
}}
onFlip={handlePageFlip}
>
{pages.map((page) => (
<BookPage key={page.id} page={page} />
))}
</HTMLFlipBook>
</div>
{/* کنترل‌های ناوبری */}
<div className="flex items-center gap-3 md:gap-6 bg-white rounded-full px-4 md:px-6 py-2 md:py-3 shadow-lg">
{/* دکمه صفحه بعد (در RTL، صفحه سمت چپ) */}
<button
onClick={goToNextPage}
disabled={currentPage <= 0}
className="p-2 md:p-2 rounded-full hover:bg-gray-100 disabled:opacity-30 disabled:cursor-not-allowed transition-all"
aria-label="صفحه بعد"
>
<ArrowLeft2 size={20} className="text-gray-700 md:w-6 md:h-6" />
</button>
{/* شماره صفحه */}
<div className="flex items-center gap-2 min-w-[100px] md:min-w-[120px] justify-center">
<span className="text-xs md:text-sm text-gray-600">
صفحه {currentPage + 1} از {pages.length}
</span>
</div>
{/* دکمه صفحه قبل (در RTL، صفحه سمت راست) */}
<button
onClick={goToPrevPage}
disabled={currentPage >= 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="صفحه قبل"
>
<ArrowRight2 size={20} className="text-gray-700 md:w-6 md:h-6" />
</button>
</div>
</div>
);
};
export default BookViewer;
+179
View File
@@ -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',
},
],
},
];
+11
View File
@@ -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[];
};
+19
View File
@@ -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> | Element[]): void;
flipNext(corner?: 'top' | 'bottom'): void;
flipPrev(corner?: 'top' | 'bottom'): void;
destroy(): void;
}
}