Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions front-end/components/BackBookmark.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import PropTypes from "prop-types";

const BackBookmark = ({ label, targetPage, y, onClick, zIndex, color = "#77425f", stroke = "#5a2f49" }) => {
const handleClick = (e) => {
if (onClick) {
onClick(e, targetPage); // send target page up
}
};

return (
<div
className="bookmark-container back-bookmark"
onClick={handleClick}
style={{
top: y,
zIndex: zIndex || 10,
position: "absolute",
right: "auto",
}}
>
<svg viewBox="0 0 300 100" preserveAspectRatio="none">
<polygon
points="300,0 40,0 0,50 40,100 300,100"
fill={color}
stroke={stroke}
strokeWidth="1"
/>
<polygon
points="295,5 45,5 4,50 46,95 295,95"
fill="none"
stroke= "white"
strokeWidth="2"
strokeDasharray="8,6"
/>
<text
x="150"
y="58"
textAnchor="middle"
fill="white"
fontWeight="bold"
fontFamily="sans-serif"
>
{label}
</text>
</svg>
</div>
);
};

BackBookmark.propTypes = {
label: PropTypes.string.isRequired,
targetPage: PropTypes.number.isRequired,
y: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onClick: PropTypes.func,
color: PropTypes.string,
stroke: PropTypes.string,
zIndex: PropTypes.number,
};

export default BackBookmark;
64 changes: 64 additions & 0 deletions front-end/components/Bookmark.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import PropTypes from "prop-types";

const Bookmark = ({ label, targetPage, y, onClick, zIndex, color = "#77425f", stroke = "#5a2f49" }) => {
const handleClick = (e) => {
if (onClick) {
onClick(e, targetPage); // send target page up
}
};

return (
<div
className="bookmark-container bookmark"
onClick={handleClick}
style={{
top: y,
zIndex: zIndex || 10,
position: "absolute",
cursor: "pointer",
}}
>
<svg viewBox="0 0 300 100" preserveAspectRatio="none">
{/* Main body */}
<polygon
points="0,0 260,0 300,50 260,100 0,100"
fill={color}
stroke={stroke}
strokeWidth="1"
/>
{/* Inner dashed border */}
<polygon
points="5,5 255,5 296,50 254,95 5,95"
fill="none"
stroke="white"
strokeWidth="2"
strokeDasharray="8,6"
/>
{/* Text label */}
<text
x="150"
y="58"
textAnchor="middle"
fill="white"
fontWeight="bold"
fontFamily="sans-serif"
>
{label}
</text>
</svg>
</div>
);
};

Bookmark.propTypes = {
label: PropTypes.string.isRequired,
targetPage: PropTypes.number.isRequired,
y: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
onClick: PropTypes.func,
color: PropTypes.string,
stroke: PropTypes.string,
zIndex: PropTypes.number,
};

export default Bookmark;
164 changes: 164 additions & 0 deletions front-end/components/FlipBook.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import React, { useEffect, useRef, useState } from "react";
import FlipPage from "./FlipPage";
import { color } from "framer-motion";

const FlipBook = () => {
const [pages, setPages] = useState([]);
const [zIndices, setZIndices] = useState([]);
const [flippedStates, setFlippedStates] = useState([]);
const [currentPage, setCurrentPage] = useState(0);
const [onFirstPage, setOnFirstPage] = useState(true);
const [onLastPage, setOnLastPage] = useState(false);
const [isTurning, setIsTurning] = useState(false);

const pageRefs = useRef([]);

useEffect(() => {
const pageList = [
{ cover: "green-cover.jpg", frontCover: true },
{ front: "test.txt", back: "test.txt" },
{ front: "test.txt", back: "test.txt" },
{ front: "test.txt", back: "test.txt",
bookmark: { label: "Page 3", targetPage: 3, y: "10%", color: "#3b82f6" }
},
{ front: "test.txt", back: "test.txt" },
{ front: "test.txt", back: "test.txt" },
{ front: "test.txt", back: "test.txt",
bookmark: { label: "Page 6", targetPage: 6, y: "32.5%", color: "#f63b89ff" }
},
{ front: "test.txt", back: "test.txt" },
{ front: "test.txt", back: "test.txt" },
{ front: "test.txt", back: "test.txt",
bookmark: { label: "Page 9", targetPage: 9, y: "55%", color: "#2aa51fff" }
},
{ front: "test.txt", back: "test.txt" },
{ cover: "green-cover.jpg" },
].map((page, index) => ({ ...page, id: index }));

setPages(pageList);

const total = pageList.length;
setZIndices(Array.from({ length: total }, (_, i) => total - i + 1));
setFlippedStates(Array(total).fill(false));

// Initialize refs array
pageRefs.current = Array(total)
.fill()
.map((_, i) => pageRefs.current[i] || React.createRef());
}, []);

const handleFlip = (pageIndex, isFlipped) => {

const updatedFlipped = [...flippedStates];
updatedFlipped[pageIndex] = isFlipped;
setFlippedStates(updatedFlipped);

const maxZ = Math.max(...zIndices);
const newZ = [...zIndices];
newZ[pageIndex] = maxZ + 1;
setZIndices(newZ);

const flippedCount = updatedFlipped.filter(Boolean).length;
setCurrentPage(flippedCount);

checkAllPagesFlipped(updatedFlipped);
};

const checkAllPagesFlipped = (flips) => {
const firstFlipped = flips[0];
const lastFlipped = flips[flips.length - 1];
setOnFirstPage(!firstFlipped);
setOnLastPage(!!lastFlipped);
};

const flipToPage = async (targetPage) => {
if (targetPage === currentPage) return;
if (isTurning) return; // prevent double clicks
setIsTurning(true);

// Forward flip
if (targetPage > currentPage) {
for (let i = currentPage; i < targetPage; i++) {
await new Promise((resolve) => {
setTimeout(() => {
pageRefs.current[i]?.flip();
resolve();
}, 800);
});
}
}
// Backward flip
else {
for (let i = currentPage - 1; i >= targetPage; i--) {
await new Promise((resolve) => {
setTimeout(() => {
pageRefs.current[i]?.flip();
resolve();
}, 800);
});
}
}

setTimeout(() => {
setIsTurning(false);
}, 1000);
};

const handleBookmarkClick = (e, targetPage) => {
e.stopPropagation();
flipToPage(targetPage);
};

// On load intro animation is played — book opens and flips 2 pages + Cover
useEffect(() => {
if (!pages.length) return;

const waitForRefs = async () => {
const total = pages.length;
for (let i = 0; i < total; i++) {
// wait up to a reasonable amount; this loop polls until the ref exists
let tries = 0;
while (!pageRefs.current[i] && tries < 40) { // ~2s max (40 * 50ms)
await new Promise((r) => setTimeout(r, 50));
tries++;
}
}

// Give the browser a frame to paint so initial z / layout are stable
await new Promise((r) => requestAnimationFrame(r));
await new Promise((r) => setTimeout(r, 300)); // small buffer

// Now run the intro flips
await flipToPage(3);
};

waitForRefs();
}, [pages.length]);

return (
<div className="book-frame">
<div className="page-wrapper slideUp-animation">

{pages.map((page, i) => (
<FlipPage
key={i}
ref={(el) => (pageRefs.current[i] = el)} // store ref
Front={page.front}
Back={page.back}
Cover={page.cover}
FrontCover={page.frontCover}
onFlip={(flipped) => handleFlip(i, flipped)}
flipped={flippedStates[i]}
zIndex={zIndices[i]}
bookmark={page.bookmark}
onBookmarkClick={handleBookmarkClick}
onFirstPage={onFirstPage}
onLastPage={onLastPage}
/>
))}
</div>
</div>
);
};

export default FlipBook;
Loading