diff --git a/src/assets/placeholderImage.png b/src/assets/placeholderImage.png
new file mode 100644
index 0000000..a095e05
Binary files /dev/null and b/src/assets/placeholderImage.png differ
diff --git a/src/components/Catalogue/arrowOverlay.jsx b/src/components/Catalogue/arrowOverlay.jsx
index 0551b90..5afc9b7 100644
--- a/src/components/Catalogue/arrowOverlay.jsx
+++ b/src/components/Catalogue/arrowOverlay.jsx
@@ -1,40 +1,51 @@
import { Box, Icon } from "@chakra-ui/react";
import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
+import { motion, AnimatePresence } from "framer-motion";
-const ArrowOverlay = ({ direction, isDisabled, onClick }) => {
+const MotionBox = motion.create(Box);
+
+const ArrowOverlay = ({ direction, isDisabled, onClick, isOverflowing }) => {
const isLeft = direction === "left";
return (
-
-
-
+
+ {isOverflowing && (
+
+
+
+ )}
+
);
};
diff --git a/src/components/Catalogue/cardComponent.jsx b/src/components/Catalogue/cardComponent.jsx
index 53bcd47..aa3b482 100644
--- a/src/components/Catalogue/cardComponent.jsx
+++ b/src/components/Catalogue/cardComponent.jsx
@@ -1,15 +1,20 @@
import { Card, Image, Text, Skeleton } from "@chakra-ui/react";
+import { useState } from "react";
+
+const CardComponent = ({ imageSrc, itemTitle, itemDescription, isSelected, expanded }) => {
+ const [isImageLoaded, setIsImageLoaded] = useState(false);
+
+ const isLoading = !isImageLoaded;
-const CardComponent = ({ imageSrc, itemTitle, itemDescription, isLoading, setIsLoading, isSelected, expanded }) => {
return (
setIsLoading(false)}
- height="180px"
+ loading={expanded ? "eager" : "lazy"}
+ onLoad={() => setIsImageLoaded(true)}
+ height="180px"
width="100%"
/>
diff --git a/src/components/Catalogue/cardItem.jsx b/src/components/Catalogue/cardItem.jsx
index e66c22c..8c61dc2 100644
--- a/src/components/Catalogue/cardItem.jsx
+++ b/src/components/Catalogue/cardItem.jsx
@@ -5,14 +5,7 @@ import CardComponent from "./cardComponent";
const MotionBox = motion.create(Box);
-const images = [
- "https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=1770&q=80",
- "https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=1770&q=80",
- "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=1770&q=80",
- "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1770&q=80",
-];
-
-const CardItem = ({ itemTitle, itemDescription, selectedTitle, index }) => {
+const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc }) => {
const isMobile = useBreakpointValue({ base: true, md: false });
const [isHovered, setIsHovered] = useState(false);
@@ -28,12 +21,6 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, index }) => {
setIsSelected(selectedTitle === itemTitle);
}, [selectedTitle, itemTitle]);
- const imageSrc = images[index % images.length];
-
- useEffect(() => {
- setIsLoading(true);
- }, [imageSrc]);
-
const updateHoverPosition = () => {
const rect = cardRef.current?.getBoundingClientRect();
if (rect) {
@@ -104,6 +91,7 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, index }) => {
return (
<>
+ {/* Base Card Container */}
{
bg={isSelected ? "gray.100" : "white"}
borderRadius="md"
boxShadow={isSelected ? "lg" : "md"}
+ zIndex={isHovered ? 30 : "auto"} // Conditional z-index
>
@@ -141,22 +129,19 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, index }) => {
position: "fixed",
top: hoverPos.top,
left: hoverPos.left + 15,
- zIndex: 10,
+ zIndex: 30, // Always high z-index for expanded card
pointerEvents: "auto",
width: "280px",
}}
onMouseLeave={handleMouseLeave}
onMouseEnter={() => setIsHovered(true)}
borderRadius="md"
- border={isSelected ? "1px solid" : "none"}
- borderColor={isSelected ? "blue.600" : "transparent"}
- boxShadow={isSelected ? "0 0 15px 2px rgba(66,153,225,0.6)" : "2xl"}
+ boxShadow="2xl"
>
@@ -168,4 +153,4 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, index }) => {
);
};
-export default CardItem;
+export default CardItem;
\ No newline at end of file
diff --git a/src/components/Catalogue/catalogueItemView.jsx b/src/components/Catalogue/catalogueItemView.jsx
index 399affa..62025c0 100644
--- a/src/components/Catalogue/catalogueItemView.jsx
+++ b/src/components/Catalogue/catalogueItemView.jsx
@@ -72,7 +72,8 @@ const CatalogueItemView = ({ isOpen, onClose, title, items, setDialogTitle, imag
useEffect(() => {
const handleKeyDown = (e) => {
if (!isOpen || isNavigating) return;
- if (e.key === "ArrowLeft" && currentIndex > 0) prevItem();
+ if (e.key === "Escape") {onClose()}
+ else if (e.key === "ArrowLeft" && currentIndex > 0) prevItem();
else if (e.key === "ArrowRight" && currentIndex < items.length - 1) nextItem();
};
window.addEventListener("keydown", handleKeyDown);
diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx
index d7a40d4..548d8ca 100644
--- a/src/components/Catalogue/mmSection.jsx
+++ b/src/components/Catalogue/mmSection.jsx
@@ -1,17 +1,11 @@
-import { Box, Table, Text, useBreakpointValue } from "@chakra-ui/react";
-import { useRef, useState, useEffect } from "react";
+import { useEffect, useState, useRef } from "react";
+import { Box, Text, Flex, useBreakpointValue, Image } from "@chakra-ui/react";
import CardItem from "./cardItem.jsx";
import ArrowOverlay from "./arrowOverlay.jsx";
+import placeholderImage from "../../assets/placeholderImage.png"
-const MMSection = ({ onItemClick, selectedTitle }) => {
+const MMSection = ({ books = [], onItemClick, selectedTitle }) => {
const isMobile = useBreakpointValue({ base: true, md: false });
-
- const items = Array.from({ length: 15 }).map((_, idx) => ({
- id: idx + 1,
- title: `Meeting ${idx + 1}`,
- description: `Description for meeting ${idx + 1}`,
- }));
-
const scrollRef = useRef(null);
const [atStart, setAtStart] = useState(true);
const [atEnd, setAtEnd] = useState(false);
@@ -20,9 +14,12 @@ const MMSection = ({ onItemClick, selectedTitle }) => {
const node = scrollRef.current;
if (!node) return;
setAtStart(node.scrollLeft === 0);
- setAtEnd(Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth);
+ setAtEnd(
+ Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth
+ );
};
+ // Scroll detection on mount
useEffect(() => {
const node = scrollRef.current;
if (!node) return;
@@ -51,48 +48,80 @@ const MMSection = ({ onItemClick, selectedTitle }) => {
};
return (
-
-
+
+
Meeting Minutes
{!isMobile && (
<>
- scroll("left")} />
- scroll("right")} />
+ scroll("left")}
+ isOverflowing={!atStart}
+ />
+ scroll("right")}
+ isOverflowing={!atEnd}
+ />
>
)}
-
-
-
-
-
- {items.map((item, index) => (
-
- handleClick(item.title)}
- cursor="pointer"
- borderRadius="md"
- >
-
-
-
- ))}
-
-
-
-
-
+
+ {books.map((book) => {
+ const firstImage = book.artefacts?.[0]?.id;
+ const imageSrc = firstImage
+ ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${firstImage}`
+ : placeholderImage;
+
+ return (
+ handleClick(book.title)}
+ >
+ {/* Hidden Image for preloading */}
+
+
+
+
+ );
+ })}
+
);
};
diff --git a/src/components/Catalogue/profileCard.jsx b/src/components/Catalogue/profileCard.jsx
new file mode 100644
index 0000000..a0aab52
--- /dev/null
+++ b/src/components/Catalogue/profileCard.jsx
@@ -0,0 +1,33 @@
+import { Box, Image, Text } from "@chakra-ui/react";
+import { useNavigate } from "react-router-dom";
+
+function ProfileCard() {
+ const navigate = useNavigate();
+
+ const handleProfileCardClick = () => {
+ navigate("/publicProfile");
+ };
+
+ return (
+
+
+ Name
+
+ );
+}
+
+export default ProfileCard;
diff --git a/src/components/Catalogue/profileSection.jsx b/src/components/Catalogue/profileSection.jsx
new file mode 100644
index 0000000..e75d3b8
--- /dev/null
+++ b/src/components/Catalogue/profileSection.jsx
@@ -0,0 +1,97 @@
+import { Box, Text, Flex, useBreakpointValue } from "@chakra-ui/react";
+import { useRef, useEffect, useState } from "react";
+import ArrowOverlay from "./arrowOverlay";
+import ProfileCard from "./profileCard";
+
+function ProfileSection() {
+ const count = 17;
+ const scrollRef = useRef(null);
+ const isMobile = useBreakpointValue({ base: true, md: false });
+
+ const [showLeftArrow, setShowLeftArrow] = useState(false);
+ const [showRightArrow, setShowRightArrow] = useState(false);
+
+ const checkOverflow = () => {
+ const el = scrollRef.current;
+ if (!el) return;
+
+ setShowLeftArrow(el.scrollLeft > 0);
+ setShowRightArrow(
+ Math.ceil(el.scrollLeft + el.clientWidth) < el.scrollWidth
+ );
+ };
+
+ const handleScroll = (direction) => {
+ const el = scrollRef.current;
+ if (!el) return;
+
+ const scrollAmount = el.clientWidth * 0.8;
+ el.scrollBy({
+ left: direction === "left" ? -scrollAmount : scrollAmount,
+ behavior: "smooth",
+ });
+ };
+
+ useEffect(() => {
+ const el = scrollRef.current;
+ if (!el) return;
+
+ checkOverflow();
+
+ el.addEventListener("scroll", checkOverflow);
+ window.addEventListener("resize", checkOverflow);
+
+ return () => {
+ el.removeEventListener("scroll", checkOverflow);
+ window.removeEventListener("resize", checkOverflow);
+ };
+ }, []);
+
+ return (
+
+
+ Key People
+
+
+ {!isMobile && (
+ <>
+ handleScroll("left")}
+ isOverflowing={showLeftArrow}
+ />
+ handleScroll("right")}
+ isOverflowing={showRightArrow}
+ />
+ >
+ )}
+
+
+ {Array.from({ length: count }).map((_, index) => (
+
+
+
+ ))}
+
+
+ );
+}
+
+export default ProfileSection;
diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx
index fb11478..d221dc7 100644
--- a/src/components/Catalogue/section.jsx
+++ b/src/components/Catalogue/section.jsx
@@ -1,25 +1,20 @@
-import { Box, Table, Text, Icon, useBreakpointValue } from "@chakra-ui/react";
+import { Box, Text, Flex, useBreakpointValue, Image } from "@chakra-ui/react";
import { useRef, useState, useEffect } from "react";
-import { FaChevronLeft, FaChevronRight } from "react-icons/fa";
import CardItem from "./cardItem.jsx";
import CatalogueItemView from "./catalogueItemView.jsx";
import ArrowOverlay from "./arrowOverlay.jsx";
+import placeholderImage from "../../assets/placeholderImage.png"
-const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => {
+const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) => {
const isMobile = useBreakpointValue({ base: true, md: false });
- const images = [
- "https://images.unsplash.com/photo-1555041469-a586c61ea9bc?auto=format&fit=crop&w=1770&q=80",
- "https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=1770&q=80",
- "https://images.unsplash.com/photo-1506744038136-46273834b3fb?auto=format&fit=crop&w=1770&q=80",
- "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?auto=format&fit=crop&w=1770&q=80",
- ];
-
- const items = Array.from({ length: secLen }).map((_, idx) => ({
- id: idx + 1,
- title: `Photo ${idx + 1}`,
- description: `Description for photo ${idx + 1}`,
- imageSrc: images[idx % images.length],
+ const items = artefacts.map((art) => ({
+ id: art.id,
+ title: art.name,
+ description: art.description,
+ imageSrc: art.id
+ ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.id}`
+ : placeholderImage,
}));
const scrollRef = useRef(null);
@@ -31,12 +26,22 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => {
const checkScrollEdges = () => {
const node = scrollRef.current;
if (!node) return;
+
setAtStart(node.scrollLeft === 0);
setAtEnd(
Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth
);
};
+ const scroll = (direction) => {
+ if (!scrollRef.current) return;
+ const scrollAmount = 1200;
+ scrollRef.current.scrollBy({
+ left: direction === "left" ? -scrollAmount : scrollAmount,
+ behavior: "smooth",
+ });
+ };
+
const handleCardClick = (title) => {
setDialogTitle(title);
setDialogOpen(true);
@@ -57,20 +62,17 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => {
};
}, []);
- const scroll = (direction) => {
- if (!scrollRef.current) return;
- const scrollAmount = 1200;
- scrollRef.current.scrollBy({
- left: direction === "left" ? -scrollAmount : scrollAmount,
- behavior: "smooth",
- });
- };
-
const currentItem = items.find((item) => item.title === dialogTitle);
return (
-
-
+
+
{sectionTitle}
@@ -80,45 +82,61 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => {
direction="left"
isDisabled={atStart}
onClick={() => scroll("left")}
+ isOverflowing={!atStart}
/>
scroll("right")}
+ isOverflowing={!atEnd}
/>
>
)}
-
-
-
-
-
- {items.map((item, index) => (
-
- handleCardClick(item.title)}
- >
-
-
-
- ))}
-
-
-
-
-
+
+ {items.length === 0 ? (
+
+ No artefacts in this section.
+
+ ) : (
+ items.map((item) => (
+ handleCardClick(item.title)}
+ >
+
+
+
+ ))
+ )}
+
}>
}>
} />
+ } />
+ } />
diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx
index 07dc198..3a64487 100644
--- a/src/pages/Catalogue.jsx
+++ b/src/pages/Catalogue.jsx
@@ -1,17 +1,45 @@
-import { Box, Button, Card, Image, Text } from "@chakra-ui/react";
+import { useEffect, useState } from "react";
+import { Box, Text, Center } from "@chakra-ui/react";
import { AnimatePresence, motion } from "framer-motion";
-import { useState } from "react";
-import { useSelector } from "react-redux";
-import server from "../networking";
import MMSection from "../components/Catalogue/mmSection.jsx";
import Section from "../components/Catalogue/section.jsx";
+import CentredSpinner from "../components/centredSpinner.jsx";
+import server, { JSONResponse } from '../networking'
+import ToastWizard from "../components/toastWizard.js";
function Catalogue() {
- const sectionTitle = "Event photos";
- const secLenValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 110];
-
const [selectedMMTitle, setSelectedMMTitle] = useState(null);
const [showDetails, setShowDetails] = useState(false);
+ const [books, setBooks] = useState([]);
+ const [categories, setCategories] = useState({});
+ const [loading, setLoading] = useState(true);
+
+ async function fetchCatalogue() {
+ try {
+ const res = await server.get("/cdn/catalogue");
+ const data = res.data;
+
+ if (!(data instanceof JSONResponse) || data.isErrorStatus()) {
+ ToastWizard.standard("error", "An error occurred", "Please try again later");
+ return;
+ }
+
+ const { categories = {}, books = [] } = data.raw.data || {};
+ setCategories(categories);
+ setBooks(books.map(book => ({
+ ...book,
+ artefacts: book.mmArtefacts || [],
+ })));
+ } catch (err) {
+ ToastWizard.standard("error", "An error occurred", "Please try again later");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ fetchCatalogue();
+ }, []);
const handleItemClick = (title, source) => {
if (source === "MM") {
@@ -23,12 +51,35 @@ function Catalogue() {
}
};
+ const selectedBook = books.find((book) => book.title === selectedMMTitle);
+ const selectedBookArtefacts = selectedBook?.artefacts || [];
+
+ const hasBooks = books.length > 0;
+ const hasCategories = Object.keys(categories).length > 0;
+
+ if (loading) {
+ return ;
+ }
+
+ if (!hasBooks && !hasCategories) {
+ return (
+
+
+ No Items Uploaded
+
+
+ );
+ }
+
return (
<>
- handleItemClick(title, "MM")}
- selectedTitle={selectedMMTitle}
- />
+ {hasBooks && (
+ handleItemClick(title, "MM")}
+ selectedTitle={selectedMMTitle}
+ books={books}
+ />
+ )}
{showDetails && selectedMMTitle && (
@@ -43,23 +94,23 @@ function Catalogue() {
)}
- {secLenValues.map((val) => {
- const key = `${sectionTitle} ${val}`;
- return (
+ {hasCategories &&
+ Object.entries(categories).map(([groupName, artefacts]) => (
- );
- })}
+ ))}
+
+
>
);
}
diff --git a/src/pages/Homepage.jsx b/src/pages/Homepage.jsx
index f11f44f..73b5021 100644
--- a/src/pages/Homepage.jsx
+++ b/src/pages/Homepage.jsx
@@ -1,4 +1,6 @@
import { Box, Button, Flex, Image, Spacer, Text, useMediaQuery, VStack } from '@chakra-ui/react'
+import { useSelector } from 'react-redux';
+import { useNavigate } from 'react-router-dom';
import hp1 from '../assets/hp1.png';
import hp2 from '../assets/hp2.png';
import hp3 from '../assets/hp3.png';
@@ -13,6 +15,17 @@ function Homepage() {
const imgColumnMinWidth = { base: "350px", lg: "400px", xl: "450px" }
const imgOffset = "-100px"
+ const navigate = useNavigate();
+ const { username } = useSelector(state => state.auth);
+
+ const handleGetStarted = () => {
+ if (username) {
+ navigate('/catalogue');
+ } else {
+ navigate('/auth/login');
+ }
+ };
+
return
@@ -54,7 +67,7 @@ function Homepage() {
Connect with your roots
The intelligent artefact digitisation platform.
- Get Started
+ Get Started
diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx
new file mode 100644
index 0000000..de291d6
--- /dev/null
+++ b/src/pages/PublicGallery.jsx
@@ -0,0 +1,72 @@
+import { Box, Text, Center } from "@chakra-ui/react";
+import { useEffect, useState } from "react";
+import server, { JSONResponse } from '../networking'
+import Section from "../components/Catalogue/section.jsx";
+import ProfileSection from "../components/Catalogue/ProfileSection.jsx";
+import CentredSpinner from "../components/centredSpinner.jsx";
+
+function PublicGallery() {
+ const [categories, setCategories] = useState({});
+ const [loading, setLoading] = useState(true);
+
+ async function fetchCatalogue() {
+ try {
+ const res = await server.get("/cdn/catalogue");
+ const data = res.data;
+
+ if (!(data instanceof JSONResponse) || data.isErrorStatus()) {
+ ToastWizard.standard("error", "An error occurred", "Please try again later");
+ return;
+ }
+
+ const { categories = {}, books = [] } = data.raw.data || {};
+ setCategories(categories);
+ setBooks(books.map(book => ({
+ ...book,
+ artefacts: book.mmArtefacts || [],
+ })));
+ } catch (err) {
+ ToastWizard.standard("error", "An error occurred", "Please try again later");
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ useEffect(() => {
+ fetchCatalogue();
+ }, []);
+
+ const hasCategories = Object.keys(categories).length > 0;
+
+ if (loading) {
+ return ;
+ }
+
+ if (!hasCategories) {
+ return (
+
+
+ No Items Uploaded
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+ {Object.entries(categories).map(([groupName, artefacts]) => (
+
+ ))}
+
+
+ >
+ );
+}
+
+export default PublicGallery;
diff --git a/src/pages/PublicProfile.jsx b/src/pages/PublicProfile.jsx
new file mode 100644
index 0000000..91c3dfc
--- /dev/null
+++ b/src/pages/PublicProfile.jsx
@@ -0,0 +1,114 @@
+import { Box, Text, Flex, Image } from "@chakra-ui/react";
+
+const PhotoGallery = ({ title, items }) => {
+ return (
+
+
+ {title}
+
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+ );
+};
+
+function PublicProfile() {
+ const photos = Array.from({ length: 5 }, (_, i) => ({
+ id: i,
+ url: `https://placehold.co/600x400?text=Photo+${i + 1}`,
+ }));
+
+ return (
+
+ {/* Profile Header */}
+
+
+
+
+ Kho Choon Keng
+
+
+
+ Leader of the Singapore Chinese Chamber Commerce and Industry
+
+
+
+
+ President of SCCCI
+
+
+ Lian Huat Group
+
+
+ Executive Chairman
+
+
+
+
+ {/* Photo Gallery */}
+
+
+
+
+ {/* Description Section */}
+
+
+ Mr. Kho graduated with First Class Honours in BSc (Engineering) from King’s College, University of London.
+ He was a recipient of the prestigious Singapore President’s Scholarship and served in the Singapore Civil Service
+ before joining Lian Huat Group in 1985.
+ At Lian Huat Group, Mr. Kho spearheaded efforts to modernize and expand the Group’s operations. He brought a strategic
+ long-term vision while preserving the rich values of traditional Chinese ethics and culture. Under his leadership,
+ the Group evolved into an internationally oriented organization, blending modern practices with cultural depth.
+
+
+
+ );
+}
+
+export default PublicProfile;