From 8f7c6e8c233ba40913b9a48716003cdbcaa842b0 Mon Sep 17 00:00:00 2001 From: JunHam Date: Mon, 14 Jul 2025 11:46:37 +0800 Subject: [PATCH 01/20] Added escape key function to catalogueItemView, added public gallery page --- .../Catalogue/catalogueItemView.jsx | 3 ++- src/main.jsx | 2 ++ src/pages/PublicGallery.jsx | 24 +++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/pages/PublicGallery.jsx 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/main.jsx b/src/main.jsx index 32e7b94..2c72464 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -19,6 +19,7 @@ import DefaultLayout from './DefaultLayout.jsx'; import SampleProtected from './pages/SampleProtected.jsx'; import ProtectedLayout from './ProtectedLayout.jsx'; import AnimateIn from './AnimateIn.jsx'; +import PublicGallery from './pages/PublicGallery.jsx'; const store = configureStore({ reducer: { @@ -57,6 +58,7 @@ createRoot(document.getElementById('root')).render( }> }> } /> + } /> diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx new file mode 100644 index 0000000..946a8d6 --- /dev/null +++ b/src/pages/PublicGallery.jsx @@ -0,0 +1,24 @@ +import server from "../networking"; +import Section from "../components/Catalogue/section.jsx"; + +function PublicGallery() { + const sectionTitle = "Event photos"; + const secLenValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 110]; + + return ( + <> + {secLenValues.map((val) => { + const key = `${sectionTitle} ${val}`; + return ( +
+ ); + })} + + ); +} + +export default PublicGallery; From 047a1260325315c07c712f0ba618330c269a98c9 Mon Sep 17 00:00:00 2001 From: JunHam Date: Mon, 14 Jul 2025 15:25:36 +0800 Subject: [PATCH 02/20] Added profileSection with profileCard, refactored Section code to use flex instead of scrollable table --- src/components/Catalogue/profileCard.jsx | 18 ++++ src/components/Catalogue/profileSection.jsx | 89 +++++++++++++++++ src/components/Catalogue/section.jsx | 105 ++++++++++---------- src/pages/Catalogue.jsx | 2 +- src/pages/PublicGallery.jsx | 4 +- 5 files changed, 162 insertions(+), 56 deletions(-) create mode 100644 src/components/Catalogue/profileCard.jsx create mode 100644 src/components/Catalogue/profileSection.jsx diff --git a/src/components/Catalogue/profileCard.jsx b/src/components/Catalogue/profileCard.jsx new file mode 100644 index 0000000..fa4871a --- /dev/null +++ b/src/components/Catalogue/profileCard.jsx @@ -0,0 +1,18 @@ +import { Box, Image, Text } from "@chakra-ui/react"; +import { useRef, useState, useEffect } from "react"; + +function ProfileCard() { + 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..d7b3818 --- /dev/null +++ b/src/components/Catalogue/profileSection.jsx @@ -0,0 +1,89 @@ +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 = 60; + const scrollRef = useRef(null); + const isMobile = useBreakpointValue({ base: true, md: false }); + + const [showLeftArrow, setShowLeftArrow] = useState(false); + const [showRightArrow, setShowRightArrow] = useState(false); + const [isOverflowing, setIsOverflowing] = useState(false); + + const checkOverflow = () => { + const el = scrollRef.current; + if (!el) return; + + setIsOverflowing(el.scrollWidth > el.clientWidth); + 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 && isOverflowing && showLeftArrow && ( + handleScroll("left")} /> + )} + {!isMobile && isOverflowing && showRightArrow && ( + handleScroll("right")} /> + )} + + + {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..2929be3 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -1,6 +1,5 @@ -import { Box, Table, Text, Icon, useBreakpointValue } from "@chakra-ui/react"; +import { Box, Text, Flex, useBreakpointValue } 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"; @@ -25,18 +24,30 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => { const scrollRef = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); + const [isOverflowing, setIsOverflowing] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); const checkScrollEdges = () => { const node = scrollRef.current; if (!node) return; + + setIsOverflowing(node.scrollWidth > node.clientWidth); 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,15 +68,6 @@ 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 ( @@ -74,51 +76,46 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => { {sectionTitle} - {!isMobile && ( - <> - scroll("left")} - /> - scroll("right")} - /> - + {/* Arrows only on non-mobile, overflow, and not at edge */} + {!isMobile && isOverflowing && !atStart && ( + scroll("left")} /> + )} + {!isMobile && isOverflowing && !atEnd && ( + scroll("right")} /> )} - - - - - - {items.map((item, index) => ( - - handleCardClick(item.title)} - > - - - - ))} - - - - - + + {items.map((item, index) => ( + handleCardClick(item.title)} + > + + + ))} + + {secLenValues.map((val) => { const key = `${sectionTitle} ${val}`; return ( From 6d14304a8581612a83e42ab8ddccc5fec3db5ba9 Mon Sep 17 00:00:00 2001 From: JunHam Date: Mon, 14 Jul 2025 15:41:37 +0800 Subject: [PATCH 03/20] Added animations to arrowOverlay. Refined all sections usage of arrowOverlay. --- src/components/Catalogue/arrowOverlay.jsx | 73 ++++++++++------- src/components/Catalogue/mmSection.jsx | 89 ++++++++++++--------- src/components/Catalogue/profileSection.jsx | 25 ++++-- src/components/Catalogue/section.jsx | 27 ++++--- 4 files changed, 130 insertions(+), 84 deletions(-) diff --git a/src/components/Catalogue/arrowOverlay.jsx b/src/components/Catalogue/arrowOverlay.jsx index 0551b90..44f0e31 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(Box); + +const ArrowOverlay = ({ direction, isDisabled, onClick, isOverflowing }) => { const isLeft = direction === "left"; return ( - - - + + {isOverflowing && ( + + + + )} + ); }; diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx index d7a40d4..d7ad1bc 100644 --- a/src/components/Catalogue/mmSection.jsx +++ b/src/components/Catalogue/mmSection.jsx @@ -1,4 +1,4 @@ -import { Box, Table, Text, useBreakpointValue } from "@chakra-ui/react"; +import { Box, Text, Flex, useBreakpointValue } from "@chakra-ui/react"; import { useRef, useState, useEffect } from "react"; import CardItem from "./cardItem.jsx"; import ArrowOverlay from "./arrowOverlay.jsx"; @@ -15,12 +15,17 @@ const MMSection = ({ onItemClick, selectedTitle }) => { const scrollRef = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); + const [isOverflowing, setIsOverflowing] = useState(false); const checkScrollEdges = () => { const node = scrollRef.current; if (!node) return; + + setIsOverflowing(node.scrollWidth > node.clientWidth); setAtStart(node.scrollLeft === 0); - setAtEnd(Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth); + setAtEnd( + Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth + ); }; useEffect(() => { @@ -51,48 +56,60 @@ 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" - > - - - - ))} - - - - - + + {items.map((item, index) => ( + handleClick(item.title)} + > + + + ))} + ); }; diff --git a/src/components/Catalogue/profileSection.jsx b/src/components/Catalogue/profileSection.jsx index d7b3818..eb5693b 100644 --- a/src/components/Catalogue/profileSection.jsx +++ b/src/components/Catalogue/profileSection.jsx @@ -4,7 +4,7 @@ import ArrowOverlay from "./arrowOverlay"; import ProfileCard from "./profileCard"; function ProfileSection() { - const count = 60; + const count = 7; const scrollRef = useRef(null); const isMobile = useBreakpointValue({ base: true, md: false }); @@ -55,20 +55,29 @@ function ProfileSection() { Key People - {!isMobile && isOverflowing && showLeftArrow && ( - handleScroll("left")} /> - )} - {!isMobile && isOverflowing && showRightArrow && ( - handleScroll("right")} /> + {!isMobile && ( + <> + handleScroll("left")} + isOverflowing={showLeftArrow} + /> + handleScroll("right")} + isOverflowing={showRightArrow} + /> + )} { const currentItem = items.find((item) => item.title === dialogTitle); return ( - - + + {sectionTitle} - {/* Arrows only on non-mobile, overflow, and not at edge */} - {!isMobile && isOverflowing && !atStart && ( - scroll("left")} /> - )} - {!isMobile && isOverflowing && !atEnd && ( - scroll("right")} /> + {!isMobile && ( + <> + scroll("left")} + isOverflowing={!atStart} + /> + scroll("right")} + isOverflowing={!atEnd} + /> + )} { {items.map((item, index) => ( Date: Mon, 14 Jul 2025 16:18:11 +0800 Subject: [PATCH 04/20] Removed unused variables --- src/components/Catalogue/mmSection.jsx | 2 -- src/components/Catalogue/profileSection.jsx | 5 ++--- src/components/Catalogue/section.jsx | 2 -- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx index d7ad1bc..ad16e08 100644 --- a/src/components/Catalogue/mmSection.jsx +++ b/src/components/Catalogue/mmSection.jsx @@ -15,13 +15,11 @@ const MMSection = ({ onItemClick, selectedTitle }) => { const scrollRef = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); - const [isOverflowing, setIsOverflowing] = useState(false); const checkScrollEdges = () => { const node = scrollRef.current; if (!node) return; - setIsOverflowing(node.scrollWidth > node.clientWidth); setAtStart(node.scrollLeft === 0); setAtEnd( Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth diff --git a/src/components/Catalogue/profileSection.jsx b/src/components/Catalogue/profileSection.jsx index eb5693b..e75d3b8 100644 --- a/src/components/Catalogue/profileSection.jsx +++ b/src/components/Catalogue/profileSection.jsx @@ -4,19 +4,17 @@ import ArrowOverlay from "./arrowOverlay"; import ProfileCard from "./profileCard"; function ProfileSection() { - const count = 7; + 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 [isOverflowing, setIsOverflowing] = useState(false); const checkOverflow = () => { const el = scrollRef.current; if (!el) return; - setIsOverflowing(el.scrollWidth > el.clientWidth); setShowLeftArrow(el.scrollLeft > 0); setShowRightArrow( Math.ceil(el.scrollLeft + el.clientWidth) < el.scrollWidth @@ -76,6 +74,7 @@ function ProfileSection() { ref={scrollRef} overflowX="auto" overflowY="hidden" + pr={10} py={6} gap={3} scrollBehavior="smooth" diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index 7d4b4c5..7ce5ad1 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -24,7 +24,6 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => { const scrollRef = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); - const [isOverflowing, setIsOverflowing] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); @@ -32,7 +31,6 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => { const node = scrollRef.current; if (!node) return; - setIsOverflowing(node.scrollWidth > node.clientWidth); setAtStart(node.scrollLeft === 0); setAtEnd( Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth From 98dafbfee6fae9b8aa1fdb3e9e35c9cb53aa2635 Mon Sep 17 00:00:00 2001 From: JunHam Date: Wed, 16 Jul 2025 16:35:43 +0800 Subject: [PATCH 05/20] Added placeholder public profile page --- src/components/Catalogue/profileCard.jsx | 31 ++++++++++++++++++------ src/main.jsx | 2 ++ src/pages/PublicProfile.jsx | 20 +++++++++++++++ 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 src/pages/PublicProfile.jsx diff --git a/src/components/Catalogue/profileCard.jsx b/src/components/Catalogue/profileCard.jsx index fa4871a..a0aab52 100644 --- a/src/components/Catalogue/profileCard.jsx +++ b/src/components/Catalogue/profileCard.jsx @@ -1,18 +1,33 @@ import { Box, Image, Text } from "@chakra-ui/react"; -import { useRef, useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; function ProfileCard() { + const navigate = useNavigate(); + + const handleProfileCardClick = () => { + navigate("/publicProfile"); + }; + return ( - + - - Name - + Name ); -}; +} export default ProfileCard; diff --git a/src/main.jsx b/src/main.jsx index 2c72464..96f4afa 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -20,6 +20,7 @@ import SampleProtected from './pages/SampleProtected.jsx'; import ProtectedLayout from './ProtectedLayout.jsx'; import AnimateIn from './AnimateIn.jsx'; import PublicGallery from './pages/PublicGallery.jsx'; +import PublicProfile from './pages/PublicProfile.jsx'; const store = configureStore({ reducer: { @@ -59,6 +60,7 @@ createRoot(document.getElementById('root')).render( }> } /> } /> + } /> diff --git a/src/pages/PublicProfile.jsx b/src/pages/PublicProfile.jsx new file mode 100644 index 0000000..ecaded6 --- /dev/null +++ b/src/pages/PublicProfile.jsx @@ -0,0 +1,20 @@ +import { Box, Text } from "@chakra-ui/react"; + +function PublicProfile() { + return ( + + + More coming soon! + + + ); +} + +export default PublicProfile; + From afd397f835adf3385f6bbd9cf462c87165694e8e Mon Sep 17 00:00:00 2001 From: JunHam Date: Fri, 18 Jul 2025 15:42:05 +0800 Subject: [PATCH 06/20] Added image rendering from backend --- src/assets/errorImage.png | Bin 0 -> 9938 bytes src/components/Catalogue/arrowOverlay.jsx | 2 +- src/components/Catalogue/cardItem.jsx | 11 +---- src/components/Catalogue/section.jsx | 27 ++++-------- src/pages/Catalogue.jsx | 48 ++++++++++++++-------- 5 files changed, 41 insertions(+), 47 deletions(-) create mode 100644 src/assets/errorImage.png diff --git a/src/assets/errorImage.png b/src/assets/errorImage.png new file mode 100644 index 0000000000000000000000000000000000000000..a095e0599bac2f6d46c6e394c6726749ebed20aa GIT binary patch literal 9938 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 YJo(oIcg`Mez@=~Auyy_YwKk{z1Em`xB>(^b literal 0 HcmV?d00001 diff --git a/src/components/Catalogue/arrowOverlay.jsx b/src/components/Catalogue/arrowOverlay.jsx index 44f0e31..ae0b924 100644 --- a/src/components/Catalogue/arrowOverlay.jsx +++ b/src/components/Catalogue/arrowOverlay.jsx @@ -2,7 +2,7 @@ import { Box, Icon } from "@chakra-ui/react"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; import { motion, AnimatePresence } from "framer-motion"; -const MotionBox = motion(Box); +const MotionBox = motion.create(Box); const ArrowOverlay = ({ direction, isDisabled, onClick, isOverflowing }) => { const isLeft = direction === "left"; diff --git a/src/components/Catalogue/cardItem.jsx b/src/components/Catalogue/cardItem.jsx index e66c22c..ece5241 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,8 +21,6 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, index }) => { setIsSelected(selectedTitle === itemTitle); }, [selectedTitle, itemTitle]); - const imageSrc = images[index % images.length]; - useEffect(() => { setIsLoading(true); }, [imageSrc]); diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index 7ce5ad1..cceb633 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -4,21 +4,14 @@ import CardItem from "./cardItem.jsx"; import CatalogueItemView from "./catalogueItemView.jsx"; import ArrowOverlay from "./arrowOverlay.jsx"; -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: `Description for ${art.name}`, + imageSrc: art.image ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.image}` : './src/assets/errorImage.png', })); const scrollRef = useRef(null); @@ -32,9 +25,7 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => { 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); }; const scroll = (direction) => { @@ -105,7 +96,7 @@ const Section = ({ sectionTitle, secLen, selectedTitle, onItemClick }) => { scrollbarWidth: 'none', }} > - {items.map((item, index) => ( + {items.map((item) => ( { onClick={() => handleCardClick(item.title)} > { + async function fetchCatalogue() { + try { + const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/getCatalogue`); + if (res && res.status === 200 && res.data?.raw.data) { + setCatalogueData(res.data?.raw.data); + } else { + console.error("Failed to fetch catalogue:", res); + } + } catch (err) { + console.error("Error fetching catalogue:", err); + } + } + + fetchCatalogue(); + }, []); const handleItemClick = (title, source) => { if (source === "MM") { @@ -43,23 +57,21 @@ function Catalogue() {
)} - {secLenValues.map((val) => { - const key = `${sectionTitle} ${val}`; - return ( -
- ); - })} + {/* Render all categories/groups */} + {Object.entries(catalogueData).map(([groupName, artefacts]) => ( +
+ ))} ); } From e200a9c467a5cd825763d80b5b0806468f04560b Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 22 Jul 2025 13:12:56 +0800 Subject: [PATCH 07/20] Implemented MMSection to use the books returned from getCatalogue --- src/components/Catalogue/cardComponent.jsx | 2 +- src/components/Catalogue/cardItem.jsx | 7 +- src/components/Catalogue/mmSection.jsx | 100 +++++++++++-------- src/components/Catalogue/section.jsx | 24 ++++- src/pages/Catalogue.jsx | 78 ++++++++++++--- src/pages/PublicGallery.jsx | 42 +++++--- src/pages/PublicProfile.jsx | 110 +++++++++++++++++++-- 7 files changed, 281 insertions(+), 82 deletions(-) diff --git a/src/components/Catalogue/cardComponent.jsx b/src/components/Catalogue/cardComponent.jsx index 53bcd47..58d6a24 100644 --- a/src/components/Catalogue/cardComponent.jsx +++ b/src/components/Catalogue/cardComponent.jsx @@ -7,7 +7,7 @@ const CardComponent = ({ imageSrc, itemTitle, itemDescription, isLoading, setIsL {itemTitle} setIsLoading(false)} height="180px" width="100%" diff --git a/src/components/Catalogue/cardItem.jsx b/src/components/Catalogue/cardItem.jsx index ece5241..39cca95 100644 --- a/src/components/Catalogue/cardItem.jsx +++ b/src/components/Catalogue/cardItem.jsx @@ -5,7 +5,7 @@ import CardComponent from "./cardComponent"; const MotionBox = motion.create(Box); -const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc }) => { +const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedImages }) => { const isMobile = useBreakpointValue({ base: true, md: false }); const [isHovered, setIsHovered] = useState(false); @@ -16,6 +16,8 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc }) => { const [hoverPos, setHoverPos] = useState({ top: 0, left: 0 }); const scrollContainerRef = useRef(null); const hoverTimeoutRef = useRef(null); + const isImageLoaded = loadedImages?.has(imageSrc); + console.log(isImageLoaded) useEffect(() => { setIsSelected(selectedTitle === itemTitle); @@ -121,7 +123,7 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc }) => { {!isMobile && ( - {isHovered && ( + {isHovered && isImageLoaded && ( { imageSrc={imageSrc} itemTitle={itemTitle} itemDescription={itemDescription} + setIsLoading={setIsLoading} isLoading={isLoading} isSelected={isSelected} expanded={true} diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx index ad16e08..d4f36da 100644 --- a/src/components/Catalogue/mmSection.jsx +++ b/src/components/Catalogue/mmSection.jsx @@ -1,31 +1,23 @@ +import { useEffect, useState, useRef } from "react"; import { Box, Text, Flex, useBreakpointValue } from "@chakra-ui/react"; -import { useRef, useState, useEffect } from "react"; import CardItem from "./cardItem.jsx"; import ArrowOverlay from "./arrowOverlay.jsx"; -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); + const [loadedImages, setLoadedImages] = useState(new Set()); const checkScrollEdges = () => { 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; @@ -40,6 +32,32 @@ const MMSection = ({ onItemClick, selectedTitle }) => { }; }, []); + // Preload images + useEffect(() => { + const preloadImages = async () => { + const imageSrcs = books + .map((book) => book.artefacts?.[0]?.image) + .filter(Boolean) + .map((img) => `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${img}`); + + const promises = imageSrcs.map((src) => { + return new Promise((resolve) => { + const img = new window.Image(); + img.src = src; + img.onload = () => { + setLoadedImages((prev) => new Set(prev).add(src)); + resolve(); + }; + img.onerror = resolve; + }); + }); + + await Promise.all(promises); + }; + + preloadImages(); + }, [books]); + const scroll = (direction) => { if (!scrollRef.current) return; const scrollAmount = 1200; @@ -61,18 +79,8 @@ const MMSection = ({ onItemClick, selectedTitle }) => { {!isMobile && ( <> - scroll("left")} - isOverflowing={!atStart} - /> - scroll("right")} - isOverflowing={!atEnd} - /> + scroll("left")} isOverflowing={!atStart} /> + scroll("right")} isOverflowing={!atEnd} /> )} @@ -90,23 +98,31 @@ const MMSection = ({ onItemClick, selectedTitle }) => { scrollbarWidth: 'none', }} > - {items.map((item, index) => ( - handleClick(item.title)} - > - - - ))} + {books.map((book) => { + const firstImage = book.artefacts?.[0]?.image; + const imageSrc = firstImage + ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${firstImage}` + : './src/assets/errorImage.png'; + + return ( + handleClick(book.title)} + > + + + ); + })} ); diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index cceb633..cdf890c 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -1,4 +1,4 @@ -import { Box, Text, Flex, useBreakpointValue } from "@chakra-ui/react"; +import { Box, Text, Flex, useBreakpointValue, Image } from "@chakra-ui/react"; import { useRef, useState, useEffect } from "react"; import CardItem from "./cardItem.jsx"; import CatalogueItemView from "./catalogueItemView.jsx"; @@ -19,6 +19,26 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = const [atEnd, setAtEnd] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); + const [loadedImages, setLoadedImages] = useState(new Set()); + + useEffect(() => { + const preloadImages = async () => { + const promises = items.map((item) => { + return new Promise((resolve) => { + const img = new window.Image(); + img.src = item.imageSrc; + img.onload = () => { + setLoadedImages(prev => new Set(prev).add(item.imageSrc)); + resolve(); + }; + img.onerror = resolve; + }); + }); + await Promise.all(promises); + }; + + preloadImages(); + }, []); const checkScrollEdges = () => { const node = scrollRef.current; @@ -105,11 +125,13 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = borderRadius="md" onClick={() => handleCardClick(item.title)} > + ))} diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index 8eca840..7cf571a 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Box } from "@chakra-ui/react"; +import { Box, Text, Center } from "@chakra-ui/react"; import { AnimatePresence, motion } from "framer-motion"; import MMSection from "../components/Catalogue/mmSection.jsx"; import Section from "../components/Catalogue/section.jsx"; @@ -8,14 +8,39 @@ import server from "../networking"; function Catalogue() { const [selectedMMTitle, setSelectedMMTitle] = useState(null); const [showDetails, setShowDetails] = useState(false); - const [catalogueData, setCatalogueData] = useState({}); + const [books, setBooks] = useState([]); + const [categories, setCategories] = useState({}); useEffect(() => { async function fetchCatalogue() { try { const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/getCatalogue`); if (res && res.status === 200 && res.data?.raw.data) { - setCatalogueData(res.data?.raw.data); + const data = res.data.raw.data; + const resolvedBooks = data.books.map((book) => { + const artefacts = book.mmIDs + .map((mmID) => { + for (const artefactGroup of Object.values(data.categories)) { + const match = artefactGroup.find((a) => a.image === mmID); + if (match) return match; + } + return { + image: mmID, + name: mmID, + description: "", + id: mmID, + }; + }) + .filter(Boolean); + + return { + ...book, + artefacts, + }; + }); + + setBooks(resolvedBooks); + setCategories(data.categories || {}); } else { console.error("Failed to fetch catalogue:", res); } @@ -37,12 +62,31 @@ 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 (!hasBooks && !hasCategories) { + return ( +
+ + No Items Uploaded + +
+ ); + } + return ( <> - handleItemClick(title, "MM")} - selectedTitle={selectedMMTitle} - /> + {hasBooks && ( + handleItemClick(title, "MM")} + selectedTitle={selectedMMTitle} + books={books} + /> + )} {showDetails && selectedMMTitle && ( @@ -57,21 +101,23 @@ function Catalogue() {
)} - {/* Render all categories/groups */} - {Object.entries(catalogueData).map(([groupName, artefacts]) => ( -
- ))} + {hasCategories && + Object.entries(categories).map(([groupName, artefacts]) => ( +
+ ))} + + ); } diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx index 1da8144..d437c4b 100644 --- a/src/pages/PublicGallery.jsx +++ b/src/pages/PublicGallery.jsx @@ -1,24 +1,42 @@ +import { Box } from "@chakra-ui/react"; +import { useEffect, useState } from "react"; import server from "../networking"; import Section from "../components/Catalogue/section.jsx"; import ProfileSection from "../components/Catalogue/ProfileSection.jsx"; function PublicGallery() { - const sectionTitle = "Event photos"; - const secLenValues = [50, 2, 3, 4, 5, 6, 7, 8, 9, 10, 110]; + const [catalogueData, setCatalogueData] = useState({}); + + useEffect(() => { + async function fetchCatalogue() { + try { + const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/getCatalogue`); + if (res && res.status === 200 && res.data?.raw.data) { + setCatalogueData(res.data?.raw.data); + } else { + console.error("Failed to fetch catalogue:", res); + } + } catch (err) { + console.error("Error fetching catalogue:", err); + } + } + + fetchCatalogue(); + }, []); + return ( <> - {secLenValues.map((val) => { - const key = `${sectionTitle} ${val}`; - return ( -
- ); - })} + {Object.entries(catalogueData).map(([groupName, artefacts]) => ( +
+ ))} + + ); } diff --git a/src/pages/PublicProfile.jsx b/src/pages/PublicProfile.jsx index ecaded6..91c3dfc 100644 --- a/src/pages/PublicProfile.jsx +++ b/src/pages/PublicProfile.jsx @@ -1,20 +1,114 @@ -import { Box, Text } from "@chakra-ui/react"; +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 ( - - More coming soon! - + {/* 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; - From 169afa0dac1dd5bd760677f0525ca4bfec071c02 Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 22 Jul 2025 13:49:20 +0800 Subject: [PATCH 08/20] Fix bug in catalogue where Section component is shared among all books --- src/components/Catalogue/cardItem.jsx | 14 ++++++++--- src/components/Catalogue/section.jsx | 35 +++++++++++---------------- src/pages/Catalogue.jsx | 8 ++++++ 3 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/components/Catalogue/cardItem.jsx b/src/components/Catalogue/cardItem.jsx index 39cca95..e11657f 100644 --- a/src/components/Catalogue/cardItem.jsx +++ b/src/components/Catalogue/cardItem.jsx @@ -5,7 +5,7 @@ import CardComponent from "./cardComponent"; const MotionBox = motion.create(Box); -const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedImages }) => { +const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedImages, onImageLoad }) => { const isMobile = useBreakpointValue({ base: true, md: false }); const [isHovered, setIsHovered] = useState(false); @@ -17,7 +17,6 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI const scrollContainerRef = useRef(null); const hoverTimeoutRef = useRef(null); const isImageLoaded = loadedImages?.has(imageSrc); - console.log(isImageLoaded) useEffect(() => { setIsSelected(selectedTitle === itemTitle); @@ -27,6 +26,13 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI setIsLoading(true); }, [imageSrc]); + const handleSetIsLoading = (val) => { + setIsLoading(val); + if (!val && onImageLoad) { + onImageLoad(imageSrc); + } + }; + const updateHoverPosition = () => { const rect = cardRef.current?.getBoundingClientRect(); if (rect) { @@ -115,7 +121,7 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI itemTitle={itemTitle} itemDescription={itemDescription} isLoading={isLoading} - setIsLoading={setIsLoading} + setIsLoading={handleSetIsLoading} isSelected={isSelected} expanded={false} /> @@ -149,7 +155,7 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI imageSrc={imageSrc} itemTitle={itemTitle} itemDescription={itemDescription} - setIsLoading={setIsLoading} + setIsLoading={handleSetIsLoading} isLoading={isLoading} isSelected={isSelected} expanded={true} diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index cdf890c..03b241b 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -11,7 +11,9 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = id: art.id, title: art.name, description: `Description for ${art.name}`, - imageSrc: art.image ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.image}` : './src/assets/errorImage.png', + imageSrc: art.image + ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.image}` + : "./src/assets/errorImage.png", })); const scrollRef = useRef(null); @@ -19,26 +21,16 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = const [atEnd, setAtEnd] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); + const [loadedImages, setLoadedImages] = useState(new Set()); useEffect(() => { - const preloadImages = async () => { - const promises = items.map((item) => { - return new Promise((resolve) => { - const img = new window.Image(); - img.src = item.imageSrc; - img.onload = () => { - setLoadedImages(prev => new Set(prev).add(item.imageSrc)); - resolve(); - }; - img.onerror = resolve; - }); - }); - await Promise.all(promises); - }; + setLoadedImages(new Set()); + }, [sectionTitle]); - preloadImages(); - }, []); + const handleImageLoad = (src) => { + setLoadedImages((prev) => new Set(prev).add(src)); + }; const checkScrollEdges = () => { const node = scrollRef.current; @@ -111,9 +103,9 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = gap={4} scrollBehavior="smooth" css={{ - '&::-webkit-scrollbar': { display: 'none' }, - msOverflowStyle: 'none', - scrollbarWidth: 'none', + "&::-webkit-scrollbar": { display: "none" }, + msOverflowStyle: "none", + scrollbarWidth: "none", }} > {items.map((item) => ( @@ -125,13 +117,14 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = borderRadius="md" onClick={() => handleCardClick(item.title)} > - +
))} diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index 7cf571a..489a870 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -3,6 +3,7 @@ import { Box, Text, Center } from "@chakra-ui/react"; import { AnimatePresence, motion } from "framer-motion"; import MMSection from "../components/Catalogue/mmSection.jsx"; import Section from "../components/Catalogue/section.jsx"; +import CentredSpinner from "../components/centredSpinner.jsx"; import server from "../networking"; function Catalogue() { @@ -10,6 +11,7 @@ function Catalogue() { const [showDetails, setShowDetails] = useState(false); const [books, setBooks] = useState([]); const [categories, setCategories] = useState({}); + const [loading, setLoading] = useState(true); useEffect(() => { async function fetchCatalogue() { @@ -46,6 +48,8 @@ function Catalogue() { } } catch (err) { console.error("Error fetching catalogue:", err); + } finally { + setLoading(false); } } @@ -68,6 +72,10 @@ function Catalogue() { const hasBooks = books.length > 0; const hasCategories = Object.keys(categories).length > 0; + if (loading) { + return ; + } + if (!hasBooks && !hasCategories) { return (
From 9ce682b1db7b82cfcf6fecb2c711f37c409c3c7c Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 22 Jul 2025 16:34:02 +0800 Subject: [PATCH 09/20] Added navigation to get started button on homepage --- src/pages/Homepage.jsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/pages/Homepage.jsx b/src/pages/Homepage.jsx index f11f44f..70808d1 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 { accountID, username, superuser } = useSelector(state => state.auth); + + const handleGetStarted = () => { + if (accountID && username && superuser !== null) { + navigate('/catalogue'); + } else { + navigate('/auth/login'); + } + }; + return @@ -54,7 +67,7 @@ function Homepage() { Connect
with your
roots
The intelligent artefact digitisation platform. - +
From 8db453a915171822334a7cd0cc95346c6d59b4f6 Mon Sep 17 00:00:00 2001 From: JunHam Date: Tue, 22 Jul 2025 22:09:39 +0800 Subject: [PATCH 10/20] Fixed public gallery to display artefacts in their respective categories --- src/pages/PublicGallery.jsx | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx index d437c4b..60039e5 100644 --- a/src/pages/PublicGallery.jsx +++ b/src/pages/PublicGallery.jsx @@ -1,41 +1,62 @@ -import { Box } from "@chakra-ui/react"; +import { Box, Text, Center } from "@chakra-ui/react"; import { useEffect, useState } from "react"; import server 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 [catalogueData, setCatalogueData] = useState({}); + const [categories, setCategories] = useState({}); + const [loading, setLoading] = useState(true); useEffect(() => { async function fetchCatalogue() { try { const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/getCatalogue`); if (res && res.status === 200 && res.data?.raw.data) { - setCatalogueData(res.data?.raw.data); + const data = res.data.raw.data; + setCategories(data.categories || {}); } else { console.error("Failed to fetch catalogue:", res); } } catch (err) { console.error("Error fetching catalogue:", err); + } finally { + setLoading(false); } } fetchCatalogue(); }, []); - + + const hasCategories = Object.keys(categories).length > 0; + + if (loading) { + return ; + } + + if (!hasCategories) { + return ( +
+ + No Items Uploaded + +
+ ); + } return ( <> - {Object.entries(catalogueData).map(([groupName, artefacts]) => ( + + {Object.entries(categories).map(([groupName, artefacts]) => (
))} - + ); From d5c3f64aea2edff2e3d83d5abf74fbcb4c09772a Mon Sep 17 00:00:00 2001 From: JunHam Date: Thu, 24 Jul 2025 00:43:53 +0800 Subject: [PATCH 11/20] Fixed for PR --- .../{errorImage.png => placeholder.png} | Bin src/components/Catalogue/mmSection.jsx | 51 ++++++++---------- src/components/Catalogue/section.jsx | 10 ++-- src/pages/Catalogue.jsx | 35 ++++-------- src/pages/Homepage.jsx | 4 +- src/pages/PublicGallery.jsx | 2 +- 6 files changed, 40 insertions(+), 62 deletions(-) rename src/assets/{errorImage.png => placeholder.png} (100%) diff --git a/src/assets/errorImage.png b/src/assets/placeholder.png similarity index 100% rename from src/assets/errorImage.png rename to src/assets/placeholder.png diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx index d4f36da..762b764 100644 --- a/src/components/Catalogue/mmSection.jsx +++ b/src/components/Catalogue/mmSection.jsx @@ -1,5 +1,5 @@ import { useEffect, useState, useRef } from "react"; -import { Box, Text, Flex, useBreakpointValue } from "@chakra-ui/react"; +import { Box, Text, Flex, useBreakpointValue, Image } from "@chakra-ui/react"; import CardItem from "./cardItem.jsx"; import ArrowOverlay from "./arrowOverlay.jsx"; @@ -10,6 +10,15 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { const [atEnd, setAtEnd] = useState(false); const [loadedImages, setLoadedImages] = useState(new Set()); + // Reset loaded images when books change + useEffect(() => { + setLoadedImages(new Set()); + }, [books]); + + const handleImageLoad = (src) => { + setLoadedImages((prev) => new Set(prev).add(src)); + }; + const checkScrollEdges = () => { const node = scrollRef.current; if (!node) return; @@ -32,32 +41,6 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { }; }, []); - // Preload images - useEffect(() => { - const preloadImages = async () => { - const imageSrcs = books - .map((book) => book.artefacts?.[0]?.image) - .filter(Boolean) - .map((img) => `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${img}`); - - const promises = imageSrcs.map((src) => { - return new Promise((resolve) => { - const img = new window.Image(); - img.src = src; - img.onload = () => { - setLoadedImages((prev) => new Set(prev).add(src)); - resolve(); - }; - img.onerror = resolve; - }); - }); - - await Promise.all(promises); - }; - - preloadImages(); - }, [books]); - const scroll = (direction) => { if (!scrollRef.current) return; const scrollAmount = 1200; @@ -99,10 +82,10 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { }} > {books.map((book) => { - const firstImage = book.artefacts?.[0]?.image; + const firstImage = book.artefacts?.[0]?.id; const imageSrc = firstImage ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${firstImage}` - : './src/assets/errorImage.png'; + : './src/assets/placeholder.png'; return ( { borderRadius="md" onClick={() => handleClick(book.title)} > + {/* Hidden Image for preloading */} + handleImageLoad(imageSrc)} + /> + { ); }; -export default MMSection; +export default MMSection; \ No newline at end of file diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index 03b241b..e3a1791 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -10,12 +10,12 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = const items = artefacts.map((art) => ({ id: art.id, title: art.name, - description: `Description for ${art.name}`, - imageSrc: art.image - ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.image}` - : "./src/assets/errorImage.png", + description: art.description, + imageSrc: art.id + ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.id}` + : "./src/assets/placeholder.png", })); - + const scrollRef = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index 489a870..3977c32 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -16,33 +16,20 @@ function Catalogue() { useEffect(() => { async function fetchCatalogue() { try { - const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/getCatalogue`); - if (res && res.status === 200 && res.data?.raw.data) { + const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/catalogue`); + if (res && res.status === 200 && res.data?.raw?.data) { const data = res.data.raw.data; - const resolvedBooks = data.books.map((book) => { - const artefacts = book.mmIDs - .map((mmID) => { - for (const artefactGroup of Object.values(data.categories)) { - const match = artefactGroup.find((a) => a.image === mmID); - if (match) return match; - } - return { - image: mmID, - name: mmID, - description: "", - id: mmID, - }; - }) - .filter(Boolean); - - return { - ...book, - artefacts, - }; - }); + const categories = data.categories || {}; + const books = data.books || []; + + setCategories(categories); + + const resolvedBooks = books.map((book) => ({ + ...book, + artefacts: book.mmArtefacts || [] + })); setBooks(resolvedBooks); - setCategories(data.categories || {}); } else { console.error("Failed to fetch catalogue:", res); } diff --git a/src/pages/Homepage.jsx b/src/pages/Homepage.jsx index 70808d1..73b5021 100644 --- a/src/pages/Homepage.jsx +++ b/src/pages/Homepage.jsx @@ -16,10 +16,10 @@ function Homepage() { const imgOffset = "-100px" const navigate = useNavigate(); - const { accountID, username, superuser } = useSelector(state => state.auth); + const { username } = useSelector(state => state.auth); const handleGetStarted = () => { - if (accountID && username && superuser !== null) { + if (username) { navigate('/catalogue'); } else { navigate('/auth/login'); diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx index 60039e5..43fa782 100644 --- a/src/pages/PublicGallery.jsx +++ b/src/pages/PublicGallery.jsx @@ -12,7 +12,7 @@ function PublicGallery() { useEffect(() => { async function fetchCatalogue() { try { - const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/getCatalogue`); + const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/catalogue`); if (res && res.status === 200 && res.data?.raw.data) { const data = res.data.raw.data; setCategories(data.categories || {}); From 7ebe789227337c92beb2c9dbef25f62349bf866e Mon Sep 17 00:00:00 2001 From: JunHam Date: Thu, 24 Jul 2025 01:43:02 +0800 Subject: [PATCH 12/20] Fixed zindex bug --- src/components/Catalogue/arrowOverlay.jsx | 2 +- src/components/Catalogue/cardItem.jsx | 10 +++++----- src/components/Catalogue/section.jsx | 2 +- src/pages/Catalogue.jsx | 1 + 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/Catalogue/arrowOverlay.jsx b/src/components/Catalogue/arrowOverlay.jsx index ae0b924..5afc9b7 100644 --- a/src/components/Catalogue/arrowOverlay.jsx +++ b/src/components/Catalogue/arrowOverlay.jsx @@ -25,7 +25,7 @@ const ArrowOverlay = ({ direction, isDisabled, onClick, isOverflowing }) => { display="flex" alignItems="center" justifyContent={isLeft ? "flex-start" : "flex-end"} - zIndex={1} + zIndex={10} px={6} bgGradient={isLeft ? "to-r" : "to-l"} gradientFrom="rgb(255, 255, 255)" diff --git a/src/components/Catalogue/cardItem.jsx b/src/components/Catalogue/cardItem.jsx index e11657f..1aaa3d4 100644 --- a/src/components/Catalogue/cardItem.jsx +++ b/src/components/Catalogue/cardItem.jsx @@ -103,6 +103,7 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI return ( <> + {/* Base Card Container */} 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" > Date: Thu, 24 Jul 2025 01:44:50 +0800 Subject: [PATCH 13/20] Removed debug console --- src/pages/Catalogue.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index cacf310..3977c32 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -19,7 +19,6 @@ function Catalogue() { const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/catalogue`); if (res && res.status === 200 && res.data?.raw?.data) { const data = res.data.raw.data; - console.log(data); const categories = data.categories || {}; const books = data.books || []; From 89c39312c222987c2195943346bf841b9b9038c7 Mon Sep 17 00:00:00 2001 From: JunHam Date: Thu, 24 Jul 2025 01:49:14 +0800 Subject: [PATCH 14/20] Renamed placeholder image --- .../{placeholder.png => placeholderImage.png} | Bin src/components/Catalogue/mmSection.jsx | 46 ++++++++++++------ src/components/Catalogue/section.jsx | 31 +++++++++--- 3 files changed, 56 insertions(+), 21 deletions(-) rename src/assets/{placeholder.png => placeholderImage.png} (100%) diff --git a/src/assets/placeholder.png b/src/assets/placeholderImage.png similarity index 100% rename from src/assets/placeholder.png rename to src/assets/placeholderImage.png diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx index 762b764..50ca6ad 100644 --- a/src/components/Catalogue/mmSection.jsx +++ b/src/components/Catalogue/mmSection.jsx @@ -23,7 +23,9 @@ const MMSection = ({ books = [], 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 @@ -56,14 +58,30 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { return ( - + Meeting Minutes {!isMobile && ( <> - scroll("left")} isOverflowing={!atStart} /> - scroll("right")} isOverflowing={!atEnd} /> + scroll("left")} + isOverflowing={!atStart} + /> + scroll("right")} + isOverflowing={!atEnd} + /> )} @@ -76,16 +94,16 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { gap={4} scrollBehavior="smooth" css={{ - '&::-webkit-scrollbar': { display: 'none' }, - msOverflowStyle: 'none', - scrollbarWidth: 'none', + "&::-webkit-scrollbar": { display: "none" }, + msOverflowStyle: "none", + scrollbarWidth: "none", }} > {books.map((book) => { const firstImage = book.artefacts?.[0]?.id; const imageSrc = firstImage ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${firstImage}` - : './src/assets/placeholder.png'; + : "./src/assets/placeholderImage.png"; return ( { onClick={() => handleClick(book.title)} > {/* Hidden Image for preloading */} - handleImageLoad(imageSrc)} /> - + { ); }; -export default MMSection; \ No newline at end of file +export default MMSection; diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index 436095e..9d95bbb 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -4,7 +4,12 @@ import CardItem from "./cardItem.jsx"; import CatalogueItemView from "./catalogueItemView.jsx"; import ArrowOverlay from "./arrowOverlay.jsx"; -const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) => { +const Section = ({ + sectionTitle, + selectedTitle, + onItemClick, + artefacts = [], +}) => { const isMobile = useBreakpointValue({ base: true, md: false }); const items = artefacts.map((art) => ({ @@ -13,9 +18,9 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = description: art.description, imageSrc: art.id ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.id}` - : "./src/assets/placeholder.png", + : "./src/assets/placeholderImage.png", })); - + const scrollRef = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); @@ -37,7 +42,9 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = 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 + ); }; const scroll = (direction) => { @@ -73,7 +80,13 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = return ( - + {sectionTitle} @@ -117,7 +130,11 @@ const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) = borderRadius="md" onClick={() => handleCardClick(item.title)} > - + Date: Thu, 24 Jul 2025 15:38:37 +0800 Subject: [PATCH 15/20] Conditionally rendered section only if there are artefacts in the section, otherwise return "No artefacts in this section" --- src/components/Catalogue/section.jsx | 55 ++++++++++++++++------------ src/pages/Catalogue.jsx | 2 +- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index 9d95bbb..32b094f 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -120,31 +120,38 @@ const Section = ({ msOverflowStyle: "none", scrollbarWidth: "none", }} + justifyContent={items.length === 0 ? "center" : "flex-start"} > - {items.map((item) => ( - handleCardClick(item.title)} - > - - - - ))} + {items.length === 0 ? ( + + No artefacts in this section. + + ) : ( + items.map((item) => ( + handleCardClick(item.title)} + > + + + + )) + )} - ))} + ))} From 6040555987bf64d11b5f9ed29a2e96758b49c6df Mon Sep 17 00:00:00 2001 From: JunHam Date: Thu, 24 Jul 2025 16:39:23 +0800 Subject: [PATCH 16/20] Fixed loading states --- src/components/Catalogue/cardComponent.jsx | 11 ++++++++--- src/components/Catalogue/cardItem.jsx | 19 ++----------------- src/components/Catalogue/mmSection.jsx | 12 ------------ src/components/Catalogue/section.jsx | 12 ------------ 4 files changed, 10 insertions(+), 44 deletions(-) diff --git a/src/components/Catalogue/cardComponent.jsx b/src/components/Catalogue/cardComponent.jsx index 58d6a24..aa3b482 100644 --- a/src/components/Catalogue/cardComponent.jsx +++ b/src/components/Catalogue/cardComponent.jsx @@ -1,6 +1,11 @@ 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 ( @@ -8,8 +13,8 @@ const CardComponent = ({ imageSrc, itemTitle, itemDescription, isLoading, setIsL src={imageSrc} alt={itemTitle} loading={expanded ? "eager" : "lazy"} - onLoad={() => setIsLoading(false)} - height="180px" + onLoad={() => setIsImageLoaded(true)} + height="180px" width="100%" /> diff --git a/src/components/Catalogue/cardItem.jsx b/src/components/Catalogue/cardItem.jsx index 1aaa3d4..8c61dc2 100644 --- a/src/components/Catalogue/cardItem.jsx +++ b/src/components/Catalogue/cardItem.jsx @@ -5,7 +5,7 @@ import CardComponent from "./cardComponent"; const MotionBox = motion.create(Box); -const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedImages, onImageLoad }) => { +const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc }) => { const isMobile = useBreakpointValue({ base: true, md: false }); const [isHovered, setIsHovered] = useState(false); @@ -16,23 +16,11 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI const [hoverPos, setHoverPos] = useState({ top: 0, left: 0 }); const scrollContainerRef = useRef(null); const hoverTimeoutRef = useRef(null); - const isImageLoaded = loadedImages?.has(imageSrc); useEffect(() => { setIsSelected(selectedTitle === itemTitle); }, [selectedTitle, itemTitle]); - useEffect(() => { - setIsLoading(true); - }, [imageSrc]); - - const handleSetIsLoading = (val) => { - setIsLoading(val); - if (!val && onImageLoad) { - onImageLoad(imageSrc); - } - }; - const updateHoverPosition = () => { const rect = cardRef.current?.getBoundingClientRect(); if (rect) { @@ -123,7 +111,6 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI itemTitle={itemTitle} itemDescription={itemDescription} isLoading={isLoading} - setIsLoading={handleSetIsLoading} isSelected={isSelected} expanded={false} /> @@ -131,7 +118,7 @@ const CardItem = ({ itemTitle, itemDescription, selectedTitle, imageSrc, loadedI {!isMobile && ( - {isHovered && isImageLoaded && ( + {isHovered && ( diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx index 50ca6ad..fbbe9a6 100644 --- a/src/components/Catalogue/mmSection.jsx +++ b/src/components/Catalogue/mmSection.jsx @@ -8,16 +8,6 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { const scrollRef = useRef(null); const [atStart, setAtStart] = useState(true); const [atEnd, setAtEnd] = useState(false); - const [loadedImages, setLoadedImages] = useState(new Set()); - - // Reset loaded images when books change - useEffect(() => { - setLoadedImages(new Set()); - }, [books]); - - const handleImageLoad = (src) => { - setLoadedImages((prev) => new Set(prev).add(src)); - }; const checkScrollEdges = () => { const node = scrollRef.current; @@ -119,7 +109,6 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { src={imageSrc} style={{ display: "none" }} alt="" - onLoad={() => handleImageLoad(imageSrc)} /> { itemDescription={book.subtitle} selectedTitle={selectedTitle} imageSrc={imageSrc} - loadedImages={loadedImages} /> ); diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index 32b094f..75da5d5 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -27,16 +27,6 @@ const Section = ({ const [dialogOpen, setDialogOpen] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); - const [loadedImages, setLoadedImages] = useState(new Set()); - - useEffect(() => { - setLoadedImages(new Set()); - }, [sectionTitle]); - - const handleImageLoad = (src) => { - setLoadedImages((prev) => new Set(prev).add(src)); - }; - const checkScrollEdges = () => { const node = scrollRef.current; if (!node) return; @@ -146,8 +136,6 @@ const Section = ({ itemTitle={item.title} itemDescription={item.description} selectedTitle={selectedTitle} - loadedImages={loadedImages} - onImageLoad={handleImageLoad} /> )) From 110172591ffb5ad79b1d09813a787e3b69f06750 Mon Sep 17 00:00:00 2001 From: JunHam Date: Fri, 25 Jul 2025 10:20:48 +0800 Subject: [PATCH 17/20] Used placeholderImage by import instead --- src/components/Catalogue/mmSection.jsx | 3 ++- src/components/Catalogue/section.jsx | 10 +++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/Catalogue/mmSection.jsx b/src/components/Catalogue/mmSection.jsx index fbbe9a6..548d8ca 100644 --- a/src/components/Catalogue/mmSection.jsx +++ b/src/components/Catalogue/mmSection.jsx @@ -2,6 +2,7 @@ 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 = ({ books = [], onItemClick, selectedTitle }) => { const isMobile = useBreakpointValue({ base: true, md: false }); @@ -93,7 +94,7 @@ const MMSection = ({ books = [], onItemClick, selectedTitle }) => { const firstImage = book.artefacts?.[0]?.id; const imageSrc = firstImage ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${firstImage}` - : "./src/assets/placeholderImage.png"; + : placeholderImage; return ( { +const Section = ({ sectionTitle, selectedTitle, onItemClick, artefacts = [] }) => { const isMobile = useBreakpointValue({ base: true, md: false }); const items = artefacts.map((art) => ({ @@ -18,7 +14,7 @@ const Section = ({ description: art.description, imageSrc: art.id ? `${import.meta.env.VITE_BACKEND_URL}/cdn/artefacts/${art.id}` - : "./src/assets/placeholderImage.png", + : placeholderImage, })); const scrollRef = useRef(null); From ed415413a260d056351fff5c4904f7a794271cd2 Mon Sep 17 00:00:00 2001 From: JunHam Date: Fri, 25 Jul 2025 11:04:05 +0800 Subject: [PATCH 18/20] Added json response checking for data retrieval in catalogue and public gallery --- src/pages/Catalogue.jsx | 34 +++++++++++++++++----------------- src/pages/PublicGallery.jsx | 23 ++++++++++++++++------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index 3007ae6..a7be702 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -5,6 +5,7 @@ import MMSection from "../components/Catalogue/mmSection.jsx"; import Section from "../components/Catalogue/section.jsx"; import CentredSpinner from "../components/centredSpinner.jsx"; import server from "../networking"; +import ToastWizard from "../components/toastWizard.js"; function Catalogue() { const [selectedMMTitle, setSelectedMMTitle] = useState(null); @@ -16,25 +17,24 @@ function Catalogue() { useEffect(() => { async function fetchCatalogue() { try { - const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/catalogue`); - if (res && res.status === 200 && res.data?.raw?.data) { - const data = res.data.raw.data; - const categories = data.categories || {}; - const books = data.books || []; + const res = await server.get("/cdn/catalogue"); + const data = res.data; - setCategories(categories); - - const resolvedBooks = books.map((book) => ({ - ...book, - artefacts: book.mmArtefacts || [] - })); - - setBooks(resolvedBooks); - } else { - console.error("Failed to fetch catalogue:", res); + if (!(data instanceof JSONResponse) || data.isErrorStatus()) { + console.log("Catalogue fetch error:", data?.fullMessage?.() || data); + 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) { - console.error("Error fetching catalogue:", err); + console.log("Catalogue fetch exception:", err.response?.data?.fullMessage?.() || err); + ToastWizard.standard("error", "An error occurred", "Please try again later"); } finally { setLoading(false); } @@ -110,7 +110,7 @@ function Catalogue() { sectionTitle={groupName} artefacts={artefacts} /> - ))} + ))} diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx index 43fa782..9290399 100644 --- a/src/pages/PublicGallery.jsx +++ b/src/pages/PublicGallery.jsx @@ -12,15 +12,24 @@ function PublicGallery() { useEffect(() => { async function fetchCatalogue() { try { - const res = await server.get(`${import.meta.env.VITE_BACKEND_URL}/cdn/catalogue`); - if (res && res.status === 200 && res.data?.raw.data) { - const data = res.data.raw.data; - setCategories(data.categories || {}); - } else { - console.error("Failed to fetch catalogue:", res); + const res = await server.get("/cdn/catalogue"); + const data = res.data; + + if (!(data instanceof JSONResponse) || data.isErrorStatus()) { + console.log("Catalogue fetch error:", data?.fullMessage?.() || data); + 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) { - console.error("Error fetching catalogue:", err); + console.log("Catalogue fetch exception:", err.response?.data?.fullMessage?.() || err); + ToastWizard.standard("error", "An error occurred", "Please try again later"); } finally { setLoading(false); } From 09eed71de2eff47a1825393c6fba97575bf313fb Mon Sep 17 00:00:00 2001 From: JunHam Date: Fri, 25 Jul 2025 11:06:17 +0800 Subject: [PATCH 19/20] Added jsonresponse imports --- src/pages/Catalogue.jsx | 2 +- src/pages/PublicGallery.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index a7be702..21e58cb 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -4,7 +4,7 @@ import { AnimatePresence, motion } from "framer-motion"; import MMSection from "../components/Catalogue/mmSection.jsx"; import Section from "../components/Catalogue/section.jsx"; import CentredSpinner from "../components/centredSpinner.jsx"; -import server from "../networking"; +import server, { JSONResponse } from '../networking' import ToastWizard from "../components/toastWizard.js"; function Catalogue() { diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx index 9290399..ea2434d 100644 --- a/src/pages/PublicGallery.jsx +++ b/src/pages/PublicGallery.jsx @@ -1,6 +1,6 @@ import { Box, Text, Center } from "@chakra-ui/react"; import { useEffect, useState } from "react"; -import server from "../networking"; +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"; From 8efc90de97a1b330c06bbf1217762a6cdba09d2f Mon Sep 17 00:00:00 2001 From: JunHam Date: Fri, 25 Jul 2025 14:47:24 +0800 Subject: [PATCH 20/20] Removed console.logs, shifted async function out of useeffect --- src/pages/Catalogue.jsx | 40 ++++++++++++++++++------------------- src/pages/PublicGallery.jsx | 40 ++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index 21e58cb..3a64487 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -14,32 +14,30 @@ function Catalogue() { const [categories, setCategories] = useState({}); const [loading, setLoading] = useState(true); - useEffect(() => { - async function fetchCatalogue() { - try { - const res = await server.get("/cdn/catalogue"); - const data = res.data; - - if (!(data instanceof JSONResponse) || data.isErrorStatus()) { - console.log("Catalogue fetch error:", data?.fullMessage?.() || data); - ToastWizard.standard("error", "An error occurred", "Please try again later"); - return; - } + async function fetchCatalogue() { + try { + const res = await server.get("/cdn/catalogue"); + const data = res.data; - const { categories = {}, books = [] } = data.raw.data || {}; - setCategories(categories); - setBooks(books.map(book => ({ - ...book, - artefacts: book.mmArtefacts || [], - }))); - } catch (err) { - console.log("Catalogue fetch exception:", err.response?.data?.fullMessage?.() || err); + if (!(data instanceof JSONResponse) || data.isErrorStatus()) { ToastWizard.standard("error", "An error occurred", "Please try again later"); - } finally { - setLoading(false); + 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(); }, []); diff --git a/src/pages/PublicGallery.jsx b/src/pages/PublicGallery.jsx index ea2434d..de291d6 100644 --- a/src/pages/PublicGallery.jsx +++ b/src/pages/PublicGallery.jsx @@ -9,32 +9,30 @@ function PublicGallery() { const [categories, setCategories] = useState({}); const [loading, setLoading] = useState(true); - useEffect(() => { - async function fetchCatalogue() { - try { - const res = await server.get("/cdn/catalogue"); - const data = res.data; - - if (!(data instanceof JSONResponse) || data.isErrorStatus()) { - console.log("Catalogue fetch error:", data?.fullMessage?.() || data); - ToastWizard.standard("error", "An error occurred", "Please try again later"); - return; - } + async function fetchCatalogue() { + try { + const res = await server.get("/cdn/catalogue"); + const data = res.data; - const { categories = {}, books = [] } = data.raw.data || {}; - setCategories(categories); - setBooks(books.map(book => ({ - ...book, - artefacts: book.mmArtefacts || [], - }))); - } catch (err) { - console.log("Catalogue fetch exception:", err.response?.data?.fullMessage?.() || err); + if (!(data instanceof JSONResponse) || data.isErrorStatus()) { ToastWizard.standard("error", "An error occurred", "Please try again later"); - } finally { - setLoading(false); + 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(); }, []);