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 ( {itemTitle} 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. - +
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) => ( + {`Photo + ))} + + + ); +}; + +function PublicProfile() { + const photos = Array.from({ length: 5 }, (_, i) => ({ + id: i, + url: `https://placehold.co/600x400?text=Photo+${i + 1}`, + })); + + return ( + + {/* Profile Header */} + + Profile + + + 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;