diff --git a/.gitignore b/.gitignore index 1461e83..7535d4a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ package-lock.json .env /src/pages/test.jsx +/src/components/Old \ No newline at end of file diff --git a/src/components/Catalogue/catalogueItemView.jsx b/src/components/Catalogue/catalogueItemView.jsx index 60f08ab..2afa607 100644 --- a/src/components/Catalogue/catalogueItemView.jsx +++ b/src/components/Catalogue/catalogueItemView.jsx @@ -1,16 +1,18 @@ import { Portal, CloseButton, Image, Box, Text, Icon, Flex, useBreakpointValue } from "@chakra-ui/react"; +import { Tooltip } from "../../components/ui/tooltip" import { useEffect, useState, useRef } from "react"; +import { useNavigate } from "react-router-dom"; import { motion, AnimatePresence } from "framer-motion"; import { FaChevronLeft, FaChevronRight } from "react-icons/fa"; import ItemViewToggle from "./itemViewToggle"; -import ItemViewMenu from "./itemViewMenu"; import MetadataDisplay from "./metadataDisplay"; import ItemChat from "./itemChat"; +import { IoOpen } from "react-icons/io5"; const MotionBox = motion.create(Box); const MotionChevron = motion.create(Box); -const CatalogueItemView = ({ isOpen, onClose, title, items, setDialogTitle, imageSrc }) => { +const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialogTitle, imageSrc }) => { const [isNavigating, setIsNavigating] = useState(false); const [direction, setDirection] = useState(0); const [isInitialRender, setIsInitialRender] = useState(true); @@ -20,6 +22,8 @@ const CatalogueItemView = ({ isOpen, onClose, title, items, setDialogTitle, imag const scrollRef = useRef(null); const modalRef = useRef(null); const currentIndex = items.findIndex((item) => item.title === title); + const currentId = currentIndex !== -1 ? items[currentIndex].id : null; + const navigate = useNavigate() const prevItem = () => { if (!isNavigating && currentIndex > 0) { @@ -357,7 +361,17 @@ const CatalogueItemView = ({ isOpen, onClose, title, items, setDialogTitle, imag > {item.title} - + + + navigate(`/studio/${sectionId}/${currentId}`)} cursor={"pointer"}/> + {title} - + + + navigate(`/studio/${sectionId}/${currentId}`)} cursor={"pointer"}/> + - + {selectedSegment === "metadata" ? ( { - return ( - - - - - - - - - - - Open in Data Studio - - - Metadata Version History - - - - - - ); -}; - -export default ItemViewMenu; \ No newline at end of file diff --git a/src/components/Catalogue/metadataDisplay.jsx b/src/components/Catalogue/metadataDisplay.jsx index 5ab3368..0c33bc3 100644 --- a/src/components/Catalogue/metadataDisplay.jsx +++ b/src/components/Catalogue/metadataDisplay.jsx @@ -249,13 +249,13 @@ function MetadataDisplay({ currentItem, isOpen }) { {metadata.addInfo && ( <> Additional Info: - {metadata.addInfo} + {metadata.addInfo} )} {/* Figure headshots (if available) */} {Array.isArray(metadata.figureIDs) && metadata.figureIDs.length > 0 ? ( - + {metadata.figureIDs.map((id) => ( { +const Section = ({ sectionTitle, sectionId, onItemClick, artefacts = [] }) => { const isMobile = useBreakpointValue({ base: true, md: false }); + const navigate = useNavigate() const items = artefacts.map((art) => ({ id: art.id, @@ -66,15 +69,21 @@ const Section = ({ sectionTitle, onItemClick, artefacts = [] }) => { return ( - - {sectionTitle} - + + + {sectionTitle} + + + + {!isMobile && ( <> @@ -136,6 +145,7 @@ const Section = ({ sectionTitle, onItemClick, artefacts = [] }) => { isOpen={dialogOpen} onClose={() => setDialogOpen(false)} title={dialogTitle} + sectionId={sectionId} items={items} setDialogTitle={setDialogTitle} imageSrc={currentItem?.imageSrc} diff --git a/src/components/DataStudio/ItemGrid.jsx b/src/components/DataStudio/ItemGrid.jsx deleted file mode 100644 index 8fcae2b..0000000 --- a/src/components/DataStudio/ItemGrid.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Box, Grid } from "@chakra-ui/react"; -import CardItem from "../Catalogue/cardItem.jsx"; -import placeholderImage from "../../assets/placeholderImage.png"; -import { useDraggable } from "@dnd-kit/core"; - -const dummyData = Array.from({ length: 24 }, (_, i) => ({ - id: `item-${i}`, - imageSrc: placeholderImage, - title: `Item ${i + 1}`, - description: `This is item ${i + 1}`, -})); - -function DraggableCard({ item }) { - const { attributes, listeners, setNodeRef, isDragging } = useDraggable({ - id: item.id, - data: item, // Pass full item data for DragOverlay - }); - - return ( - - - - ); -} - -function ItemGrid() { - return ( - - - {dummyData.map((item) => ( - - ))} - - - ); -} - -export default ItemGrid; \ No newline at end of file diff --git a/src/components/DataStudio/ItemGroups.jsx b/src/components/DataStudio/ItemGroups.jsx deleted file mode 100644 index ae25348..0000000 --- a/src/components/DataStudio/ItemGroups.jsx +++ /dev/null @@ -1,72 +0,0 @@ -import { Box, Flex } from "@chakra-ui/react"; -import GroupToggle from "./groupToggle"; -import GroupCard from "./groupCard"; -import { useDroppable } from "@dnd-kit/core"; -import { useEffect, useState } from 'react' - -function DroppableGroupCard({ id, title, description, items }) { - const { setNodeRef, isOver } = useDroppable({ id }); - - return ( - - - - ); -} - -function ItemGroups({ droppedItems }) { - const [selectedGroup, setSelectedGroup] = useState("books"); - - const title = "Book of LIFE"; - const description = - "This book contains the life of Mr Joon (AKA B.A.S), a legend forged in caffeine, code, and sheer audacity."; - - return ( - - - - - - - {Array.from({ length: 10 }).map((_, i) => { - const groupId = `group-${i}`; - return ( - - ); - })} - - - ); -} - -export default ItemGroups; \ No newline at end of file diff --git a/src/components/DataStudio/editorCard.jsx b/src/components/DataStudio/editorCard.jsx index 29dbc71..0b1e50b 100644 --- a/src/components/DataStudio/editorCard.jsx +++ b/src/components/DataStudio/editorCard.jsx @@ -1,4 +1,4 @@ -import { Card, Button, Flex, Text, Field, Input, Textarea, Box } from "@chakra-ui/react"; +import { Card, Button, Flex, Text, Field, Input, Textarea, Box, useBreakpointValue } from "@chakra-ui/react"; import { useState, useEffect, useRef } from "react"; import { IoInformationCircleOutline } from "react-icons/io5"; import { MdOutlineEdit, MdOutlineCancel } from "react-icons/md"; @@ -96,6 +96,9 @@ function EditorCard({ metadata, artefactId, refreshArtefactData }) { const [originalArtefactData, setOriginalArtefactData] = useState({}); const [artefactData, setArtefactData] = useState({}); + // Responsive layout + const isMobile = useBreakpointValue({ base: true, md: false }); + // Initialize data when metadata prop changes useEffect(() => { if (metadata) { @@ -155,16 +158,16 @@ function EditorCard({ metadata, artefactId, refreshArtefactData }) { return ( <> - - + + Artefact Details - - - @@ -174,8 +177,8 @@ function EditorCard({ metadata, artefactId, refreshArtefactData }) { {isMMArtefact ? ( <> - - Name: + + Name: {isEditing ? ( {/* MM Artefact Specific Fields */} - - Transcription: + + Transcription: @@ -248,8 +252,8 @@ function EditorCard({ metadata, artefactId, refreshArtefactData }) { ) : ( - - Name: + + Name: {isEditing ? ( 50 ? description.slice(0, 50) + "..." : description; - - return ( - - - - {title} - - - {truncatedDesc} - - - - - - - ); -} - -export default GroupCard; \ No newline at end of file diff --git a/src/components/DataStudio/groupCardItem.jsx b/src/components/DataStudio/groupCardItem.jsx new file mode 100644 index 0000000..b7315e2 --- /dev/null +++ b/src/components/DataStudio/groupCardItem.jsx @@ -0,0 +1,25 @@ +import { Box } from "@chakra-ui/react"; +import CardComponent from "../Catalogue/cardComponent"; + +const GroupCardItem = ({ itemTitle, itemDescription, imageSrc }) => { + + return ( + <> + {/* Base Card Container */} + + + + + ); +}; + +export default GroupCardItem; \ No newline at end of file diff --git a/src/components/DataStudio/groupItemsSection.jsx b/src/components/DataStudio/groupItemsSection.jsx deleted file mode 100644 index 6643368..0000000 --- a/src/components/DataStudio/groupItemsSection.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Box, Image, Flex } from "@chakra-ui/react"; -import { useRef, useState, useEffect } from "react"; -import ArrowOverlay from "../Catalogue/arrowOverlay"; -import placeholderImage from "../../assets/placeholderImage.png"; - -function GroupItemsSection() { - const scrollRef = useRef(null); - const [atStart, setAtStart] = useState(true); - const [atEnd, setAtEnd] = useState(false); - const [isOverflowing, setIsOverflowing] = useState(false); - - const items = Array.from({ length: 12 }, (_, i) => ({ - id: i + 1, - imageSrc: placeholderImage, - name: `Item ${i + 1}`, - })); - - const checkScrollEdges = () => { - const node = scrollRef.current; - if (!node) return; - - setAtStart(node.scrollLeft === 0); - setAtEnd( - Math.ceil(node.scrollLeft + node.clientWidth) >= node.scrollWidth - ); - - setIsOverflowing(node.scrollWidth > node.clientWidth); - }; - - const scroll = (direction) => { - if (!scrollRef.current) return; - const scrollAmount = 300; - scrollRef.current.scrollBy({ - left: direction === "left" ? -scrollAmount : scrollAmount, - behavior: "smooth", - }); - }; - - useEffect(() => { - const node = scrollRef.current; - if (!node) return; - - checkScrollEdges(); - node.addEventListener("scroll", checkScrollEdges); - window.addEventListener("resize", checkScrollEdges); - - return () => { - node.removeEventListener("scroll", checkScrollEdges); - window.removeEventListener("resize", checkScrollEdges); - }; - }, [items]); - - return ( - - {/* Arrow overlays with small size */} - scroll("left")} - isOverflowing={isOverflowing && !atStart} - smaller={true} - /> - scroll("right")} - isOverflowing={isOverflowing && !atEnd} - smaller={true} - /> - - {/* Scrollable items container */} - - {items.map((item) => ( - - {item.name} - - ))} - - - ); -} - -export default GroupItemsSection; diff --git a/src/components/DataStudio/groupToggle.jsx b/src/components/DataStudio/groupToggle.jsx deleted file mode 100644 index 9cd9a88..0000000 --- a/src/components/DataStudio/groupToggle.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import { HStack, SegmentGroup, Text } from "@chakra-ui/react"; -import { FaBookOpen, FaImages, FaCubes } from "react-icons/fa"; - -const GroupToggle = ({ value, onChange }) => { - const items = [ - { - value: "books", - label: ( - - - - Books - - - ), - }, - { - value: "categories", - label: ( - - - - Categories - - - ), - }, - { - value: "batch", - label: ( - - - - Batch - - - ), - }, - ]; - - return ( - onChange(value)} - size="lg" - bg="gray.50" - p="1" - rounded="l" - boxShadow="md" - gap="1" - > - - - - ); -}; - -export default GroupToggle; diff --git a/src/components/DataStudio/metadataToggle.jsx b/src/components/DataStudio/metadataToggle.jsx index 416e122..91a4eb9 100644 --- a/src/components/DataStudio/metadataToggle.jsx +++ b/src/components/DataStudio/metadataToggle.jsx @@ -1,6 +1,6 @@ -import { SegmentGroup, Text } from "@chakra-ui/react"; +import { SegmentGroup, Text, useBreakpointValue } from "@chakra-ui/react"; -const MetadataToggle = ({ value, onChange }) => { +const MetadataToggle = ({ value, onChange, isMobile }) => { const items = [ { value: "tradCN", label: "Trad CN" }, { value: "simplifiedCN", label: "Simp CN" }, @@ -8,13 +8,18 @@ const MetadataToggle = ({ value, onChange }) => { { value: "summary", label: "SUM" }, ]; + // Responsive sizing + const segmentSize = useBreakpointValue({ base: "sm", md: "md" }); + const fontSize = useBreakpointValue({ base: "xs", md: "sm" }); + const padding = useBreakpointValue({ base: "0.5", md: "1" }); + return ( onChange(value)} - size="sm" + size={segmentSize} bg="gray.100" - p="1" + p={padding} rounded="lg" boxShadow="sm" w="100%" @@ -24,7 +29,7 @@ const MetadataToggle = ({ value, onChange }) => { { value: item.value, label: ( {item.label} @@ -50,6 +57,7 @@ const MetadataToggle = ({ value, onChange }) => { display: "flex", justifyContent: "center", alignItems: "center", + padding: isMobile ? "0.25rem" : "0.5rem", }, }))} /> diff --git a/src/main.jsx b/src/main.jsx index 5500b43..4f79425 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -21,7 +21,6 @@ import AnimateIn from './AnimateIn.jsx'; import DataImport from './pages/DataImport.jsx'; import PublicGallery from './pages/PublicGallery.jsx'; import PublicProfile from './pages/PublicProfile.jsx'; -import DataStudio from './pages/DataStudio.jsx'; import ArtefactEditor from './pages/ArtefactEditor.jsx'; import GroupView from './pages/GroupView.jsx'; import Profile from './pages/Profile.jsx'; @@ -60,7 +59,6 @@ createRoot(document.getElementById('root')).render( } /> - } /> } /> } /> diff --git a/src/pages/ArtefactEditor.jsx b/src/pages/ArtefactEditor.jsx index e1f30c5..c101e17 100644 --- a/src/pages/ArtefactEditor.jsx +++ b/src/pages/ArtefactEditor.jsx @@ -1,6 +1,6 @@ -import { Box, Flex, Text, Image } from "@chakra-ui/react"; -import { useParams } from 'react-router-dom'; -import { useEffect, useRef, useState } from "react"; +import { Box, Flex, Text, Image, useBreakpointValue } from "@chakra-ui/react"; +import { useParams, useNavigate } from 'react-router-dom'; +import { useEffect, useState } from "react"; import { useSelector } from "react-redux"; import ToastWizard from "../components/toastWizard"; import server, { JSONResponse } from "../networking"; @@ -10,9 +10,112 @@ import EditorCard from "../components/DataStudio/editorCard"; import { FaCircleArrowLeft } from "react-icons/fa6"; function ArtefactEditor() { - const { artID } = useParams(); + const { colID, artID } = useParams(); const [metadata, setMetadata] = useState(null); + const [collection, setCollection] = useState(null); const { loaded } = useSelector(state => state.auth); + const navigate = useNavigate(); + const [isInvalid, setIsInvalid] = useState(false); + + // Responsive layout values + const isMobile = useBreakpointValue({ base: true, md: false }); + const imageWidth = useBreakpointValue({ base: "100%", md: "46%" }); + const editorWidth = useBreakpointValue({ base: "100%", md: "46%" }); + const chevronWidth = useBreakpointValue({ base: "8%", md: "4%" }); + + // Get the list of artefact IDs from collection data + const getArtefactIds = () => { + // collection is now always an array of IDs + return Array.isArray(collection) ? collection : []; + }; + + // Get current artefact index and navigation info + const getNavigationInfo = () => { + const artefactIds = getArtefactIds(); + const currentIndex = artefactIds.indexOf(artID); + + return { + artefactIds, + currentIndex, + isFirst: currentIndex <= 0, + isLast: currentIndex >= artefactIds.length - 1, + hasPrevious: currentIndex > 0, + hasNext: currentIndex < artefactIds.length - 1 + }; + }; + + const navigateToPrevious = () => { + const { artefactIds, currentIndex, hasPrevious } = getNavigationInfo(); + if (hasPrevious) { + navigate(`/studio/${colID}/${artefactIds[currentIndex - 1]}`, { replace: true }); + } + }; + + const navigateToNext = () => { + const { artefactIds, currentIndex, hasNext } = getNavigationInfo(); + if (hasNext) { + navigate(`/studio/${colID}/${artefactIds[currentIndex + 1]}`, { replace: true }); + } + }; + + // Collection items fetching function + const fetchCollection = async () => { + try { + const response = await server.get(`/cdn/collectionMemberIDs/${colID}`); + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case + setCollection(response.data.raw.data) + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in login request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard( + "error", + "An Error Occured", + err.response.data.message || "We could not fetch the collection data. Please try again later.", + 3000, + true, + fetchCollection, + 'Retry' + ); + } else { + ToastWizard.standard( + "error", + "An Error Occured", + "We could not fetch the collection data. Please try again later.", + 3000, + true, + fetchCollection, + 'Retry' + ); + } + } else { + console.log("Unexpected error in fetching collection:", err); + ToastWizard.standard( + "error", + "An Error Occured", + "We could not fetch the collection data. Please try again later.", + 3000, + true, + fetchCollection, + 'Retry' + ); + } + } + } // Metadata fetching function const fetchMetadata = async () => { @@ -74,51 +177,130 @@ function ArtefactEditor() { } }; - // Fetch metadata if user is loaded + // Fetch collection once after load useEffect(() => { if (loaded) { - fetchMetadata() + fetchCollection(); } - }, [loaded]); + }, [loaded, colID]); + + // Always fetch metadata when artID changes + useEffect(() => { + if (loaded && artID) { + fetchMetadata(); + } + }, [artID, loaded]); + + useEffect(() => { + if (collection && metadata) { + const isInCollection = Array.isArray(collection) && collection.includes(artID); + + if (!isInCollection) { + setIsInvalid(true); + setTimeout(() => { + ToastWizard.standard( + "error", + "An Error Occured", + "Artefact and collection does not exist." + ); + }, 0); + } + } + }, [collection, metadata, artID]); // Show loading spinner while data is being fetched - if (!metadata) { + if (!metadata || !collection || isInvalid) { return } + const { isFirst, isLast } = getNavigationInfo(); + return ( - + - + navigate(-1)} size={40} color='darkBlue' /> Data Studio {metadata.name} - - - + {isMobile ? ( + + + + + + + + + + + + + + + + + + + ) : ( + + + + - - - + + + - - - + + + - - + + + - + )} ); } diff --git a/src/pages/Catalogue.jsx b/src/pages/Catalogue.jsx index 3dd990c..b406cdf 100644 --- a/src/pages/Catalogue.jsx +++ b/src/pages/Catalogue.jsx @@ -137,6 +137,7 @@ function Catalogue() {
@@ -145,11 +146,12 @@ function Catalogue() { {hasCategories && - Object.entries(categories).map(([groupName, artefacts]) => ( + Object.entries(categories).map(([catID, catObj]) => (
))} diff --git a/src/pages/DataStudio.jsx b/src/pages/DataStudio.jsx deleted file mode 100644 index 5efb649..0000000 --- a/src/pages/DataStudio.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import { DndContext, DragOverlay } from "@dnd-kit/core"; -import { useState } from "react"; -import { Box, Flex, Text } from "@chakra-ui/react"; -import ItemGrid from "../components/DataStudio/ItemGrid.jsx"; -import ItemGroups from "../components/DataStudio/ItemGroups.jsx"; -import CardItem from "../components/Catalogue/cardItem.jsx"; - -function DataStudio() { - const [droppedItems, setDroppedItems] = useState({}); - const [activeItem, setActiveItem] = useState(null); - - const handleDragStart = (event) => { - setActiveItem(event.active.data.current); - }; - - const handleDragEnd = (event) => { - const { active, over } = event; - setActiveItem(null); - - if (over && active) { - const groupId = over.id; - const itemId = active.id; - - setDroppedItems((prev) => { - // Avoid duplicates by checking if item already exists in the group - const existingItems = prev[groupId] || []; - if (existingItems.some((item) => item.id === itemId)) { - return prev; - } - - return { - ...prev, - [groupId]: [...existingItems, active.data.current], - }; - }); - } - }; - - return ( - - - {/* Left Panel (Draggable Items) */} - - - Data Studio - Batch - - - - - - - {/* Right Panel (Droppable Groups) */} - - - - - - {/* Drag Preview */} - - {activeItem ? ( - - - - ) : null} - - - ); -} - -export default DataStudio; \ No newline at end of file diff --git a/src/pages/GroupView.jsx b/src/pages/GroupView.jsx index d3fbe4d..f7dbc72 100644 --- a/src/pages/GroupView.jsx +++ b/src/pages/GroupView.jsx @@ -1,9 +1,180 @@ -import { useParams } from 'react-router-dom'; +import { Flex, Box, Text, Grid } from '@chakra-ui/react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { FaCircleArrowLeft } from 'react-icons/fa6'; +import { useEffect, useState } from 'react'; +import CentredSpinner from '../components/CentredSpinner'; +import server, { JSONResponse } from '../networking' +import ToastWizard from '../components/toastWizard'; +import GroupCardItem from '../components/DataStudio/groupCardItem'; function GroupView() { const { colID } = useParams(); + const navigate = useNavigate(); + const [collection, setCollection] = useState(null); + const [loading, setLoading] = useState(true); - return
GroupView for collection {colID}
; + useEffect(() => { + const fetchCollection = async () => { + try { + setLoading(true); + const response = await server.get(`/cdn/collection/${colID}`); + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw errObject; + } + + // Success case - format the data + let formattedData; + const apiData = response.data.raw.data; + + if (apiData.type === 'book') { + formattedData = { + type: 'book', + name: apiData.title, + description: apiData.subtitle || '', + items: apiData.mmArtefacts.map(art => ({ + id: art.id, + title: art.name, + description: art.description, + image: art.image || null + })) + }; + } else if (apiData.type === 'category') { + formattedData = { + type: 'category', + name: apiData.name, + description: apiData.description || '', + items: apiData.members.map(art => ({ + id: art.id, + title: art.name, + description: art.description, + image: art.image + })) + }; + } else if (apiData.type === 'batch') { + formattedData = { + type: 'batch', + name: apiData.name || `Batch ${apiData.id}`, + description: apiData.description || '', + items: apiData.artefacts.map(art => ({ + id: art.id, + title: art.name, + description: art.description, + image: art.image || null + })) + }; + } else { + throw new Error('Unknown collection type'); + } + + setCollection(formattedData); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in collection request:", err.response.data.fullMessage()); + + if (err.response.data.userErrorType()) { + ToastWizard.standard( + "error", + "An Error Occured", + "We could not fetch the collection data. Please try again later.", + 3000, + true, + fetchCollection, + 'Retry' + ); + } else { + ToastWizard.standard( + "error", + "An Error Occured", + "We could not fetch the collection data. Please try again later.", + 3000, + true, + fetchCollection, + 'Retry' + ); + } + } else { + console.log("Unexpected error in collection request:", err); + ToastWizard.standard( + "error", + "An Error Occured", + "We could not fetch the collection data. Please try again later.", + 3000, + true, + fetchCollection, + 'Retry' + ); + } + } finally { + setLoading(false); + } + }; + + fetchCollection(); + }, [colID]); + + if (loading || !collection ) return ; + + return ( + + {/* Header with back button */} + + navigate(-1)} + size={40} + color="darkBlue" + /> + + Group View + + + + + {collection.name} + {collection.description && ( + + {collection.description} + + )} + + + {/* Items grid */} + + {collection.items.map(item => ( + + ))} + + + {collection.items.length === 0 && ( + + This collection has no items + + )} + + ); } -export default GroupView; +export default GroupView; \ No newline at end of file