From c0f4ee60ea8fb6f3fc6fbb5acba74466c5471c40 Mon Sep 17 00:00:00 2001 From: JunHam Date: Fri, 15 Aug 2025 00:11:09 +0800 Subject: [PATCH 01/19] Added create group button and dialog --- src/components/DataStudio/createGroup.jsx | 171 ++++++++++++++++++ src/components/DataStudio/groupTypeToggle.jsx | 73 ++++++++ src/pages/Catalogue.jsx | 44 +++-- 3 files changed, 271 insertions(+), 17 deletions(-) create mode 100644 src/components/DataStudio/createGroup.jsx create mode 100644 src/components/DataStudio/groupTypeToggle.jsx diff --git a/src/components/DataStudio/createGroup.jsx b/src/components/DataStudio/createGroup.jsx new file mode 100644 index 0000000..1da9aae --- /dev/null +++ b/src/components/DataStudio/createGroup.jsx @@ -0,0 +1,171 @@ +import { useState } from "react"; +import { + Button, + CloseButton, + Dialog, + Portal, + Field, + Input, + Text, +} from "@chakra-ui/react"; +import { HiPlus } from "react-icons/hi"; +import server, { JSONResponse } from "../../networking"; +import ToastWizard from "../toastWizard"; +import GroupTypeToggle from "./groupTypeToggle"; + +function CreateGroup() { + const [groupType, setGroupType] = useState("book"); + const [title, setTitle] = useState(""); + const [subtitle, setSubtitle] = useState(""); + const [isCreating, setIsCreating] = useState(false); + + const validateForm = () => { + const errors = []; + + if (!title.trim()) { + errors.push("Title is required"); + } else if (title.trim().length > 25) { + errors.push("Title must be 25 characters or less"); + } + + if (subtitle.trim() && subtitle.trim().length > 100) { + errors.push("Subtitle must be 100 characters or less"); + } + + return errors; + }; + + const handleCreateGroup = async () => { + const validationErrors = validateForm(); + if (validationErrors.length > 0) { + ToastWizard.standard( + "error", + "Validation Error", + validationErrors.join(". ") + ); + return; + } + + setIsCreating(true); + try { + const payload = { + title: title.trim(), + subtitle: subtitle.trim() || undefined, + type: groupType, + }; + + const response = await server.post("/group/create", payload); + + if ( + response.data instanceof JSONResponse && + response.data.isErrorStatus() + ) { + throw { response }; + } + + ToastWizard.standard( + "success", + "Group created successfully", + response.data.message + ); + + // Reset form + setTitle(""); + setSubtitle(""); + } catch (err) { + const errorMsg = + err.response?.data?.message || + "Failed to create group. Please try again."; + + ToastWizard.standard("error", "Group creation failed", errorMsg); + } finally { + setIsCreating(false); + } + }; + + const isTitleValid = title.trim().length > 0 && title.trim().length <= 25; + const isSubtitleValid = !subtitle.trim() || subtitle.trim().length <= 100; + const isFormValid = isTitleValid && isSubtitleValid; + + return ( + + + + + + + + + + Create New Group + + + + + + + + + + {groupType === "book" + ? "Book Title" + : "Category Name"} + + setTitle(e.target.value)} + maxLength={25} + /> + + {title.length}/25 characters + + + + + + {groupType === "book" + ? "Subtitle" + : "Description"} + + + setSubtitle(e.target.value) + } + maxLength={100} + /> + + {subtitle.length}/100 characters + + + + + + + + + + + + + + ); +} + +export default CreateGroup; diff --git a/src/components/DataStudio/groupTypeToggle.jsx b/src/components/DataStudio/groupTypeToggle.jsx new file mode 100644 index 0000000..0df4fe1 --- /dev/null +++ b/src/components/DataStudio/groupTypeToggle.jsx @@ -0,0 +1,73 @@ +import { HStack, SegmentGroup, Text, useBreakpointValue } from "@chakra-ui/react"; +import { LuBook, LuTag } from "react-icons/lu"; + +const GroupTypeToggle = ({ value, onChange, size = "md" }) => { + const isMobile = useBreakpointValue({ base: true, md: false }); + + const items = [ + { + value: "book", + label: ( + + + + Book + + + ), + }, + { + value: "category", + label: ( + + + + {isMobile ? "Cat" : "Category"} + + + ), + }, + ]; + + const segmentProps = { + sm: { + p: "1", + rounded: "lg", + width: "180px" + }, + md: { + p: "1", + rounded: "lg", + width: "210px" + }, + lg: { + p: "1.5", + rounded: "lg", + width: "240px" + } + }; + + const currentProps = segmentProps[size] || segmentProps.md; + + return ( + onChange(value)} + size={size} + bg="gray.50" + p={currentProps.p} + rounded={currentProps.rounded} + boxShadow="md" + gap="1" + mb={6} + > + + + + ); +}; + +export default GroupTypeToggle; \ No newline at end of file diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index b406cdf..9491873 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -5,8 +5,9 @@ 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, { JSONResponse } from '../networking' +import server, { JSONResponse } from "../networking"; import ToastWizard from "../components/toastWizard.js"; +import CreateGroup from "../components/DataStudio/createGroup.jsx"; function Catalogue() { const [selectedMMTitle, setSelectedMMTitle] = useState(null); @@ -14,7 +15,7 @@ function Catalogue() { const [books, setBooks] = useState([]); const [categories, setCategories] = useState({}); const [loading, setLoading] = useState(true); - const { loaded } = useSelector(state => state.auth); + const { loaded } = useSelector((state) => state.auth); async function fetchCatalogue() { try { @@ -24,19 +25,22 @@ function Catalogue() { if (response.data.isErrorStatus()) { const errObject = { response: { - data: response.data - } + data: response.data, + }, }; throw errObject; } // Success case - const { categories = {}, books = [] } = response.data.raw.data || {}; + const { categories = {}, books = [] } = + response.data.raw.data || {}; setCategories(categories); - setBooks(books.map(book => ({ - ...book, - artefacts: book.mmArtefacts || [], - }))); + setBooks( + books.map((book) => ({ + ...book, + artefacts: book.mmArtefacts || [], + })) + ); } else { throw new Error("Unexpected response format"); } @@ -44,21 +48,22 @@ function Catalogue() { if (err.response && err.response.data instanceof JSONResponse) { if (err.response.data.userErrorType()) { ToastWizard.standard( - "error", - "Could not load catalogue", - err.response.data.message || "There was a problem loading the catalogue. Please try again later." + "error", + "Could not load catalogue", + err.response.data.message || + "There was a problem loading the catalogue. Please try again later." ); } else { ToastWizard.standard( - "error", - "Something went wrong", + "error", + "Something went wrong", "We are having trouble loading the catalogue. Please try again in a moment." ); } } else { ToastWizard.standard( - "error", - "Connection issue", + "error", + "Connection issue", "We could not connect to the server. Please check your internet connection or try again later." ); } @@ -107,14 +112,19 @@ function Catalogue() { <> {/* Fixed Headers */} Catalogue Browser + + {hasBooks && ( From 40474cc0ee988a976a48fc20f25ceb5b0132c645 Mon Sep 17 00:00:00 2001 From: JunHam Date: Fri, 15 Aug 2025 00:26:11 +0800 Subject: [PATCH 02/19] Refined group creation to call fetchCatalogue after creation --- src/components/DataStudio/createGroup.jsx | 42 ++++++++++++----------- src/pages/Catalogue.jsx | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/components/DataStudio/createGroup.jsx b/src/components/DataStudio/createGroup.jsx index 1da9aae..2732d20 100644 --- a/src/components/DataStudio/createGroup.jsx +++ b/src/components/DataStudio/createGroup.jsx @@ -1,23 +1,16 @@ import { useState } from "react"; -import { - Button, - CloseButton, - Dialog, - Portal, - Field, - Input, - Text, -} from "@chakra-ui/react"; +import { Button, CloseButton, Dialog, Portal, Field, Input, Text } from "@chakra-ui/react"; import { HiPlus } from "react-icons/hi"; import server, { JSONResponse } from "../../networking"; import ToastWizard from "../toastWizard"; import GroupTypeToggle from "./groupTypeToggle"; -function CreateGroup() { +function CreateGroup({ fetchCatalogue }) { const [groupType, setGroupType] = useState("book"); const [title, setTitle] = useState(""); const [subtitle, setSubtitle] = useState(""); const [isCreating, setIsCreating] = useState(false); + const [isOpen, setIsOpen] = useState(false); const validateForm = () => { const errors = []; @@ -28,7 +21,9 @@ function CreateGroup() { errors.push("Title must be 25 characters or less"); } - if (subtitle.trim() && subtitle.trim().length > 100) { + if (!subtitle.trim()) { + errors.push("Subtitle is required"); + } else if (subtitle.trim().length > 100) { errors.push("Subtitle must be 100 characters or less"); } @@ -40,7 +35,7 @@ function CreateGroup() { if (validationErrors.length > 0) { ToastWizard.standard( "error", - "Validation Error", + "All fields must be filled!", validationErrors.join(". ") ); return; @@ -50,7 +45,7 @@ function CreateGroup() { try { const payload = { title: title.trim(), - subtitle: subtitle.trim() || undefined, + subtitle: subtitle.trim(), type: groupType, }; @@ -69,9 +64,12 @@ function CreateGroup() { response.data.message ); - // Reset form + // Reset form and close dialog setTitle(""); setSubtitle(""); + setGroupType("book"); + setIsOpen(false); + fetchCatalogue() } catch (err) { const errorMsg = err.response?.data?.message || @@ -84,11 +82,15 @@ function CreateGroup() { }; const isTitleValid = title.trim().length > 0 && title.trim().length <= 25; - const isSubtitleValid = !subtitle.trim() || subtitle.trim().length <= 100; + const isSubtitleValid = subtitle.trim().length > 0 && subtitle.trim().length <= 100; const isFormValid = isTitleValid && isSubtitleValid; return ( - + setIsOpen(open)} + > + + setIsConfirmSaveOpen(e.open)}> + + + + + + + + + Update Collection + + + + Are you sure you'd like to save these changes? This action cannot be undone. + + + + + + + + + + + + + + + ); +} + +export default GroupViewActionBar; \ No newline at end of file diff --git a/src/components/DataStudio/groupViewDetails.jsx b/src/components/DataStudio/groupViewDetails.jsx index b76fdcc..d18c51f 100644 --- a/src/components/DataStudio/groupViewDetails.jsx +++ b/src/components/DataStudio/groupViewDetails.jsx @@ -1,16 +1,118 @@ -import { Box, Text } from "@chakra-ui/react"; - -const GroupViewDetails = ({ collection, colID }) => ( - - - {collection.name} - - {collection.description && ( - - {collection.description} - - )} - -); - -export default GroupViewDetails; +import { Box, Text, Input, Textarea, Flex, Button, useBreakpointValue } from "@chakra-ui/react"; +import { useState, useEffect } from "react"; +import { MdOutlineEdit, MdOutlineCancel } from "react-icons/md"; +import ToastWizard from "../toastWizard"; +import GroupViewActionBar from "./GroupViewActionBar"; + +const GroupViewDetails = ({ collection, colID, refreshCollectionData }) => { + const [isEditing, setIsEditing] = useState(false); + const [originalCollectionData, setOriginalCollectionData] = useState({}); + const [collectionData, setCollectionData] = useState({}); + const isMobile = useBreakpointValue({ base: true, md: false }); + + // Initialize data when collection prop changes + useEffect(() => { + if (collection) { + const processedData = { + name: collection.name || '', + description: collection.description || '' + }; + setOriginalCollectionData(processedData); + setCollectionData(processedData); + } + }, [collection]); + + const handleEditClick = () => { + setIsEditing(true); + ToastWizard.standard("info", "Editor Mode Enabled", "You are currently in editor mode. Remember to save your changes!", 3000); + }; + + const handleCancelClick = () => { + setCollectionData(originalCollectionData); + setIsEditing(false); + ToastWizard.standard("warning", "Editor Mode Disabled", "All changes reverted.", 3000); + }; + + const handleNameChange = (e) => { + setCollectionData({ + ...collectionData, + name: e.target.value + }); + }; + + const handleDescriptionChange = (e) => { + setCollectionData({ + ...collectionData, + description: e.target.value + }); + }; + + return ( + <> + + + {isEditing ? ( + + ) : ( + + {collectionData.name} + + )} + + + + + {isEditing ? ( +