diff --git a/README.md b/README.md index 24dbe20..171020f 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Here's a glimpse of what TCG Enhanced Cardstore offers: ### Cart -![Cart](public/images/pages/deckBuilder.png) +![Cart](https://github.com/reedoooo/enhanced-card-store/blob/733fee5d8ab28ca034f9f53bb56a8a5aee5330cb/public/images/pages/cart.png) _For an interactive experience, visit the [Live Demo](https://65624888827a3700084a3478--enhanced-cardstore.netlify.app/)._ diff --git a/src/assets/cleanup/CardActionButtons.jsx b/src/assets/cleanup/CardActionButtons.jsx new file mode 100644 index 0000000..583e1a8 --- /dev/null +++ b/src/assets/cleanup/CardActionButtons.jsx @@ -0,0 +1,192 @@ +// import React, { useCallback } from 'react'; +// import { +// Alert, +// Box, +// Button, +// CardActions, +// Grid, +// IconButton, +// Typography, +// useMediaQuery, +// } from '@mui/material'; +// import { +// AddCircleOutlineOutlined, +// RemoveCircleOutlineOutlined, +// } from '@mui/icons-material'; +// import { useMode } from '../../../context/hooks/colormode'; +// import { useCollectionStore } from '../../../context/CollectionContext/CollectionContext'; +// import { useDeckStore } from '../../../context/DeckContext/DeckContext'; +// import { useCartStore } from '../../../context/CartContext/CartContext'; +// import Logger from '../../reusable/Logger'; +// const cardOtherLogger = new Logger([ +// 'Action', +// 'Card Name', +// 'Quantity', +// 'Total Price', +// ]); + +// const CardActionButtons = ({ card, context, closeModal, page }) => { +// const { theme } = useMode(); +// const { addOneToCollection, removeOneFromCollection, selectedCollection } = +// useCollectionStore(); +// const { addOneToDeck, removeOneFromDeck, selectedDeck, allDecks } = +// useDeckStore(); +// const { addOneToCart, removeOneFromCart, cartData } = useCartStore(); +// const isXSmallScreen = useMediaQuery(theme.breakpoints.down('xs')); +// const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); + +// const isCardInContext = useCallback(() => { +// switch (context) { +// case 'Collection': +// return !!selectedCollection?.cards?.find((c) => c?.id === card?.id); +// case 'Deck': +// return !!selectedDeck?.cards?.find((c) => c?.id === card?.id); +// case 'Cart': +// return !!cartData?.cart?.find((c) => c?.id === card?.id); +// default: +// return false; +// } +// }, [card.id, context, selectedCollection, selectedDeck, cartData]); + +// const performAction = useCallback( +// (action) => { +// try { +// if (context === 'Collection') { +// if (!selectedCollection) { +// +// +// No Collection Selected, Please Select A Collection +// +//

+// Please select a collection to add cards to. You can do this by +// clicking one of the "Deck Icon" buttons in the +// dropdown menu. +//

+//
; +// } +// action === 'add' +// ? addOneToCollection(card, selectedCollection) +// : removeOneFromCollection(card, selectedCollection); +// } else if (context === 'Deck') { +// if (!selectedDeck) { +// +// +// No Deck Selected, Please Select A Deck +// +//

+// Please select a deck to add cards to. You can do this by +// clicking one of the "Deck Icon" buttons in the +// dropdown menu. +//

+//
; +// } +// action === 'add' +// ? addOneToDeck(card, selectedDeck?._id || allDecks[0]?._id) +// : removeOneFromDeck(selectedDeck?._id, card); +// } else if (context === 'Cart' || context === 'Store') { +// action === 'add' ? addOneToCart(card) : removeOneFromCart(card); +// } + +// cardOtherLogger.logCardAction(`${action} Card`, card); +// } catch (error) { +// console.error(`Error performing '${action}' action`, error); +// } +// }, +// [ +// context, +// card, +// addOneToCollection, +// removeOneFromCollection, +// addOneToDeck, +// removeOneFromDeck, +// addOneToCart, +// removeOneFromCart, +// ] +// ); + +// const handleAddClick = () => { +// performAction('add'); +// closeModal?.(); +// }; +// const handleRemoveOne = () => { +// performAction('remove'); +// closeModal?.(); +// }; +// const getButtonLabel = () => { +// if (isXSmallScreen) { +// return ''; // No text for very small screens +// } else if (isSmallScreen) { +// return 'Add'; // Shortened text for small screens +// } else { +// return `Add to ${context}`; // Full text for larger screens +// } +// }; + +// const buttonStyles = { +// addButton: { +// color: theme.palette.success.contrastText, +// backgroundColor: theme.palette.success.main, +// '&:hover': { +// backgroundColor: theme.palette.success.darker, +// }, +// }, +// removeButton: { +// color: theme.palette.error.contrastText, +// backgroundColor: theme.palette.error.main, +// '&:hover': { +// backgroundColor: theme.palette.error.dark, +// }, +// }, +// }; +// return ( +// +// +// {isCardInContext() ? ( +// +// +// +// +// +// +// +// +// +// +// +// +// ) : ( +// +// )} +// +// +// ); +// }; + +// export default CardActionButtons; diff --git a/src/assets/cleanup/formStyles.jsx b/src/assets/cleanup/formStyles.jsx new file mode 100644 index 0000000..8f9b7a2 --- /dev/null +++ b/src/assets/cleanup/formStyles.jsx @@ -0,0 +1,23 @@ +// import styled from 'styled-components'; +// import { Button, TextField } from '@mui/material'; + +// export const FormWrapper = styled('form')` +// display: flex; +// flex-direction: column; +// gap: 20px; +// max-width: 400px; +// margin: auto; +// `; + +// export const StyledTextField = styled(TextField)` +// && { +// margin-bottom: 12px; +// width: 100%; // Ensures that the text fields take the full width +// } +// `; + +// export const StyledButton = styled(Button)` +// && { +// margin-top: 16px; +// } +// `; diff --git a/src/assets/styles/App.scss b/src/assets/styles/App.scss index 10e9c28..c99d447 100644 --- a/src/assets/styles/App.scss +++ b/src/assets/styles/App.scss @@ -291,7 +291,7 @@ div.frame-container.container { .frame { z-index: 1; - min-width: 100%; + // min-width: 100%; max-height: 100%; padding: 0; bottom: 0; @@ -387,7 +387,7 @@ button:focus { justify-content: center; margin: auto; // min-width: 100vh; - max-width: 100vw; + // max-width: 100vw; // width: 100%; font: 12px/18px 'opensans-bold', diff --git a/src/components/buttons/actionButtons/CardActionButtons.jsx b/src/components/buttons/actionButtons/CardActionButtons.jsx deleted file mode 100644 index ac41129..0000000 --- a/src/components/buttons/actionButtons/CardActionButtons.jsx +++ /dev/null @@ -1,194 +0,0 @@ -import React, { useCallback } from 'react'; -import { - Alert, - Box, - Button, - CardActions, - Grid, - IconButton, - Typography, - useMediaQuery, -} from '@mui/material'; -import { - AddCircleOutlineOutlined, - RemoveCircleOutlineOutlined, -} from '@mui/icons-material'; -import { useMode } from '../../../context/hooks/colormode'; -import { useCollectionStore } from '../../../context/CollectionContext/CollectionContext'; -import { useDeckStore } from '../../../context/DeckContext/DeckContext'; -import { useCartStore } from '../../../context/CartContext/CartContext'; -import Logger from '../../reusable/Logger'; -import { Container } from 'react-bootstrap'; - -const cardOtherLogger = new Logger([ - 'Action', - 'Card Name', - 'Quantity', - 'Total Price', -]); - -const CardActionButtons = ({ card, context, closeModal, page }) => { - const { theme } = useMode(); - const { addOneToCollection, removeOneFromCollection, selectedCollection } = - useCollectionStore(); - const { addOneToDeck, removeOneFromDeck, selectedDeck, allDecks } = - useDeckStore(); - const { addOneToCart, removeOneFromCart, cartData } = useCartStore(); - const isXSmallScreen = useMediaQuery(theme.breakpoints.down('xs')); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - - const isCardInContext = useCallback(() => { - switch (context) { - case 'Collection': - return !!selectedCollection?.cards?.find((c) => c?.id === card?.id); - case 'Deck': - return !!selectedDeck?.cards?.find((c) => c?.id === card?.id); - case 'Cart': - return !!cartData?.cart?.find((c) => c?.id === card?.id); - default: - return false; - } - }, [card.id, context, selectedCollection, selectedDeck, cartData]); - - const performAction = useCallback( - (action) => { - try { - if (context === 'Collection') { - if (!selectedCollection) { - - - No Collection Selected, Please Select A Collection - -

- Please select a collection to add cards to. You can do this by - clicking one of the "Deck Icon" buttons in the - dropdown menu. -

-
; - } - action === 'add' - ? addOneToCollection(card, selectedCollection) - : removeOneFromCollection(card, selectedCollection); - } else if (context === 'Deck') { - if (!selectedDeck) { - - - No Deck Selected, Please Select A Deck - -

- Please select a deck to add cards to. You can do this by - clicking one of the "Deck Icon" buttons in the - dropdown menu. -

-
; - } - action === 'add' - ? addOneToDeck(card, selectedDeck?._id || allDecks[0]?._id) - : removeOneFromDeck(selectedDeck?._id, card); - } else if (context === 'Cart' || context === 'Store') { - action === 'add' ? addOneToCart(card) : removeOneFromCart(card); - } - - cardOtherLogger.logCardAction(`${action} Card`, card); - } catch (error) { - console.error(`Error performing '${action}' action`, error); - } - }, - [ - context, - card, - addOneToCollection, - removeOneFromCollection, - addOneToDeck, - removeOneFromDeck, - addOneToCart, - removeOneFromCart, - ] - ); - - const handleAddClick = () => { - performAction('add'); - closeModal?.(); - }; - const handleRemoveOne = () => { - performAction('remove'); - closeModal?.(); - }; - const getButtonLabel = () => { - if (isXSmallScreen) { - return ''; // No text for very small screens - } else if (isSmallScreen) { - return 'Add'; // Shortened text for small screens - } else { - return `Add to ${context}`; // Full text for larger screens - } - }; - - const buttonStyles = { - addButton: { - color: theme.palette.success.contrastText, - backgroundColor: theme.palette.success.main, - '&:hover': { - backgroundColor: theme.palette.success.darker, - }, - }, - removeButton: { - color: theme.palette.error.contrastText, - backgroundColor: theme.palette.error.main, - '&:hover': { - backgroundColor: theme.palette.error.dark, - }, - }, - }; - return ( - - - {isCardInContext() ? ( - - - - - - - - - - - - - ) : ( - - )} - - - ); -}; - -export default CardActionButtons; diff --git a/src/components/buttons/actionButtons/GenericActionButtons.jsx b/src/components/buttons/actionButtons/GenericActionButtons.jsx index 5d9ee03..a414560 100644 --- a/src/components/buttons/actionButtons/GenericActionButtons.jsx +++ b/src/components/buttons/actionButtons/GenericActionButtons.jsx @@ -1,70 +1,248 @@ -import React, { useContext } from 'react'; -import CardActionButtons from './CardActionButtons'; +import React, { useCallback, useState } from 'react'; import useAppContext from '../../../context/hooks/useAppContext'; -import { ModalContext } from '../../../context/ModalContext/ModalContext'; -import { Box, CardActions } from '@mui/material'; - +import { useModalContext } from '../../../context/ModalContext/ModalContext'; +import { + Box, + CardActions, + Alert, + Button, + Grid, + IconButton, + Typography, + useMediaQuery, + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions, +} from '@mui/material'; +import { + AddCircleOutlineOutlined, + RemoveCircleOutlineOutlined, +} from '@mui/icons-material'; +import { useMode } from '../../../context/hooks/colormode'; +import { useCollectionStore } from '../../../context/CollectionContext/CollectionContext'; +import { useDeckStore } from '../../../context/DeckContext/DeckContext'; +import { useCartStore } from '../../../context/CartContext/CartContext'; +import Logger from '../../reusable/Logger'; +import useSelectedContext from '../../../context/hooks/useSelectedContext'; +import useResponsiveStyles from '../../../context/hooks/useResponsiveStyles'; +import { useGenericActionButtonStyles } from '../../../context/hooks/useGenericActionButtonStyles'; +import { useSelectionDialog } from '../../../context/hooks/useSelectionDialog'; +import { useCardActions } from '../../../context/hooks/useCardActions'; +const cardOtherLogger = new Logger([ + 'Action', + 'Card Name', + 'Quantity', + 'Total Price', +]); const GenericActionButtons = ({ card, context, + onClick, // New onClick prop for handling context selection + originalContext, onSuccess, onFailure, page, }) => { const contextProps = useAppContext(); // Assuming useAppContext returns the context object - const { closeModal, isModalOpen, setModalOpen } = useContext(ModalContext); + const { closeModal, isModalOpen, setModalOpen } = useModalContext(); + const { theme } = useMode(); + const { + addOneToCollection, + removeOneFromCollection, + selectedCollection, + allCollections, + setSelectedCollection, + } = useCollectionStore(); + const { + addOneToDeck, + removeOneFromDeck, + selectedDeck, + allDecks, + setSelectedDeck, + } = useDeckStore(); + const { addOneToCart, removeOneFromCart, cartData } = useCartStore(); + const { + isXSmall, + isMobile, + isMedium, + isLarge, + getButtonTypographyVariant2, + getButtonTypographyVariant, + } = useResponsiveStyles(theme); + const performAction = useCardActions( + context, + card, + selectedCollection, + selectedDeck, + addOneToCollection, + removeOneFromCollection, + addOneToDeck, + removeOneFromDeck, + addOneToCart, + removeOneFromCart, + onSuccess, + onFailure + ); + const styles = useGenericActionButtonStyles(theme); + const { + selectDialogOpen, + itemsForSelection, + openSelectionDialog, + handleSelection, + setSelectDialogOpen, + } = useSelectionDialog( + context, + selectedCollection, + selectedDeck, + allCollections, + allDecks + ); - if (!contextProps) { - return ( - Provider not found for {context} - ); - } + const isCardInContext = useCallback(() => { + const cardsList = { + Collection: selectedCollection?.cards, + Deck: selectedDeck?.cards, + Cart: cartData?.cart, + }; + return !!cardsList[context]?.find((c) => c?.id === card?.id); + }, [context, card.id, selectedCollection, selectedDeck, cartData]); - let modifiedContextProps = contextProps[context]; + const handleAddClick = () => { + onClick?.(); // Set the selected context when adding + performAction('add'); + }; + const handleRemoveOne = () => { + performAction('remove'); + closeModal?.(); + }; + const renderSelectionDialog = () => ( + setSelectDialogOpen(false)}> + Select {context} + + {/* Render the items for selection */} + {itemsForSelection.map((item) => ( + + ))} + + + ); + const getButtonLabel = () => { + // If modal is open and screen is not large, return context only + if (isModalOpen && !isLarge) { + return `${context}`; + } + // For large screens or when modal is not open, provide more detailed text + return `Add to ${context}`; + }; - if (!modifiedContextProps) { - return ( - - Invalid context provided: {context} - - ); - } + // const getButtonLabel = () => { + // if (isXSmall && isModalOpen) { + // return `${context}`; // Abbreviated text for medium-large screens + // } + // if (isMobile && isModalOpen) { + // return `${context}`; // Abbreviated text for medium-large screens + // } + // if (isMedium && isModalOpen) { + // return `${context}`; // Abbreviated text for medium-large screens + // } + // if (isLarge && isModalOpen) { + // return `Add to ${context}`; // Full text for larger screens + // } + + // if (isXSmall && !isModalOpen) { + // return `${context}`; // Shortened text for small screens + // } + // if (isMobile && !isModalOpen) { + // return `${context}`; // Shortened text for small screens + // } + // if (isMedium && !isModalOpen) { + // return `Add to ${context}`; // Full text for larger screens + // } + // if (isLarge && !isModalOpen) { + // return `${context}`; // Shortened text for small screens + // } + // }; return ( - - - + {renderSelectionDialog()} + + - setModalOpen(false)} - open={isModalOpen} - /> - - - + {isCardInContext(context) ? ( + + + + + {`${context}`} + + + {/* */} + + + + + + + + + + + {/* */} + + + ) : ( + + )} + + + ); }; diff --git a/src/components/cards/GenericCard.jsx b/src/components/cards/GenericCard.jsx index 2fac485..0acb835 100644 --- a/src/components/cards/GenericCard.jsx +++ b/src/components/cards/GenericCard.jsx @@ -1,14 +1,6 @@ import React, { useCallback, useContext, useEffect, useRef } from 'react'; -import { - Card, - CardContent, - CardActions, - Typography, - styled, - useMediaQuery, - Container, -} from '@mui/material'; -import CardMediaSection from '../media/CardMediaSection'; +import { CardActions, Typography } from '@mui/material'; +import CardMediaSection from './media/CardMediaSection'; import GenericActionButtons from '../buttons/actionButtons/GenericActionButtons'; import placeholderImage from '../../assets/images/placeholder.jpeg'; import { useModalContext } from '../../context/ModalContext/ModalContext'; @@ -19,82 +11,67 @@ import { useCartStore } from '../../context/CartContext/CartContext'; import { useCollectionStore } from '../../context/CollectionContext/CollectionContext'; import { useDeckStore } from '../../context/DeckContext/DeckContext'; import { useTheme } from '@mui/styles'; -const AspectRatioBox = styled('div')(({ theme }) => ({ - width: '100%', // Full width of the parent container - position: 'relative', -})); - -const StyledCard = styled(Card)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - minWidth: '120px', // Adjusted for better responsiveness - maxWidth: '100%', - width: 'auto', - maxHeight: '100%', // Adjusted for better height management - flexGrow: 1, - backgroundColor: theme.palette.background.paper, - borderRadius: theme.shape.borderRadius, - boxShadow: theme.shadows[5], - transition: 'transform 0.3s ease-in-out', - '&:hover': { - transform: 'scale(1.03)', - }, -})); +import { + AspectRatioBox, + QuantityLine, + StyledCard, + StyledCardContent, +} from './cardStyledComponents'; const GenericCard = React.forwardRef((props, ref) => { + const { card, context, page } = props; + const { theme } = useMode(); const theme2 = useTheme(); const cardRef = useRef(null); - const { card, context, page } = props; const { cartData } = useCartStore(); - const { selectedCollection } = useCollectionStore(); - const { selectedDeck } = useDeckStore(); + const { selectedCollection, allCollections } = useCollectionStore(); + const { selectedDeck, allDecks } = useDeckStore(); const { openModalWithCard, setModalOpen, setClickedCard } = useModalContext(); const { setHoveredCard, setIsPopoverOpen, hoveredCard } = useContext(PopoverContext); - const handleClick = () => { + // Event handlers and callbacks + const handleClick = useCallback(() => { openModalWithCard(card); - // setClickedCard(card); setModalOpen(true); setIsPopoverOpen(false); - }; + }, [openModalWithCard, setModalOpen, setIsPopoverOpen, card]); - const handleInteraction = (hoverState) => { - if (!hoverState) { + const handleInteraction = useCallback( + (hoverState) => { setHoveredCard(hoverState ? card : null); setIsPopoverOpen(hoverState); - } - }; + }, + [setHoveredCard, setIsPopoverOpen, card] + ); useEffect(() => { setIsPopoverOpen(hoveredCard === card); }, [hoveredCard, card, setIsPopoverOpen]); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - const isMediumScreen = useMediaQuery(theme2.breakpoints.between('sm', 'md')); - const isLargeScreen = useMediaQuery(theme.breakpoints.up('lg')); - // if ((isMediumScreen && page === 'Deck') || page === 'DeckBuilder') { - // console.log('small'); - // } - // const deckBuilder + // const isCardInContext = useCallback(() => { + // switch (context) { + // case 'Collection': + // return !!selectedCollection?.cards?.find((c) => c?.id === card?.id); + // case 'Deck': + // return !!selectedDeck?.cards?.find((c) => c?.id === card?.id); + // case 'Cart' || 'Store': + // return !!cartData?.cart?.find((c) => c?.id === card?.id); + // default: + // return false; + // } + // }, [context, card, selectedCollection, selectedDeck, cartData]); const isCardInContext = useCallback(() => { - switch (context) { - case 'Collection': - return !!selectedCollection?.cards?.find((c) => c?.id === card?.id); - case 'Deck': - return !!selectedDeck?.cards?.find((c) => c?.id === card?.id); - case 'Cart' || 'Store': - return !!cartData?.cart?.find((c) => c?.id === card?.id); - default: - return false; - } - }, [card.id, context, selectedCollection, selectedDeck, cartData]); + const cardsList = { + Collection: selectedCollection?.cards, + Deck: selectedDeck?.cards, + Cart: cartData?.cart, + }; + return !!cardsList[context]?.find((c) => c?.id === card?.id); + }, [context, card.id, selectedCollection, selectedDeck, cartData]); const isInContext = isCardInContext(); - - // const isSmallCard = ref?.current?.offsetWidth < 150; const name = card?.name; - const imgUrl = card?.card_images?.[0]?.image_url || placeholderImage; const price = `Price: ${ card?.latestPrice?.num || @@ -102,116 +79,134 @@ const GenericCard = React.forwardRef((props, ref) => { card?.card_prices?.[0]?.tcgplayer_price || 'N/A' }`; - // const quantity = card?.quantity || 0; - // let quantity = 0; - let cartQuantity = isInContext - ? cartData?.cart?.find((c) => c?.id === card?.id)?.quantity - : 0; - let storeQuantity = isInContext - ? cartData?.cart?.find((c) => c?.id === card?.id)?.quantity - : 0; - - let deckQuantity = isInContext - ? selectedDeck?.cards?.find((c) => c?.id === card?.id)?.quantity - : 0; - // let collectionQuantity = isInContext ? card?.quantity : 0; - let collectionQuantity = isInContext - ? selectedCollection?.cards?.find((c) => c?.id === card?.id)?.quantity - : 0; - // Simplify the method to retrieve the relevant quantity - let relevantQuantity = 0; - switch (page) { - case 'Store': - case 'StorePage': - relevantQuantity = storeQuantity; - break; - case 'Cart': - case 'CartPage': - relevantQuantity = cartQuantity; - break; - case 'Deck': - case 'DeckPage': - case 'DeckBuilder': - relevantQuantity = deckQuantity; - break; - case 'Collection': - relevantQuantity = collectionQuantity; - break; - default: - relevantQuantity = card?.quantity; - } - - const isSmallCard = cardRef.current && cardRef.current.offsetWidth < 140; - const getFontSize = () => ({ - xs: '0.7rem', - sm: '0.8rem', - md: '0.9rem', - lg: '1rem', - }); + // Get the quantity of card across all contexts + const getQuantity = useCallback(() => { + const findCardQuantity = (collectionsOrDecks) => + collectionsOrDecks?.reduce( + (acc, item) => + acc + (item?.cards?.find((c) => c.id === card.id)?.quantity ?? 0), + 0 + ) ?? 0; + + const cartQuantity = isInContext + ? cartData?.cart?.find((c) => c.id === card.id)?.quantity ?? 0 + : 0; + const collectionQuantity = + isInContext && selectedCollection + ? selectedCollection?.cards?.find((c) => c.id === card.id)?.quantity ?? + 0 + : findCardQuantity(allCollections); + const deckQuantity = + isInContext && selectedDeck + ? selectedDeck?.cards?.find((c) => c.id === card.id)?.quantity ?? 0 + : findCardQuantity(allDecks); + + return Math.max(cartQuantity, collectionQuantity, deckQuantity); + }, [ + card, + cartData, + selectedCollection, + allCollections, + selectedDeck, + allDecks, + ]); + + // let cartQuantity = isInContext + // ? cartData?.cart?.find((c) => c?.id === card?.id)?.quantity + // : 0; + // let storeQuantity = isInContext + // ? cartData?.cart?.find((c) => c?.id === card?.id)?.quantity + // : 0; + + // // Modify the logic to calculate quantity across all decks or collections if none is selected + // let deckQuantity = selectedDeck + // ? selectedDeck?.cards?.find((c) => c.id === card.id)?.quantity + // : allDecks?.reduce((acc, deck) => { + // const foundCard = deck?.cards?.find((c) => c.id === card.id); + // return foundCard ? acc + foundCard.quantity : acc; + // }, 0); + + // let collectionQuantity = selectedCollection + // ? selectedCollection?.cards?.find((c) => c.id === card.id)?.quantity + // : allCollections?.reduce((acc, collection) => { + // const foundCard = collection?.cards?.find((c) => c.id === card.id); + // return foundCard ? acc + foundCard.quantity : acc; + // }, 0); + // // Simplify the method to retrieve the relevant quantity + // let relevantQuantity = 0; + // switch (page) { + // case 'Store': + // case 'StorePage': + // relevantQuantity = storeQuantity; + // break; + // case 'Cart': + // case 'CartPage': + // relevantQuantity = cartQuantity; + // break; + // case 'Deck': + // case 'DeckPage': + // case 'DeckBuilder': + // relevantQuantity = deckQuantity; + // break; + // case 'Collection': + // relevantQuantity = collectionQuantity; + // break; + // default: + // relevantQuantity = card?.quantity; + // } + + // Function to render the card's media section + const renderCardMediaSection = () => ( + + + + ); const renderCardContent = () => { return ( - - {' '} - {/* Align text to left */} - + + {name} - {!isSmallCard && ( - <> - - {price} - - - {cartQuantity && `Cart: ${cartQuantity}`} - {collectionQuantity && `Collection: ${collectionQuantity}`} - {deckQuantity && `Deck: ${deckQuantity}`} - - - )} - + + {price} + + {`Cart: ${ + cartData?.cart?.find((c) => c.id === card.id)?.quantity ?? 0 + }`} + + + {`Collection: ${ + selectedCollection?.cards?.find((c) => c.id === card.id) + ?.quantity ?? 0 + }`} + + {`Deck: ${ + selectedDeck?.cards?.find((c) => c.id === card.id)?.quantity ?? 0 + }`} + ); }; + const renderCardActions = () => ( + + + + ); return ( - - - - - + + {renderCardMediaSection()} {renderCardContent()} - - - + {renderCardActions()} ); }); diff --git a/src/components/cards/cardStyledComponents.jsx b/src/components/cards/cardStyledComponents.jsx new file mode 100644 index 0000000..499337c --- /dev/null +++ b/src/components/cards/cardStyledComponents.jsx @@ -0,0 +1,52 @@ +import { Card, CardContent, Typography } from '@mui/material'; +import { styled } from '@mui/styles'; + +export const AspectRatioBox = styled('div')(({ theme }) => ({ + width: '100%', // Full width of the parent container + position: 'relative', + justifyContent: 'center', +})); + +// Updated StyledCardContent with proper media queries for padding +export const StyledCardContent = styled(CardContent)(({ theme }) => ({ + flexGrow: 1, + textAlign: 'left', + minHeight: '100px', // Adjust based on the size of the text + border: `1px solid ${theme.palette.background.quaternary}`, + borderRadius: theme.shape.borderRadius, + // Media queries for padding + padding: theme.spacing(1), // default padding + [theme.breakpoints.down('xs')]: { + padding: theme.spacing(1), + }, + [theme.breakpoints.between('sm', 'md')]: { + padding: theme.spacing(1.5), + }, + [theme.breakpoints.up('lg')]: { + padding: theme.spacing(2), + }, +})); + +export const StyledCard = styled(Card)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + minWidth: '120px', // Adjusted for better responsiveness + maxWidth: '100%', + width: 'auto', + maxHeight: '100%', // Adjusted for better height management + flexGrow: 1, + backgroundColor: theme.palette.background.paper, + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shadows[5], + justifyContent: 'center', + // margin: 'auto', + transition: 'transform 0.3s ease-in-out', + '&:hover': { + transform: 'scale(1.03)', + }, +})); + +export const QuantityLine = styled(Typography)(({ theme }) => ({ + fontSize: theme.typography.pxToRem(12), + color: theme.palette.text.secondary, +})); diff --git a/src/components/cards/media/CardMediaSection.js b/src/components/cards/media/CardMediaSection.js new file mode 100644 index 0000000..db4f66f --- /dev/null +++ b/src/components/cards/media/CardMediaSection.js @@ -0,0 +1,120 @@ +import React, { useEffect, forwardRef } from 'react'; +import { CardMedia, Popover } from '@mui/material'; +import CardToolTip from '../CardToolTip'; +import { makeStyles } from '@mui/styles'; +import PropTypes from 'prop-types'; +import { useModalContext, usePopoverContext } from '../../../context'; + +const useStyles = makeStyles((theme) => ({ + mediaContainer: { + cursor: 'pointer', + position: 'relative', + }, + popover: { + pointerEvents: 'none', + }, + media: { + width: '100%', + height: 'auto', + flexGrow: 1, + [theme.breakpoints.down('xs')]: { + padding: theme.spacing(1), + }, + [theme.breakpoints.down('sm')]: { + padding: theme.spacing(1), + }, + [theme.breakpoints.down('md')]: { + padding: theme.spacing(1), + }, + [theme.breakpoints.down('lg')]: { + padding: theme.spacing(0.5), + }, + [theme.breakpoints.down('xl')]: { + padding: theme.spacing(0.5), + }, + }, +})); + +const anchorOrigin = { + vertical: 'bottom', + horizontal: 'left', +}; + +const transformOrigin = { + vertical: 'top', + horizontal: 'left', +}; + +const CardMediaSection = forwardRef( + ( + { imgUrl, card, isHovered, handleInteraction, handleClick, isRequired }, + ref + ) => { + const classes = useStyles(); + const { setModalImgUrl, clickedCard, setClickedCard } = useModalContext(); + + const eventHandlers = isRequired + ? { + onMouseEnter: () => handleInteraction?.(true), + onMouseLeave: () => handleInteraction?.(false), + onClick: () => { + handleClick?.(); + setClickedCard?.(card); + }, + } + : {}; + + useEffect(() => { + if (imgUrl && clickedCard) { + setModalImgUrl(imgUrl); + } + }, [imgUrl, clickedCard, setModalImgUrl]); + + return ( +
handleClick?.()} + > + + {isRequired && isHovered && ( + handleInteraction(false)} + anchorOrigin={anchorOrigin} + transformOrigin={transformOrigin} + disableRestoreFocus + > + + + )} +
+ ); + } +); + +CardMediaSection.displayName = 'CardMediaSection'; + +CardMediaSection.propTypes = { + imgUrl: PropTypes.string.isRequired, + card: PropTypes.object.isRequired, + isHovered: PropTypes.bool, + handleInteraction: PropTypes.func, + handleClick: PropTypes.func, + isRequired: PropTypes.bool, +}; + +CardMediaSection.defaultProps = { + isHovered: false, + isRequired: true, +}; + +export default CardMediaSection; diff --git a/src/components/chart/LinearChart.js b/src/components/chart/LinearChart.js index b78518a..1c35dc2 100644 --- a/src/components/chart/LinearChart.js +++ b/src/components/chart/LinearChart.js @@ -19,19 +19,6 @@ const useStyles = makeStyles((theme) => ({ fontWeight: 'bold', color: theme.palette.text.primary, }, - // chartContainer: { - // width: '100%', - // height: 'auto', - // }, - // chartContainer: { - // width: '100%', - // height: 'auto', - // '@media (max-width: 600px)': { - // width: '130%', // Adjust width to 150% for mobile screens - - // height: '300px', // Adjust height for mobile screens - // }, - // }, chartContainer: { display: 'flex', alignItems: 'center', @@ -136,76 +123,71 @@ const LinearChart = ({ data: series?.data.filter((dataPoint) => { const dataPointTime = new Date(dataPoint?.x).getTime(); const isWithinRange = dataPointTime >= currentTime - timeRange; - // Detailed logging - // console.log( - // `Data Point: ${new Date( - // dataPointTime - // ).toISOString()}, Current Time: ${new Date( - // currentTime - // ).toISOString()}, Within Range: ${isWithinRange}` - // ); return isWithinRange; }), })); }, [nivoReadyData, timeRange]); - const chartProps = { - margin: { top: 50, right: 110, bottom: 50, left: 60 }, - data: dataWithinTimeRange, - animate: true, - motionStiffness: 90, - motionDamping: 15, - background: '#2c2121', - xScale: { - type: 'time', - format: '%Y-%m-%dT%H:%M:%S.%LZ', - useUTC: false, - precision: 'second', - }, - xFormat: 'time:%Y-%m-%d %H:%M:%S', - axisBottom: { - tickRotation: 0, - legendOffset: -24, - legend: 'Time', - tickPadding: 10, - tickSize: 1, - format: xFormat, - tickValues: tickValues, - }, - yScale: { type: 'linear', min: 'auto', max: 'auto' }, - axisLeft: { - orient: 'left', - legend: 'Value ($)', - legendOffset: 12, - legendPosition: 'middle', - format: (value) => `$${value}`, - tickPadding: 10, - tickSize: 10, - }, - pointSize: 10, - pointBorderWidth: 2, - pointBorderColor: theme.palette.primary.main, - pointColor: theme.palette.success.light, - colors: theme.palette.primaryDark.main, - lineWidth: 3, - curve: 'monotoneX', - useMesh: true, - theme: theme.chart, - onMouseMove: handleMouseMove, - onMouseLeave: handleMouseLeave, - onClick: () => setIsZoomed(!isZoomed), - tooltip: CustomTooltip, - }; + const chartProps = useMemo( + () => ({ + margin: { top: 50, right: 110, bottom: 50, left: 60 }, + data: dataWithinTimeRange, + animate: true, + motionStiffness: 90, + motionDamping: 15, + background: '#2c2121', + xScale: { + type: 'time', + format: '%Y-%m-%dT%H:%M:%S.%LZ', + useUTC: false, + precision: 'second', + }, + xFormat: 'time:%Y-%m-%d %H:%M:%S', + axisBottom: { + tickRotation: 0, + legendOffset: -24, + legend: 'Time', + tickPadding: 10, + tickSize: 1, + format: xFormat, + tickValues: tickValues, + }, + yScale: { type: 'linear', min: 'auto', max: 'auto' }, + axisLeft: { + orient: 'left', + legend: 'Value ($)', + legendOffset: 12, + legendPosition: 'middle', + format: (value) => `$${value}`, + tickPadding: 10, + tickSize: 10, + }, + pointSize: 10, + pointBorderWidth: 2, + pointBorderColor: theme.palette.primary.main, + pointColor: theme.palette.success.light, + colors: theme.palette.primaryDark.main, + lineWidth: 3, + curve: 'monotoneX', + useMesh: true, + theme: theme.chart, + onMouseMove: handleMouseMove, + onMouseLeave: handleMouseLeave, + onClick: () => setIsZoomed(!isZoomed), + tooltip: CustomTooltip, + }), + [theme, nivoReadyData, timeRange] + ); return ( -
{/*
*/} -
+ ); }; diff --git a/src/components/chart/PortfolioChart.jsx b/src/components/chart/PortfolioChart.jsx index 61b4b47..928b032 100644 --- a/src/components/chart/PortfolioChart.jsx +++ b/src/components/chart/PortfolioChart.jsx @@ -113,14 +113,22 @@ const PortfolioChart = () => { const nivoReadyData2 = useMemo(() => { return rawData2?.length > 0 ? convertDataForNivo2(rawData2) : []; }, [rawData2]); + // useMemo for expensive calculations + + // Debounced window resize handler + const debouncedCalculateChartDimensions = useMemo( + () => debounce(calculateChartDimensions, 100), + [] + ); + useEffect(() => { - const handleResize = debounce(calculateChartDimensions, 100); - window.addEventListener('resize', handleResize); + window.addEventListener('resize', debouncedCalculateChartDimensions); return () => { - window.removeEventListener('resize', handleResize); - handleResize.cancel(); + window.removeEventListener('resize', debouncedCalculateChartDimensions); + debouncedCalculateChartDimensions.cancel(); }; - }, []); + }, [debouncedCalculateChartDimensions]); + if (!dataReady) { return LoadingIndicator(); } diff --git a/src/components/collection/CardPortfolio.jsx b/src/components/collection/CardPortfolio.jsx index 56528d4..1ab3a96 100644 --- a/src/components/collection/CardPortfolio.jsx +++ b/src/components/collection/CardPortfolio.jsx @@ -4,8 +4,30 @@ import PortfolioContent from './PortfolioContent'; import { Box, Typography, useTheme } from '@mui/material'; import { useCollectionStore } from '../../context/CollectionContext/CollectionContext'; import { useMode } from '../../context'; +import { makeStyles } from '@mui/styles'; +const useCardPortfolioStyles = makeStyles((theme) => ({ + boxStyle: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + borderRadius: theme.shape.borderRadius, + flexGrow: 1, + background: '#333', + padding: { + xs: theme.spacing(1), + sm: theme.spacing(1), + md: theme.spacing(2.5), + lg: theme.spacing(2.5), + }, + height: '100%', + width: '100%', + }, +})); const CardPortfolio = ({ allCollections, onCollectionSelect }) => { const { theme } = useMode(); + const classes = useCardPortfolioStyles(theme); + const [showCollections, setShowCollections] = useState(true); const [showPortfolio, setShowPortfolio] = useState(false); const [selectedCards, setSelectedCards] = useState([]); @@ -26,25 +48,7 @@ const CardPortfolio = ({ allCollections, onCollectionSelect }) => { } }; return ( - + {showCollections ? ( ) : showPortfolio ? ( diff --git a/src/components/collection/PortfolioContent.jsx b/src/components/collection/PortfolioContent.jsx index 798186b..750aaf5 100644 --- a/src/components/collection/PortfolioContent.jsx +++ b/src/components/collection/PortfolioContent.jsx @@ -7,89 +7,76 @@ import { useMode } from '../../context/hooks/colormode'; import CollectionPortfolioChartContainer from '../../containers/collectionPageContainers/CollectionPortfolioChartContainer'; // eslint-disable-next-line max-len import CollectionPortfolioListContainer from '../../containers/collectionPageContainers/CollectionPortfolioListContainer'; +import { makeStyles } from '@mui/styles'; +const useStyles = makeStyles((theme) => ({ + portfolioBox: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + maxWidth: '100vw', + width: '100%', + height: '100%', + margin: { xs: 0, sm: 'auto', md: 'auto', lg: 'auto' }, + padding: { + xs: theme.spacing(1), + sm: theme.spacing(2), + md: theme.spacing(3), + lg: theme.spacing(3), + }, + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + }, + portfolioPaper: { + background: theme.palette.background.main, + color: theme.palette.text.primary, + display: 'flex', + justifyContent: 'center', + flexDirection: 'column', + margin: 'auto', + width: '100%', + padding: { + xs: theme.spacing(1), + sm: theme.spacing(1), + md: theme.spacing(2), + lg: theme.spacing(2), + }, + borderRadius: theme.shape.borderRadius, + }, + gridContainer: { + width: '100%', + maxWidth: '100vw', + justifyContent: 'center', + }, + gridItem: { + justifyContent: 'center', + }, +})); const PortfolioContent = ({ selectedCards, removeCard }) => { - const { theme } = useMode(); const { selectedCollection } = useCollectionStore(); const [collectionName, setCollectionName] = useState( selectedCollection?.name ); + const classes = useStyles(useMode().theme); + useEffect(() => { if (selectedCollection?.name) { setCollectionName(selectedCollection.name); } }, [selectedCollection]); - return ( - - + + - - + + { +const useSelectCollectionStyles = makeStyles((theme) => ({ + boxStyle: { + p: 2, + justifyContent: 'center', + margin: 'auto', + }, + typographyStyle: { + fontWeight: 'bold', + color: 'black', + m: 'auto', + }, + button: { + variant: 'outlined', + '& .MuiTypography-root': { + variant: 'button', + [theme.breakpoints.down('sm')]: { + variant: 'caption', + }, + }, + }, +})); + +const SelectCollection = ({ handleSelectCollection }) => { const { theme } = useMode(); + const classes = useSelectCollectionStyles(theme); const [isDialogOpen, setDialogOpen] = useState(false); const [isNew, setIsNew] = useState(false); const { setSelectedCollection, selectedCollection } = useCollectionStore(); - // const [editedName, setEditedName] = useState(''); - // const [editedDescription, setEditedDescription] = useState(''); - const isXSmallScreen = useMediaQuery(theme.breakpoints.down('xs')); - const isSmallCard = useMediaQuery(theme.breakpoints.down('sm')); - const isMediumCard = useMediaQuery(theme.breakpoints.between('sm', 'md')); - const isMediumLargeCard = useMediaQuery( - theme.breakpoints.between('md', 'lg') - ); - const buttonSize = isSmallCard ? 'small' : isMediumCard ? 'medium' : 'large'; + const handleOpenCollectionModal = useCallback(() => { setDialogOpen(true); setIsNew(true); - // No need to set editedName and editedDescription here }, []); const closeDialog = useCallback(() => setDialogOpen(false), []); const handleSave = useCallback( (collection) => { setSelectedCollection(collection); - // setShowCollections(false); - // setShowPortfolio(true); closeDialog(); }, [setSelectedCollection, closeDialog] @@ -52,7 +58,6 @@ const SelectCollection = ({ const openDialog = useCallback((isNewCollection) => { setDialogOpen(true); setIsNew(isNewCollection); - // No need to set editedName and editedDescription here }, []); return ( @@ -60,31 +65,19 @@ const SelectCollection = ({ - - + + Choose a Collection - + @@ -93,20 +86,17 @@ const SelectCollection = ({ openDialog(false)} // Indicate that this is not a new collection - isXSmallScreen={isXSmallScreen} // Pass this prop to adjust styles inside the list + openDialog={() => openDialog(false)} /> diff --git a/src/components/dialogs/cardDialog/CardCarousel.jsx b/src/components/dialogs/cardDialog/CardCarousel.jsx new file mode 100644 index 0000000..633fcf9 --- /dev/null +++ b/src/components/dialogs/cardDialog/CardCarousel.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { styled } from '@mui/styles'; +import { Swiper, SwiperSlide } from 'swiper/react'; +import { EffectCoverflow, Pagination } from 'swiper'; +import 'swiper/css'; +import 'swiper/css/effect-coverflow'; +import 'swiper/css/pagination'; +import GenericCard from '../../cards/GenericCard'; + +const StyledCarousel = styled('div')(({ theme }) => ({ + bottom: theme.spacing(2), + right: theme.spacing(2), + width: '100%', + height: 180, + perspective: '1200px', + [theme.breakpoints.up('md')]: { + width: '50%', + height: 250, + }, +})); + +const StyledSwiperSlide = styled(SwiperSlide)(({ theme }) => ({ + '& .swiper-slide': { + opacity: 0.5, + transform: 'scale(0.85)', + transition: 'transform 0.3s, opacity 0.3s', + '&-active': { + opacity: 1, + transform: 'scale(1.2)', + }, + '&-next, &-prev': { + transform: 'scale(0.8)', + }, + }, +})); + +const CardCarousel = ({ cards, context }) => { + const swiperSettings = { + effect: 'coverflow', + grabCursor: true, + centeredSlides: true, + slidesPerView: 'auto', + coverflowEffect: { + rotate: 50, + stretch: 0, + depth: 200, + modifier: 1, + slideShadows: true, + }, + loop: true, + pagination: { clickable: true }, + }; + + return ( + + + {cards.map((card, index) => ( + + + + ))} + + + ); +}; + +export default CardCarousel; diff --git a/src/components/dialogs/cardDialog/CardDetail.jsx b/src/components/dialogs/cardDialog/CardDetail.jsx new file mode 100644 index 0000000..f5d210d --- /dev/null +++ b/src/components/dialogs/cardDialog/CardDetail.jsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import useResponsiveStyles from '../../../context/hooks/useResponsiveStyles'; +import { useMode } from '../../../context'; + +const CardDetail = ({ icon, title, value, className }) => { + const { theme } = useMode(); + const { getHeaderStyle, getButtonTypographyVariant } = + useResponsiveStyles(theme); + + return ( + + {icon && {icon}} + + {title}: {value || 'N/A'} + + + ); +}; + +export default CardDetail; diff --git a/src/components/dialogs/cardDialog/CardDetailsContainer.jsx b/src/components/dialogs/cardDialog/CardDetailsContainer.jsx new file mode 100644 index 0000000..18413f8 --- /dev/null +++ b/src/components/dialogs/cardDialog/CardDetailsContainer.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Grid, Typography, useTheme } from '@mui/material'; +import { + FaDragon, + FaLevelUpAlt, + FaRegCopy, + FaRegLightbulb, + FaShieldAlt, + FaVenusMars, +} from 'react-icons/fa'; +import CardDetail from './CardDetail'; +import { styled } from '@mui/system'; + +// Styled wrapper for icon for consistent styling +const IconWrapper = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + marginRight: theme.spacing(1), + fontSize: '1.5rem', + color: theme.palette.text.secondary, +})); + +const CardDetailsContainer = ({ card, className }) => { + const theme = useTheme(); + + return ( + + {[ + { icon: FaLevelUpAlt, title: 'Level', value: card?.level }, + { icon: FaVenusMars, title: 'Type', value: card?.type }, + { icon: FaDragon, title: 'Race', value: card?.race }, + { icon: FaRegLightbulb, title: 'Attribute', value: card?.attribute }, + { title: 'ATK', value: card?.atk }, + { icon: FaShieldAlt, title: 'DEF', value: card?.def }, + ].map((detail, index) => ( + + + + + ) + } + title={detail.title} + value={detail.value} + /> + + ))} + + ); +}; + +export default CardDetailsContainer; diff --git a/src/components/dialogs/cardDialog/GenericCardDialog.jsx b/src/components/dialogs/cardDialog/GenericCardDialog.jsx new file mode 100644 index 0000000..bb1be0b --- /dev/null +++ b/src/components/dialogs/cardDialog/GenericCardDialog.jsx @@ -0,0 +1,203 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + Snackbar, + Alert, + Grid, + Container, + useMediaQuery, + Slide, + Fade, + IconButton, +} from '@mui/material'; +import { useTheme } from '@mui/styles'; +import axios from 'axios'; +import useSnackbar from '../../../context/hooks/useSnackBar'; +import useSelectedContext from '../../../context/hooks/useSelectedContext'; +import { + useModalContext, + useCollectionStore, + useUserContext, +} from '../../../context'; +import CardMediaSection from '../../cards/media/CardMediaSection'; +import CardCarousel from './CardCarousel'; +import GenericActionButtons from '../../buttons/actionButtons/GenericActionButtons'; +import CardDetailsContainer from '../../../containers/CardDetailsContainer'; +import CloseIcon from '@mui/icons-material/Close'; +import useResponsiveStyles from '../../../context/hooks/useResponsiveStyles'; +import CardDetail from './CardDetail'; +import { FaRegCopy } from 'react-icons/fa'; + +const GenericCardDialog = (props) => { + const theme = useTheme(); + + const { + open = false, + transition = false, + close, + hideBackdrop = false, + title = '', + content = '', + actions = '', + dividers = false, + closeIcon = true, + fullScreen = false, + actionPosition = 'left', + closeIconSx = { + position: 'absolute', + right: 8, + top: 8, + color: (theme) => theme.palette.grey[500], + }, + card, + context, + ...other + } = props || {}; + const { closeModal } = useModalContext(); + const { user } = useUserContext(); + // const { updateCollection, selectedCollection } = useCollectionStore(); + const { getHeaderStyle, isMobile } = useResponsiveStyles(theme); + // const [updatedCard, setUpdatedCard] = useState(card); + const [imageUrl, setImageUrl] = useState(card?.card_images[0]?.image_url); + const [snackbar, handleSnackbar, handleCloseSnackbar] = useSnackbar(); + const [hasLoggedCard, setHasLoggedCard] = useState(false); + const { setContext, setIsContextSelected } = useSelectedContext(); + // Helper function to handle successful and failed actions + const handleAction = useCallback( + (message, severity, error) => { + handleSnackbar(message, severity); + if (error) console.error('Action failed:', error); + }, + [handleSnackbar] + ); + // Update card data from the server if necessary + // useEffect(() => { + // async function updateCardData() { + // if (card.card_images.length === 0) { + // try { + // const response = await axios.patch( + // `${process.env.REACT_APP_SERVER}/api/cards/ygopro/${card.id}`, + // { id: card.id, name: card.name, card, user } + // ); + // if (response.data && response.data.data) { + // setUpdatedCard(response.data.data); + // setImageUrl(response.data.data.card_images[0]?.image_url); + // updateCollection(response.data.data, 'update', selectedCollection); + // } + // } catch (err) { + // console.error('Error fetching card images:', err); + // } + // } + // } + + // if (open) { + // updateCardData(); + // } + // }, [card, user, updateCollection, selectedCollection, open]); + // Snackbar handling for loading card details + useEffect(() => { + if (open && card && !hasLoggedCard) { + handleSnackbar('Card details loaded successfully.', 'success'); + setHasLoggedCard(true); + } + return () => { + if (!open) setHasLoggedCard(false); + }; + }, [open, card, handleSnackbar, hasLoggedCard]); + // Context selection handling + const handleContextSelect = useCallback( + (newContext) => { + setContext(newContext); + setIsContextSelected(true); + }, + [setContext, setIsContextSelected] + ); + + return ( + + + {title} + {closeIcon && ( + + + + )} + {' '} + + + + {/* these two grid items are for the card media and card details */} + + + + + } + title="Description" + value={card?.desc} + /> + + + + + + {/* these two grid items are for the card carousel and action buttons */} + + + {['Deck', 'Collection', 'Cart'].map((mappedContext) => ( + handleContextSelect(mappedContext)} + onSuccess={() => + handleAction('Action was successful!', 'success') + } + onFailure={(error) => + handleAction( + 'Action failed. Please try again.', + 'error', + error + ) + } + /> + ))} + + + + + + + {snackbar.message} + + + + ); +}; + +export default GenericCardDialog; diff --git a/src/components/dialogs/homeDetailsModal/DetailsModal.jsx b/src/components/dialogs/homeDetailsModal/DetailsModal.jsx new file mode 100644 index 0000000..8a58a88 --- /dev/null +++ b/src/components/dialogs/homeDetailsModal/DetailsModal.jsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + IconButton, + ImageList, + ImageListItem, + Link, + Typography, + Box, + Chip, +} from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { FaGithub, FaExternalLinkAlt } from 'react-icons/fa'; +import { useModalContext } from '../../../context'; + +const DetailsModal = () => { + const { featureData, detailsModalShow, closeDetailsModal } = + useModalContext(); + + return ( + + + + + + + {featureData?.title} + + + + + + + + + + {featureData?.description} + + + {featureData?.images?.map((imgSrc, idx) => ( + + {`Image + + ))} + + + + Technologies Used: + + {/* Additional content here if needed */} + + + + {featureData?.url && ( + + + View Live + + )} + {featureData?.readmeurl && ( + + + View on GitHub + + )} + + + ); +}; + +export default DetailsModal; diff --git a/src/components/dialogs/modalStyles.jsx b/src/components/dialogs/modalStyles.jsx new file mode 100644 index 0000000..764369d --- /dev/null +++ b/src/components/dialogs/modalStyles.jsx @@ -0,0 +1,123 @@ +// // import { makeStyles } from '@mui/styles'; +// import { makeStyles } from '@mui/styles'; + +// export const useStyles = makeStyles((theme) => ({ +// flexCenter: { +// display: 'flex', +// alignItems: 'center', +// }, +// actionButtons: { +// justifyContent: 'center', +// gap: '1.5rem', +// }, +// media: { +// objectFit: 'cover', +// borderRadius: '4px', +// boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)', +// }, +// details: { +// gap: '1.5rem', +// marginBottom: '1.5rem', +// }, +// dialogTitle: { +// fontSize: '1.5rem', +// fontWeight: 600, +// color: theme.palette.primary.dark, +// }, +// dialogContent: { +// padding: '2rem', +// }, +// listItem: { +// justifyContent: 'space-between', +// padding: theme.spacing(2), +// backgroundColor: '#ffffff', +// borderRadius: '8px', +// marginBottom: theme.spacing(2), +// boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)', +// }, +// listItemText: { +// flex: 1, +// textAlign: 'left', +// marginLeft: theme.spacing(3), +// }, +// // flexCenter: { +// // display: 'flex', +// // alignItems: 'center', +// // justifyContent: 'center', +// // }, +// // actionButtons: { +// // order: { xs: 2, md: 3 }, +// // }, +// mediaAndDetails: { +// order: { xs: 1, md: 1 }, +// }, +// carousel: { +// order: { xs: 3, md: 2 }, +// display: 'flex', +// justifyContent: 'center', +// }, +// swiperSlide: { +// '& .swiper-slide': { +// opacity: 0.5, +// transform: 'scale(0.85)', +// transition: 'transform 0.3s, opacity 0.3s', +// '&-active': { +// opacity: 1, +// transform: 'scale(1.2)', +// }, +// '&-next, &-prev': { +// transform: 'scale(0.8)', +// }, +// }, +// }, +// carouselContainer: { +// bottom: theme.spacing(2), +// right: theme.spacing(2), +// width: '100%', +// height: 180, +// perspective: '1200px', +// }, +// })); + +// // export const useStyles = makeStyles((theme) => ({ +// // actionButtons: { +// // display: 'flex', +// // justifyContent: 'center', +// // alignItems: 'center', +// // gap: '1.5rem', +// // }, +// // media: { +// // objectFit: 'cover', +// // borderRadius: '4px', +// // boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)', +// // }, +// // details: { +// // display: 'flex', +// // alignItems: 'center', +// // gap: '1.5rem', +// // marginBottom: '1.5rem', +// // }, +// // dialogTitle: { +// // fontSize: '1.5rem', +// // fontWeight: 600, +// // color: theme.palette.primary.dark, +// // }, +// // dialogContent: { +// // padding: '2rem', +// // }, +// // listItem: { +// // display: 'flex', +// // alignItems: 'center', +// // justifyContent: 'space-between', +// // padding: theme.spacing(2), +// // backgroundColor: '#ffffff', +// // borderRadius: '8px', +// // marginBottom: theme.spacing(2), +// // boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)', +// // }, +// // listItemText: { +// // flex: 1, +// // textAlign: 'left', +// // marginLeft: theme.spacing(3), +// // }, +// // })); diff --git a/src/components/modals/stripeModal/StripeCheckoutModal.js b/src/components/dialogs/stripeModal/StripeCheckoutModal.js similarity index 100% rename from src/components/modals/stripeModal/StripeCheckoutModal.js rename to src/components/dialogs/stripeModal/StripeCheckoutModal.js diff --git a/src/components/forms/LoginForm.jsx b/src/components/forms/LoginForm.jsx index 8c85fe5..f203972 100644 --- a/src/components/forms/LoginForm.jsx +++ b/src/components/forms/LoginForm.jsx @@ -1,5 +1,11 @@ -import React from 'react'; -import { FormWrapper, StyledTextField, StyledButton } from './styled'; +import React, { useState } from 'react'; +import { + FormWrapper, + StyledTextField, + StyledButton, + StyledBorderContainer, +} from './styled'; // Ensure this path is correct for your project +import { useMode } from '../../context'; // Ensure this path is correct for your project const LoginForm = ({ username, @@ -15,6 +21,8 @@ const LoginForm = ({ setRoleData, handleSubmit, }) => { + const { theme } = useMode(); // Ensure your context provides theme correctly + return ( setUsername(e.target.value)} variant="outlined" required + theme={theme} /> + + {/* Additional fields appear if in signup mode */} {signupMode && ( <> setRoleData(e.target.value)} variant="outlined" required + theme={theme} /> )} + setPassword(e.target.value)} variant="outlined" required + theme={theme} /> - + + {/* Submit Button */} + {signupMode ? 'Sign Up' : 'Login'} diff --git a/src/components/forms/customerCheckoutForm/CustomerForm.js b/src/components/forms/customerCheckoutForm/CustomerForm.js index 9003cc3..af7da6f 100644 --- a/src/components/forms/customerCheckoutForm/CustomerForm.js +++ b/src/components/forms/customerCheckoutForm/CustomerForm.js @@ -2,7 +2,7 @@ import React, { useCallback, useContext } from 'react'; import { Box, Container, Typography, Grid } from '@mui/material'; import { useCartStore } from '../../../context/CartContext/CartContext'; import CustomerInfoFields from './CustomerInfoFields'; -import StripeCheckoutModal from '../../modals/stripeModal/StripeCheckoutModal'; +import StripeCheckoutModal from '../../dialogs/stripeModal/StripeCheckoutModal'; import { ModalContext } from '../../../context/ModalContext/ModalContext'; import CartSummary from '../../other/dataDisplay/CartSummary'; import OrderSubmitButton from '../../buttons/other/OrderSubmitButton'; diff --git a/src/components/forms/formStyles.jsx b/src/components/forms/formStyles.jsx deleted file mode 100644 index 8e4eaa9..0000000 --- a/src/components/forms/formStyles.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import styled from 'styled-components'; -import { Button, TextField } from '@mui/material'; - -export const FormWrapper = styled('form')` - display: flex; - flex-direction: column; - gap: 20px; - max-width: 400px; - margin: auto; -`; - -export const StyledTextField = styled(TextField)` - && { - margin-bottom: 12px; - width: 100%; // Ensures that the text fields take the full width - } -`; - -export const StyledButton = styled(Button)` - && { - margin-top: 16px; - } -`; diff --git a/src/components/forms/styled.jsx b/src/components/forms/styled.jsx index ac8edaa..d04a400 100644 --- a/src/components/forms/styled.jsx +++ b/src/components/forms/styled.jsx @@ -1,123 +1,117 @@ -import styled from 'styled-components'; +import styled, { keyframes, css } from 'styled-components'; import { Button, Paper, TextField } from '@mui/material'; export const FormWrapper = styled('form')` - display: flex; + display: 'flex'; flex-direction: column; - gap: 20px; - max-width: 400px; + gap: '20px'; + max-width: '400px'; margin: auto; `; -// export const StyledTextField = styled(TextField)` -// && { -// margin-bottom: 12px; -// width: 100%; // Ensures that the text fields take the full width -// } -// `; - -// export const StyledButton = styled(Button)` -// && { -// margin-top: 16px; -// } -// `; - export const StyledPaper = styled(Paper)(({ theme }) => ({ width: '100%', padding: theme.spacing(3), borderRadius: theme.shape.borderRadius, - backgroundColor: theme.palette.success.lighter, // Adjusted for better contrast + backgroundColor: theme.palette.success.lighter, boxShadow: theme.shadows[5], - maxWidth: 550, // Increased for better layout + maxWidth: '550px', margin: 'auto', transition: 'box-shadow 0.3s ease-in-out', - '&:hover': { - boxShadow: theme.shadows[7], // Slightly deeper shadow on hover + boxShadow: theme.shadows[7], }, })); export const StyledButton = styled(Button)(({ theme }) => ({ - backgroundColor: theme.palette.success.lightish, // Changed to secondary for better visual appeal + backgroundColor: theme.palette.success.lightish, color: theme.palette.background.dark, padding: theme.spacing(1.5), fontWeight: 'bold', - // textShadow: '1px 1px 1px rgba(0,0,0,0.25)', - marginTop: theme.spacing(2), // Added margin-top for spacing - + marginTop: theme.spacing(2), '&:hover': { - backgroundColor: theme.palette.secondary.dark, // Darker shade on hover + backgroundColor: theme.palette.secondary.dark, }, })); export const StyledTextField = styled(TextField)(({ theme }) => ({ - borderRadius: theme.shape.borderRadius, - backgroundColor: theme.palette.background.paper, // Adjusted for better contrast - boxShadow: theme.shadows[1], '& .MuiOutlinedInput-root': { position: 'relative', + transition: 'border-color 0.5s ease', // Add transition for border color + + // Remove border by default '& .MuiOutlinedInput-notchedOutline': { borderColor: 'transparent', }, + + // Change border color on hover '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: theme.palette.primary.light, }, + + // Change border color on focus '&.Mui-focused .MuiOutlinedInput-notchedOutline': { - borderWidth: 0, - }, - '&.Mui-focused::before, &.Mui-focused::after, &.Mui-focused::before, &.Mui-focused::after': - { - content: '""', - position: 'absolute', - borderStyle: 'solid', - borderWidth: '2px', - borderColor: theme.palette.primary.main, - }, - '&.Mui-focused::before': { - left: 0, - top: 0, - bottom: 0, - width: '2px', - animation: 'left-border-animation 0.25s forwards', - }, - '&.Mui-focused::after': { - right: 0, - top: 0, - bottom: 0, - width: '2px', - animation: 'right-border-animation 0.25s 0.25s forwards', - }, - // eslint-disable-next-line no-dupe-keys - '&.Mui-focused::before': { - top: 0, - left: 0, - right: 0, - height: '2px', - animation: 'top-border-animation 0.25s 0.5s forwards', - }, - // eslint-disable-next-line no-dupe-keys - '&.Mui-focused::after': { - bottom: 0, - left: 0, - right: 0, - height: '2px', - animation: 'bottom-border-animation 0.25s 0.75s forwards', + borderColor: theme.palette.primary.main, + borderWidth: '2px', // or other width as you like }, }, - '@keyframes left-border-animation': { - '0%': { height: '0%' }, - '100%': { height: '100%' }, + borderRadius: theme.shape.borderRadius, + color: theme.palette.success.main, + width: '100%', + backgroundColor: theme.palette.background.paper, + boxShadow: theme.shadows[1], +})); + +// Define simple expand animations +const expandWidth = keyframes` + from { width: 0%; } + to { width: 100%; } +`; + +const expandHeight = keyframes` + from { height: 0%; } + to { height: 100%; } +`; + +export const StyledBorderContainer = styled('div')(({ theme }) => ({ + position: 'relative', + width: '100%', + height: '100%', + '&:before, &:after': { + content: '""', + position: 'absolute', + backgroundColor: theme.palette.primary.light, + zIndex: 1, + transition: 'all 0.5s ease', + }, + '&:before': { + top: 0, + left: 0, + height: '2px', + animation: css` + ${expandWidth} 0.5s ease forwards + `, }, - '@keyframes right-border-animation': { - '0%': { height: '0%' }, - '100%': { height: '100%' }, + '&:after': { + bottom: 0, + right: 0, + height: '2px', + animation: css` + ${expandWidth} 0.5s 1.5s ease forwards + `, }, - '@keyframes top-border-animation': { - '0%': { width: '0%' }, - '100%': { width: '100%' }, + '&::before': { + width: '2px', + top: 0, + animation: css` + ${expandHeight} 0.5s 0.5s ease forwards + `, }, - '@keyframes bottom-border-animation': { - '0%': { width: '0%' }, - '100%': { width: '100%' }, + '&::after': { + width: '2px', + bottom: 0, + animation: css` + ${expandHeight} 0.5s 2s ease forwards + `, }, })); diff --git a/src/components/grids/collectionGrids/CardList.jsx b/src/components/grids/collectionGrids/CardList.jsx index 18edd66..2028631 100644 --- a/src/components/grids/collectionGrids/CardList.jsx +++ b/src/components/grids/collectionGrids/CardList.jsx @@ -1,9 +1,8 @@ -import React, { useMemo, useRef, useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { Box, Typography, Paper, - Container, IconButton, Table, TableBody, @@ -14,18 +13,15 @@ import { TableRow, Button, TableHead, - useTheme, - ButtonGroup, Divider, } from '@mui/material'; import AssessmentIcon from '@mui/icons-material/Assessment'; import TablePaginationActions from '../../reusable/TablePaginationActions'; -import PropTypes from 'prop-types'; import { useCollectionStore } from '../../../context/CollectionContext/CollectionContext'; -import { styled } from '@mui/styles'; -import { useMode } from '../../../context/hooks/colormode'; import Logger from '../../reusable/Logger'; -// Instantiate logger outside of the component +import { useMode } from '../../../context'; +import useCardListStyles from '../../../context/hooks/useCardListStyles'; + const cardLogger = new Logger([ 'Action', 'Card Name', @@ -33,59 +29,33 @@ const cardLogger = new Logger([ 'Total Price', ]); -const StyledContainer = styled(Container)(({ theme }) => ({ - display: 'flex', - flexDirection: 'column', - height: '100%', - width: '100%', - alignItems: 'center', - // background: theme.palette.background.main, - background: theme.palette.background.dark, - padding: theme.spacing(2), - color: '#fff', // White text color - // padding: 2, - borderRadius: 4, -})); - -const StyledButtonGroup = styled(ButtonGroup)(({ theme }) => ({ - display: 'flex', - width: '100%', - borderRadius: '5px', - overflow: 'hidden', - flexDirection: 'column', - justifyContent: 'space-between', - alignItems: 'center', - boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)', -})); -const StyledTableHeader = styled(TableHead)(({ theme }) => ({ - backgroundColor: '#555', // Darker shade for header - color: '#fff', -})); -const StyledTableCell = styled(TableCell)(({ theme }) => ({ - color: '#ddd', // Lighter text for better readability -})); - const CardList = () => { const { theme } = useMode(); + + const { + StyledContainer, + StyledButtonGroup, + StyledTableHeader, + StyledTableCell, + } = useCardListStyles(theme); + const { selectedCollection, getTotalPrice, removeOneFromCollection, addOneToCollection, } = useCollectionStore(); + // const { theme } = useMode(); + const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(5); const selectedCards = selectedCollection?.cards; const count = selectedCards?.length || 0; - const [chartDimensions, setChartDimensions] = useState({ - width: 0, - height: 0, - }); - const emptyRows = useMemo(() => { - return page > 0 - ? Math.max(0, (1 + page) * rowsPerPage - (selectedCards?.length || 0)) - : 0; - }, [page, rowsPerPage, selectedCards]); // <-- Dependencies for useMemo + + const emptyRows = useMemo( + () => Math.max(0, (1 + page) * rowsPerPage - (selectedCards?.length || 0)), + [page, rowsPerPage, selectedCards] + ); const handleChangePage = (event, newPage) => { cardLogger.logCardAction('Change Page', {}); @@ -109,187 +79,506 @@ const CardList = () => { }; return ( - - + - - - - - Cards in Portfolio - - {/* Include the CronTrigger button */} - - - - - Name - Price - Total Price - Quantity - TCGPlayer Price - Actions - - - - - {(rowsPerPage > 0 - ? // eslint-disable-next-line no-unsafe-optional-chaining - selectedCards?.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - : selectedCards - )?.map((card, index) => ( - - - {card?.name} - - {card?.price} - - {card?.totalPrice} - - - {card?.quantity} - - - {card.card_prices && - card.card_prices[0] && - card.card_prices[0].tcgplayer_price - ? `$${ - card?.latestPrice?.num || - card?.price || - card.card_prices[0].tcgplayer_price - }` - : 'Price not available'} - - - - - - - - - - ))} - - {emptyRows > 0 && ( - - - - )} - - - - - - -
-
- - - {`Total: $${getTotalPrice()}`} - - -
+ + + Cards in Portfolio + + +
); }; +const CardListTable = ({ + selectedCards, + rowsPerPage, + page, + handleChangePage, + handleChangeRowsPerPage, + handleRemoveCard, + handleAddCard, + emptyRows, + StyledTableHeader, + StyledTableCell, + count, + StyledButtonGroup, +}) => { + const { theme } = useMode(); + + return ( + + + + + Name + Price + Total Price + Quantity + TCGPlayer Price + Actions + + + + + {(rowsPerPage > 0 + ? selectedCards?.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ) + : selectedCards + )?.map((card, index) => ( + + + {card?.name} + + {card?.price} + + {card?.totalPrice} + + {card?.quantity} + + {/* TCGPlayer Price rendering logic */} + + + + + + + + + + ))} + + {emptyRows > 0 && ( + + + + )} + + + + + + + +
+
+ ); +}; + +const TotalPriceBox = ({ totalPrice }) => ( + + + {`Total: $${totalPrice}`} + + +); + export default CardList; + +// import React, { useMemo, useRef, useState } from 'react'; +// import { +// Box, +// Typography, +// Paper, +// Container, +// IconButton, +// Table, +// TableBody, +// TableCell, +// TableContainer, +// TableFooter, +// TablePagination, +// TableRow, +// Button, +// TableHead, +// useTheme, +// ButtonGroup, +// Divider, +// } from '@mui/material'; +// import AssessmentIcon from '@mui/icons-material/Assessment'; +// import TablePaginationActions from '../../reusable/TablePaginationActions'; +// import PropTypes from 'prop-types'; +// import { useCollectionStore } from '../../../context/CollectionContext/CollectionContext'; +// import { styled } from '@mui/styles'; +// import { useMode } from '../../../context/hooks/colormode'; +// import Logger from '../../reusable/Logger'; +// import useCardListStyles from '../../../context/hooks/useCardListStyles'; +// // Instantiate logger outside of the component +// const cardLogger = new Logger([ +// 'Action', +// 'Card Name', +// 'Quantity', +// 'Total Price', +// ]); + +// const StyledContainer = styled(Container)(({ theme }) => ({ +// display: 'flex', +// flexDirection: 'column', +// height: '100%', +// width: '100%', +// alignItems: 'center', +// // background: theme.palette.background.main, +// background: theme.palette.background.dark, +// padding: theme.spacing(2), +// color: '#fff', // White text color +// // padding: 2, +// borderRadius: 4, +// })); + +// const StyledButtonGroup = styled(ButtonGroup)(({ theme }) => ({ +// display: 'flex', +// width: '100%', +// borderRadius: '5px', +// overflow: 'hidden', +// flexDirection: 'column', +// justifyContent: 'space-between', +// alignItems: 'center', +// boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)', +// })); +// const StyledTableHeader = styled(TableHead)(({ theme }) => ({ +// backgroundColor: '#555', // Darker shade for header +// color: '#fff', +// })); +// const StyledTableCell = styled(TableCell)(({ theme }) => ({ +// color: '#ddd', // Lighter text for better readability +// })); + +// const CardList = () => { +// const { theme } = useMode(); +// const { +// StyledContainer, +// StyledButtonGroup, +// StyledTableHeader, +// StyledTableCell, +// } = useCardListStyles(); + +// const { +// selectedCollection, +// getTotalPrice, +// removeOneFromCollection, +// addOneToCollection, +// } = useCollectionStore(); +// const [page, setPage] = useState(0); +// const [rowsPerPage, setRowsPerPage] = useState(5); +// const selectedCards = selectedCollection?.cards; +// const count = selectedCards?.length || 0; +// const [chartDimensions, setChartDimensions] = useState({ +// width: 0, +// height: 0, +// }); +// const emptyRows = useMemo(() => { +// return page > 0 +// ? Math.max(0, (1 + page) * rowsPerPage - (selectedCards?.length || 0)) +// : 0; +// }, [page, rowsPerPage, selectedCards]); // <-- Dependencies for useMemo + +// const handleChangePage = (event, newPage) => { +// cardLogger.logCardAction('Change Page', {}); +// setPage(newPage); +// }; + +// const handleChangeRowsPerPage = (event) => { +// cardLogger.logCardAction('Change Rows Per Page', {}); +// setRowsPerPage(parseInt(event.target.value, 10)); +// setPage(0); +// }; + +// const handleRemoveCard = (card) => { +// removeOneFromCollection(card, selectedCollection); +// cardLogger.logCardAction('remove', card); +// }; + +// const handleAddCard = (card) => { +// addOneToCollection(card, selectedCollection); +// cardLogger.logCardAction('add', card); +// }; + +// return ( +// +// +// +// +// +// +// Cards in Portfolio +// +// {/* Include the CronTrigger button */} +// +// +// +// +// Name +// Price +// Total Price +// Quantity +// TCGPlayer Price +// Actions +// +// + +// +// {(rowsPerPage > 0 +// ? // eslint-disable-next-line no-unsafe-optional-chaining +// selectedCards?.slice( +// page * rowsPerPage, +// page * rowsPerPage + rowsPerPage +// ) +// : selectedCards +// )?.map((card, index) => ( +// +// +// {card?.name} +// +// {card?.price} +// +// {card?.totalPrice} +// +// +// {card?.quantity} +// +// +// {card.card_prices && +// card.card_prices[0] && +// card.card_prices[0].tcgplayer_price +// ? `$${ +// card?.latestPrice?.num || +// card?.price || +// card.card_prices[0].tcgplayer_price +// }` +// : 'Price not available'} +// +// +// +// +// +// +// +// +// +// ))} + +// {emptyRows > 0 && ( +// +// +// +// )} +// +// +// {/* +// +// */} +// +// +// +// +//
+//
+// +// +// {`Total: $${getTotalPrice()}`} +// +// +//
+//
+// ); +// }; + +// const TotalPriceBox = ({ totalPrice }) => ( +// +// +// {`Total: $${totalPrice}`} +// +// +// ); + +// export default CardList; diff --git a/src/components/grids/collectionGrids/SelectCollectionList.jsx b/src/components/grids/collectionGrids/SelectCollectionList.jsx index a93d0dd..f713747 100644 --- a/src/components/grids/collectionGrids/SelectCollectionList.jsx +++ b/src/components/grids/collectionGrids/SelectCollectionList.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { memo, useCallback, useEffect, useState } from 'react'; import { List, ListItem, @@ -9,100 +9,129 @@ import { Grid, Typography, useTheme, + Skeleton, } from '@mui/material'; -import { makeStyles } from '@mui/styles'; -import { useCookies } from 'react-cookie'; import PropTypes from 'prop-types'; import LoadingIndicator from '../../reusable/indicators/LoadingIndicator'; import { useCollectionStore } from '../../../context/CollectionContext/CollectionContext'; import { useStatisticsStore } from '../../../context/StatisticsContext/StatisticsContext'; -import { useMode } from '../../../context/hooks/colormode'; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import LongMenu from '../../reusable/LongMenu'; import { roundToNearestTenth } from '../../../context/Helpers'; -const useStyles = makeStyles((theme) => ({ - listItemText: { - flex: 1, - textAlign: 'left', - marginLeft: theme.spacing(3), +import useSelectCollectionListStyles from '../../../context/hooks/useSelectCollectionListStyles'; +// eslint-disable-next-line react/display-name +const ListItemSkeleton = memo( + () => { + const classes = useSelectCollectionListStyles(); + return ( + + + + ); }, - loadingContainer: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - height: '100vh', - }, - editButton: { - marginLeft: theme.spacing(2), - backgroundColor: theme.palette.primary.main, - color: '#ffffff', - '&:hover': { - backgroundColor: theme.palette.primary.dark, - }, - }, - listItem: { - position: 'relative', // Added to position the menu button absolutely - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - padding: theme.spacing(1), - backgroundColor: '#ffffff', - borderRadius: '8px', - width: '100%', - marginBottom: theme.spacing(1), - boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)', - [theme.breakpoints.up('sm')]: { - flexDirection: 'row', - padding: theme.spacing(2), - marginBottom: theme.spacing(2), - }, - }, - gridItem: { - display: 'flex', - flexDirection: 'column', - alignItems: 'flex-start', - width: '50%', // Half width for xs breakpoint - justifyContent: 'center', - padding: theme.spacing(0.5), // Reduced padding - [theme.breakpoints.up('sm')]: { - width: '100%', // Full width for larger screens - padding: theme.spacing(1), - }, - }, - gridItemText: { - fontWeight: 'bold', - fontSize: '0.8rem', // Smaller text size - [theme.breakpoints.up('sm')]: { - fontSize: '1rem', // Larger text size for larger screens - }, - }, - positivePerformance: { - color: 'green', - }, - negativePerformance: { - color: 'red', - }, - menuButton: { - position: 'absolute', - top: 0, - right: 0, - // Adjust padding and margin as needed - }, -})); + () => true +); + +// eslint-disable-next-line react/display-name +const CollectionListItem = memo( + ({ + collection, + handleSelect, + handleOpenDialog, + isSelected, + statsByCollectionId, + isPlaceholder, + }) => { + const classes = useSelectCollectionListStyles(); + const twentyFourHourChange = + statsByCollectionId[collection?._id]?.twentyFourHourAverage; + return ( + + + !isPlaceholder && handleSelect(collection?._id)} + > + + + Name: + {collection?.name} + + + Value: + + ${roundToNearestTenth(collection?.totalPrice)} + + + + + Performance: + + + {twentyFourHourChange?.priceChange > 0 ? ( + + ) : ( + + )} + {twentyFourHourChange?.percentageChange} + + + + Cards: + {collection?.totalQuantity} + + + +
+ handleOpenDialog(collection)} + onStats={() => console.log('Stats:', collection)} + onView={() => console.log('View:', collection)} + /> +
+
+ +
+ ); + }, + (prevProps, nextProps) => { + // Only re-render if specific props have changed + return ( + prevProps.collection._id === nextProps.collection._id && + prevProps.isSelected === nextProps.isSelected + ); + } +); const SelectCollectionList = ({ onSave, openDialog, handleSelectCollection, }) => { - const classes = useStyles(); + const classes = useSelectCollectionListStyles(); + const [selectedCollectionId, setSelectedCollectionId] = useState(null); + const { allCollections, setSelectedCollection, fetchAllCollectionsForUser } = useCollectionStore(); const { statsByCollectionId } = useStatisticsStore(); + const [isComponentMounted, setIsComponentMounted] = useState(false); const [isLoading, setIsLoading] = useState(false); + const handleSelect = useCallback( (selectedId) => { + setSelectedCollectionId(selectedId); // Keep track of the selected collection ID const selected = allCollections.find( (collection) => collection._id === selectedId ); @@ -114,9 +143,8 @@ const SelectCollectionList = ({ handleSelectCollection(selected?._id); onSave(selected); }, - [allCollections, onSave, setSelectedCollection] + [allCollections, onSave, setSelectedCollection, handleSelectCollection] ); - const handleOpenDialog = useCallback( (collection) => { setSelectedCollection(collection); @@ -124,88 +152,38 @@ const SelectCollectionList = ({ }, [openDialog, setSelectedCollection] ); - return ( <> {isLoading ? ( -
- -
+ + {[...new Array(5)].map( + ( + _, + index // Render 5 skeleton list items + ) => ( + + + + + ) + )} + ) : ( - {allCollections?.map((collection, index) => { - const twentyFourHourChange = - statsByCollectionId[collection?._id]?.twentyFourHourAverage; // Adjust according to your actual data structure - const isPlaceholder = index >= allCollections?.length; + {allCollections?.map((collection) => { + const isSelected = collection?._id === selectedCollectionId; return ( - - - - !isPlaceholder && handleSelect(collection._id) - } - > - - - - Name: - - {collection?.name} - - - - Value: - - - ${roundToNearestTenth(collection?.totalPrice)} - - - - - Performance: - - - {twentyFourHourChange?.priceChange > 0 ? ( - - ) : ( - - )} - {twentyFourHourChange?.percentageChange} - - - - - Cards: - - {collection?.totalQuantity} - - - - {!isPlaceholder && ( -
- handleOpenDialog(collection)} - onStats={() => console.log('Stats:', collection)} - onView={() => console.log('View:', collection)} - /> -
- )} -
- -
+ ); })}
diff --git a/src/components/grids/deckBuilderGrids/CardsGrid.js b/src/components/grids/deckBuilderGrids/CardsGrid.js index aea356c..2d0e44a 100644 --- a/src/components/grids/deckBuilderGrids/CardsGrid.js +++ b/src/components/grids/deckBuilderGrids/CardsGrid.js @@ -28,9 +28,7 @@ const CardsGrid = ({ isLoading }) => { } }, [selectedCards]); - const skeletonCount = 12; // Adjust this number based on your layout/grid - - // Show error message if error is present + const skeletonCount = 12; if (error) { return {error}; } diff --git a/src/components/grids/searchResultGrids/DeckSearchCardGrid.jsx b/src/components/grids/searchResultGrids/DeckSearchCardGrid.jsx index 67e3c08..67c655a 100644 --- a/src/components/grids/searchResultGrids/DeckSearchCardGrid.jsx +++ b/src/components/grids/searchResultGrids/DeckSearchCardGrid.jsx @@ -5,41 +5,15 @@ import { useMode } from '../../../context/hooks/colormode'; import { styled } from '@mui/system'; import SkeletonDeckItem from '../SkeletonDeckItem'; import DeckItem from '../DeckItem'; - -const StyledGrid = styled(Grid)(({ theme }) => ({ - [theme.breakpoints.down('sm')]: { - margin: theme.spacing(0.5), - }, - [theme.breakpoints.up('md')]: { - margin: theme.spacing(0.5), - }, - [theme.breakpoints.up('lg')]: { - margin: theme.spacing(2), - }, -})); - -const StyledGridItem = styled(Grid)(({ theme }) => ({ - display: 'flex', // Enable flex container - flexDirection: 'column', // Stack children vertically - alignItems: 'stretch', // Stretch children to fill the width - // height: '7%', // Make sure each item takes full available height - - [theme.breakpoints.down('sm')]: { - padding: theme.spacing(1), - }, - [theme.breakpoints.up('md')]: { - padding: theme.spacing(0.25), - }, - [theme.breakpoints.up('lg')]: { - padding: theme.spacing(1), - }, -})); +import useResponsiveStyles from '../../../context/hooks/useResponsiveStyles'; const DeckSearchCardGrid = ({ cards, userDecks }) => { const { theme } = useMode(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm')); - const isLargeScreen = useMediaQuery(theme.breakpoints.up('lg')); const [isLoading, setIsLoading] = useState(true); + const { getStyledGridStyle, getStyledGridItemStyle } = + useResponsiveStyles(theme); + const StyledGrid = getStyledGridStyle(theme); + const StyledGridItem = getStyledGridItemStyle(theme); useEffect(() => { const timer = setTimeout(() => setIsLoading(false), 1000); @@ -53,37 +27,53 @@ const DeckSearchCardGrid = ({ cards, userDecks }) => { if (isLoading) { return ( - + {Array.from({ length: 9 }).map((_, index) => ( - + - + ))} - +
); } return ( - + {uniqueCards.map((card) => ( - + - {/* */} - + {/* + /> */} - + ))} - +
); }; diff --git a/src/components/grids/storeSearchResultsGrid/ProductGrid.js b/src/components/grids/storeSearchResultsGrid/ProductGrid.js index 03fbeec..aa763b2 100644 --- a/src/components/grids/storeSearchResultsGrid/ProductGrid.js +++ b/src/components/grids/storeSearchResultsGrid/ProductGrid.js @@ -4,9 +4,12 @@ import StoreItem from '../StoreItem'; // Ensure StoreItem is wrapped with React. import CustomPagination from '../../reusable/CustomPagination'; import { useCardStore, useMode } from '../../../context'; import SkeletonStoreItem from '../SkeletonStoreItem'; // A new component for skeleton screens +import useResponsiveStyles from '../../../context/hooks/useResponsiveStyles'; const ProductGrid = ({ updateHeight }) => { const { theme } = useMode(); + const { getProductGridContainerStyle } = useResponsiveStyles(theme); // Use the hook + const containerStyles = getProductGridContainerStyle(); // Get the styles const { searchData, setSlicedAndMergedSearchData, isCardDataValid } = useCardStore(); const [page, setPage] = useState(1); @@ -32,13 +35,7 @@ const ProductGrid = ({ updateHeight }) => { return ( {isCardDataValid diff --git a/src/components/headings/header/menuItemsData.jsx b/src/components/headings/header/menuItemsData.jsx index 992d3f3..90a31e2 100644 --- a/src/components/headings/header/menuItemsData.jsx +++ b/src/components/headings/header/menuItemsData.jsx @@ -44,7 +44,8 @@ export const getMenuItemsData = (isLoggedIn) => { icon: ( { - const classes = useStyles(); - const altText = `Image for ${imgUrl || 'the card'}`; - - return ( - - ); - } -); - -ReusableCardMedia.displayName = 'ReusableCardMedia'; - -ReusableCardMedia.propTypes = { - imgUrl: PropTypes.string, - onMouseEnter: PropTypes.func, - onMouseLeave: PropTypes.func, - onClick: PropTypes.func, -}; - -export default ReusableCardMedia; diff --git a/src/components/media/CardMediaAndDetails.jsx b/src/components/media/CardMediaAndDetails.jsx deleted file mode 100644 index c0f8512..0000000 --- a/src/components/media/CardMediaAndDetails.jsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import axios from 'axios'; -import { Grid } from '@mui/material'; -import CardMediaSection from './CardMediaSection'; -import CardDetailsContainer from '../modals/cardModal/CardDetailsContainer'; -import { - useCardStore, - useCollectionStore, - useUserContext, -} from '../../context'; - -const CardMediaAndDetails = ({ card }) => { - const [updatedCard, setUpdatedCard] = useState(card); - const { updateCardInCollection } = useCardStore(); // Method to update the card in the collection - const { updateCollection, selectedCollection } = useCollectionStore(); // Method to update the card in the collection - const [imgUrl, setImgUrl] = useState(updatedCard?.card_images[0]?.image_url); // [1 - const { user } = useUserContext(); - useEffect(() => { - const updateCardData = async () => { - if (!card.card_images || card.card_images.length === 0) { - const cardId = card.id; - try { - const response = await axios.patch( - `${process.env.REACT_APP_SERVER}/api/cards/ygopro/${cardId}`, - { id: card.id, name: card.name, card: card, user: user } - ); - if (response.data && response.data.data) { - console.log('RESPONSE DATA', response.data.data); - setUpdatedCard(response.data.data); - setImgUrl(response?.data?.data?.card_images[0]?.image_url); // [1 - updateCollection(response.data.data, 'update', selectedCollection); - } - } catch (err) { - console.error('Error fetching card images:', err); - } - } - }; - - updateCardData(); - }, [card, updateCardInCollection]); - - return ( - - - - - - - - - ); -}; - -export default React.memo(CardMediaAndDetails); diff --git a/src/components/media/CardMediaSection.js b/src/components/media/CardMediaSection.js deleted file mode 100644 index 85f1012..0000000 --- a/src/components/media/CardMediaSection.js +++ /dev/null @@ -1,121 +0,0 @@ -// CardMediaSection.js -import React, { useEffect } from 'react'; -import ReusableCardMedia from './CardMedia'; -import CardToolTip from '../cards/CardToolTip'; -import { makeStyles } from '@mui/styles'; -import { Popover } from '@mui/material'; -import PropTypes from 'prop-types'; -import { useModalContext, usePopoverContext } from '../../context'; - -const useStyles = makeStyles({ - mediaContainer: { - cursor: 'pointer', // Change cursor to indicate it's clickable - position: 'relative', - }, - popover: { - pointerEvents: 'none', // Ensure popover doesn't block mouse events - }, -}); - -const anchorOrigin = { - vertical: 'bottom', - horizontal: 'left', -}; - -const transformOrigin = { - vertical: 'top', - horizontal: 'left', -}; - -const CardMediaSection = React.forwardRef( - ( - { imgUrl, card, isHovered, handleInteraction, handleClick, isRequired }, - ref - ) => { - const classes = useStyles(); - const { modalImgUrl, setModalImgUrl, clickedCard, setClickedCard } = - useModalContext(); - // console.log('CardMediaSection: ', card); - // Event handlers are now only assigned if isRequired is true - const eventHandlers = isRequired - ? { - onMouseEnter: () => { - if (typeof handleInteraction === 'function') { - handleInteraction(true); - } - }, - onMouseLeave: () => { - if (typeof handleInteraction === 'function') { - handleInteraction(false); - } - }, - onClick: () => { - if (typeof handleClick === 'function') { - handleClick(); - } - if (typeof setClickedCard === 'function') { - setClickedCard(card); - } - }, - } - : {}; - - const onClickHandler = () => { - if (handleClick) { - handleClick(); - } - }; - - useEffect(() => { - if (imgUrl && clickedCard) { - setModalImgUrl(imgUrl); - } - }, [modalImgUrl, setModalImgUrl, clickedCard]); - - return ( -
- - {isRequired && isHovered && ( - handleInteraction(false)} - anchorOrigin={anchorOrigin} - transformOrigin={transformOrigin} - disableRestoreFocus - > - - - )} -
- ); - } -); - -CardMediaSection.displayName = 'CardMediaSection'; - -CardMediaSection.propTypes = { - imgUrl: PropTypes.string.isRequired, - card: PropTypes.object.isRequired, - isHovered: PropTypes.bool, - handleInteraction: PropTypes.func, - handleClick: PropTypes.func, - setClickedCard: PropTypes.func, - isRequired: PropTypes.bool, -}; - -CardMediaSection.defaultProps = { - isHovered: false, - handleInteraction: null, - handleClick: null, - setClickedCard: null, - isRequired: true, -}; - -export default CardMediaSection; diff --git a/src/components/media/mediaStyles.jsx b/src/components/media/mediaStyles.jsx deleted file mode 100644 index 4bccab9..0000000 --- a/src/components/media/mediaStyles.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { makeStyles } from '@mui/styles'; - -export const useStyles = makeStyles({ - mediaContainer: { - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - cursor: 'pointer', - '&:hover': { - opacity: 0.7, - }, - }, - media: { - width: '100%', - flexGrow: 1, // Allow the media part to fill the space - }, -}); diff --git a/src/components/modals/cardModal/CardDetail.jsx b/src/components/modals/cardModal/CardDetail.jsx deleted file mode 100644 index 254d20e..0000000 --- a/src/components/modals/cardModal/CardDetail.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import { Box, Typography } from '@mui/material'; - -const CardDetail = ({ icon, title, value, className }) => ( - - {icon && {icon}} - - {title}: {value || 'N/A'} - - -); - -export default CardDetail; diff --git a/src/components/modals/cardModal/CardDetailsContainer.jsx b/src/components/modals/cardModal/CardDetailsContainer.jsx deleted file mode 100644 index 4ad86ce..0000000 --- a/src/components/modals/cardModal/CardDetailsContainer.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import React from 'react'; -import { - FaDragon, - FaLevelUpAlt, - FaRegCopy, - FaRegLightbulb, - FaShieldAlt, - FaVenusMars, -} from 'react-icons/fa'; -import CardDetail from './CardDetail'; - -const CardDetailsContainer = ({ card, className }) => ( - <> - } - title="Level" - value={card?.level} - /> - } - title="Type" - value={card?.type} - /> - } - title="Race" - value={card?.race} - /> - } - title="Attribute" - value={card?.attribute} - /> - - } - title="DEF" - value={card?.def} - /> - } - title="Description" - value={card?.desc} - /> - -); - -export default CardDetailsContainer; diff --git a/src/components/modals/cardModal/GenericCardModal.jsx b/src/components/modals/cardModal/GenericCardModal.jsx deleted file mode 100644 index 3d72080..0000000 --- a/src/components/modals/cardModal/GenericCardModal.jsx +++ /dev/null @@ -1,270 +0,0 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; -import { - Dialog, - DialogTitle, - DialogContent, - Snackbar, - Alert, - Grid, - Container, - useTheme, - useMediaQuery, -} from '@mui/material'; -import { useStyles } from '../modalStyles'; -import useSnackbar from '../../../context/hooks/useSnackBar'; -import CardMediaAndDetails from '../../media/CardMediaAndDetails'; -import GenericActionButtons from '../../buttons/actionButtons/GenericActionButtons'; -import { ModalContext } from '../../../context/ModalContext/ModalContext'; -import { styled } from '@mui/styles'; -import { Swiper, SwiperSlide } from 'swiper/react'; -import { EffectCoverflow, Pagination } from 'swiper'; -import 'swiper/css'; -import 'swiper/css/effect-coverflow'; -import 'swiper/css/pagination'; -import GenericCard from '../../cards/GenericCard'; - -const GenericCardModal = ({ open, card, context, imgUrl }) => { - const classes = useStyles(); - const { openModalWithCard, closeModal, isModalOpen, modalContent } = - useContext(ModalContext); - const [snackbar, handleSnackbar, handleCloseSnackbar] = useSnackbar(); - const [hasLoggedCard, setHasLoggedCard] = useState(false); - const [currentIndex, setCurrentIndex] = useState(2); - const middleRef = useRef(null); - const carouselCards = useMemo(() => [card, card, card, card, card], [card]); - - useEffect(() => { - if (open && card && !hasLoggedCard) { - handleSnackbar('Card details loaded successfully.', 'success'); - setHasLoggedCard(true); - } - }, [open, card, handleSnackbar, hasLoggedCard]); - - useEffect(() => { - if (!open) { - setHasLoggedCard(false); - } - }, [open]); - - const handleActionSuccess = useCallback(() => { - handleSnackbar('Action was successful!', 'success'); - }, [handleSnackbar]); - - const handleActionFailure = useCallback( - (error) => { - console.error('Action failed:', error); - handleSnackbar('Action failed. Please try again.', 'error'); - }, - [handleSnackbar] - ); - - // Updated Grid layout styles for responsiveness - const gridStyles = { - mediaAndDetails: { - order: { xs: 1, md: 1 }, - }, - carousel: { - order: { xs: 3, md: 2 }, - display: 'flex', - justifyContent: 'center', // Center the carousel on smaller screens - }, - actionButtons: { - order: { xs: 2, md: 3 }, - }, - }; - const StyledSwiperSlide = styled('div')(() => ({ - '& .swiper-slide': { - opacity: 0.5, - transform: 'scale(0.85)', - transition: 'transform 0.3s, opacity 0.3s', - '&-active': { - opacity: 1, - transform: 'scale(1.2)', - }, - '&-next, &-prev': { - transform: 'scale(0.8)', - }, - }, - })); - - // const StyledCarousel = styled('div')(() => ({ - // bottom: theme.spacing(2), - // right: theme.spacing(2), - // width: isSmall ? '100%' : 300, - // height: isSmall ? 150 : 180, - // perspective: '1200px', - // })); - const StyledCarousel = styled('div')(({ theme }) => ({ - bottom: theme.spacing(2), - right: theme.spacing(2), - width: '100%', // Adjust based on your design requirements - height: 180, // Adjust based on your design requirements - perspective: '1200px', - })); - // const swiperSettings = { - // effect: 'coverflow', - // grabCursor: true, - // centeredSlides: true, - // slidesPerView: 'auto', - // coverflowEffect: { - // rotate: 50, - // stretch: 0, - // depth: 200, - // modifier: 1, - // slideShadows: true, - // }, - // loop: true, - // pagination: { clickable: true }, - // }; - const swiperSettings = useMemo( - () => ({ - effect: 'coverflow', - grabCursor: true, - centeredSlides: true, - slidesPerView: 'auto', - coverflowEffect: { - rotate: 50, - stretch: 0, - depth: 200, - modifier: 1, - slideShadows: true, - }, - loop: true, - pagination: { clickable: true }, - }), - [] - ); - return ( - // - // {card?.name} - // - // - // {/* Add a Container */} - // - // - // - // - // - // - // - // {carouselCards.map((carouselCard, index) => ( - // - // - // - // - // - // ))} - // - // - // - // - // {/* Action buttons can be further styled or wrapped in their own component */} - // - // - // - // - // - // - // - // - // - // {snackbar.message} - // - // - // - - {card?.name} - - - - - - - - - - {carouselCards.map((carouselCard, index) => ( - - - - - - ))} - - - - - - - - - - - - - - {snackbar.message} - - - - ); -}; - -export default GenericCardModal; diff --git a/src/components/modals/homeDetailsModal/DetailsModal.jsx b/src/components/modals/homeDetailsModal/DetailsModal.jsx deleted file mode 100644 index a3c3932..0000000 --- a/src/components/modals/homeDetailsModal/DetailsModal.jsx +++ /dev/null @@ -1,135 +0,0 @@ -/* eslint-disable max-len */ -import React, { useContext } from 'react'; -import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalCloseButton, - Box, - Link, - Text, - List, - ListItem, - Badge, - Image, - HStack, - Center, - Flex, -} from '@chakra-ui/react'; -import { FaGithub, FaExternalLinkAlt } from 'react-icons/fa'; -// import * as FaIcons from 'react-icons/fa'; -// import * as MdIcons from 'react-icons/md'; -import { ProjectContext } from '../../context/ProjectContext'; -import { useResumeContext } from '../../context/ResumeContext'; - -const ProjectDetailsModal = () => { - const { detailsModalShow, closeDetailsModal, detailsModalData } = - useContext(ProjectContext); - const { allIcons } = useResumeContext(); - - const tech = detailsModalData?.technologies?.map((iconDetail, i) => { - // Get the actual React component from our mapping using the class field - const IconComponent = allIcons[iconDetail.class]; - - return ( - - {IconComponent && } - - {iconDetail.name} - - - ); - }); - - return ( - - - - - - - {detailsModalData?.startDate} - - {detailsModalData?.title} - - -
- - {detailsModalData?.url && ( - - - - View Live - - - )} - {detailsModalData?.readmeurl && ( - - - - View on GitHub - - - )} - -
-
- - - {detailsModalData?.description} - - - {detailsModalData?.images?.map((imgSrc, idx) => ( - {`Project - ))} - - - - Technologies Used: - - {tech} - - -
-
- ); -}; - -export default ProjectDetailsModal; diff --git a/src/components/modals/modalStyles.jsx b/src/components/modals/modalStyles.jsx deleted file mode 100644 index 6d9ae29..0000000 --- a/src/components/modals/modalStyles.jsx +++ /dev/null @@ -1,86 +0,0 @@ -// import { makeStyles } from '@mui/styles'; -import { makeStyles } from '@mui/styles'; - -export const useStyles = makeStyles((theme) => ({ - flexCenter: { - display: 'flex', - alignItems: 'center', - }, - actionButtons: { - justifyContent: 'center', - gap: '1.5rem', - }, - media: { - objectFit: 'cover', - borderRadius: '4px', - boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)', - }, - details: { - gap: '1.5rem', - marginBottom: '1.5rem', - }, - dialogTitle: { - fontSize: '1.5rem', - fontWeight: 600, - color: theme.palette.primary.dark, - }, - dialogContent: { - padding: '2rem', - }, - listItem: { - justifyContent: 'space-between', - padding: theme.spacing(2), - backgroundColor: '#ffffff', - borderRadius: '8px', - marginBottom: theme.spacing(2), - boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)', - }, - listItemText: { - flex: 1, - textAlign: 'left', - marginLeft: theme.spacing(3), - }, -})); - -// export const useStyles = makeStyles((theme) => ({ -// actionButtons: { -// display: 'flex', -// justifyContent: 'center', -// alignItems: 'center', -// gap: '1.5rem', -// }, -// media: { -// objectFit: 'cover', -// borderRadius: '4px', -// boxShadow: '0px 4px 12px rgba(0, 0, 0, 0.1)', -// }, -// details: { -// display: 'flex', -// alignItems: 'center', -// gap: '1.5rem', -// marginBottom: '1.5rem', -// }, -// dialogTitle: { -// fontSize: '1.5rem', -// fontWeight: 600, -// color: theme.palette.primary.dark, -// }, -// dialogContent: { -// padding: '2rem', -// }, -// listItem: { -// display: 'flex', -// alignItems: 'center', -// justifyContent: 'space-between', -// padding: theme.spacing(2), -// backgroundColor: '#ffffff', -// borderRadius: '8px', -// marginBottom: theme.spacing(2), -// boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)', -// }, -// listItemText: { -// flex: 1, -// textAlign: 'left', -// marginLeft: theme.spacing(3), -// }, -// })); diff --git a/src/components/search/DeckSearch.js b/src/components/search/DeckSearch.js index 3f79c94..c10c10f 100644 --- a/src/components/search/DeckSearch.js +++ b/src/components/search/DeckSearch.js @@ -48,11 +48,21 @@ const DeckSearch = ({ userDecks }) => { height: '100%', }} > - + + + { - const theme = useTheme(); - - return ( - - - Your Cart - - {children} - - ); -}; - -export default CartContainer; diff --git a/src/containers/cartPageContainers/CartContent.js b/src/containers/cartPageContainers/CartContent.js deleted file mode 100644 index b801aad..0000000 --- a/src/containers/cartPageContainers/CartContent.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import { Typography, Skeleton, Box } from '@mui/material'; -import CartContainer from './CartContainer'; -import CartItem from '../../components/grids/CartItem'; -import CartTotal from '../../components/other/dataDisplay/CartTotal'; -import { useCartStore } from '../../context/CartContext/CartContext'; - -const CartContent = () => { - const { cartData, getTotalCost, isLoading } = useCartStore(); - - return ( - - {isLoading ? ( - - ) : cartData?.cart?.length > 0 ? ( - cartData.cart.map((card, index) => ( - - )) - ) : ( - - Your cart is empty. - - )} - {cartData?.cart?.length > 0 && } - - ); -}; - -export default CartContent; - -const SkeletonCartItem = () => ( - - - - - - -); diff --git a/src/containers/cartPageContainers/Checkout.jsx b/src/containers/cartPageContainers/Checkout.jsx index e4d1065..f2155a2 100644 --- a/src/containers/cartPageContainers/Checkout.jsx +++ b/src/containers/cartPageContainers/Checkout.jsx @@ -14,6 +14,9 @@ import Typography from '@mui/material/Typography'; import AddressForm from './AddressForm'; import PaymentForm from './PaymentForm'; import Review from './Review'; +import { Elements } from '@stripe/react-stripe-js'; +import { loadStripe } from '@stripe/stripe-js'; +import { useUserContext } from '../../context'; function Copyright() { return ( @@ -29,6 +32,7 @@ function Copyright() { } const steps = ['Shipping address', 'Payment details', 'Review your order']; +const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY); function getStepContent(step) { switch (step) { @@ -45,7 +49,7 @@ function getStepContent(step) { export default function Checkout() { const [activeStep, setActiveStep] = React.useState(0); - + const { user } = useUserContext(); const handleNext = () => { setActiveStep(activeStep + 1); }; @@ -55,72 +59,74 @@ export default function Checkout() { }; return ( - - - `1px solid ${t.palette.divider}`, - }} - > - - - Company name - - - - - + + + `1px solid ${t.palette.divider}`, + }} > - - Checkout - - - {steps.map((label) => ( - - {label} - - ))} - - {activeStep === steps.length ? ( - - - Thank you for your order. - - - Your order number is #2001539. We have emailed your order - confirmation, and will send you an update when your order has - shipped. - - - ) : ( - - {getStepContent(activeStep)} - - {activeStep !== 0 && ( - - )} + + + {`${user && user?.username}'s Shopping Cart`} + + + + + + + Checkout + + + {steps.map((label) => ( + + {label} + + ))} + + {activeStep === steps.length ? ( + + + Thank you for your order. + + + Your order number is #2001539. We have emailed your order + confirmation, and will send you an update when your order has + shipped. + + + ) : ( + + {getStepContent(activeStep)} + + {activeStep !== 0 && ( + + )} - - - - )} - - - - + +
+ + )} +
+ + + + ); } diff --git a/src/containers/cartPageContainers/PaymentForm.jsx b/src/containers/cartPageContainers/PaymentForm.jsx index 9116fd0..16310ec 100644 --- a/src/containers/cartPageContainers/PaymentForm.jsx +++ b/src/containers/cartPageContainers/PaymentForm.jsx @@ -1,65 +1,66 @@ -import * as React from 'react'; +import React from 'react'; +import { useStripe, useElements, CardElement } from '@stripe/react-stripe-js'; import Typography from '@mui/material/Typography'; import Grid from '@mui/material/Grid'; -import TextField from '@mui/material/TextField'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import Checkbox from '@mui/material/Checkbox'; +import Button from '@mui/material/Button'; +import { TextField } from '@mui/material'; export default function PaymentForm() { + const stripe = useStripe(); + const elements = useElements(); + + const handleSubmit = async (event) => { + event.preventDefault(); + + if (!stripe || !elements) { + return; + } + + const cardElement = elements.getElement(CardElement); + + const { error, paymentMethod } = await stripe.createPaymentMethod({ + type: 'card', + card: cardElement, + }); + + if (error) { + console.log('[error]', error); + } else { + console.log('[PaymentMethod]', paymentMethod); + // Process the payment + } + }; + return ( Payment method - - - - - - - - - - - - - - - } - label="Remember credit card details for next time" - /> +
+ + + + + + Card details + + + + + + + - +
); } diff --git a/src/containers/cartPageContainers/Review.jsx b/src/containers/cartPageContainers/Review.jsx index ad827af..cd02ae8 100644 --- a/src/containers/cartPageContainers/Review.jsx +++ b/src/containers/cartPageContainers/Review.jsx @@ -4,30 +4,31 @@ import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import Grid from '@mui/material/Grid'; +import { useCartStore } from '../../context'; -const products = [ - { - name: 'Product 1', - desc: 'A nice thing', - price: '$9.99', - }, - { - name: 'Product 2', - desc: 'Another thing', - price: '$3.45', - }, - { - name: 'Product 3', - desc: 'Something else', - price: '$6.51', - }, - { - name: 'Product 4', - desc: 'Best thing of all', - price: '$14.11', - }, - { name: 'Shipping', desc: '', price: 'Free' }, -]; +// const products = [ +// { +// name: 'Product 1', +// desc: 'A nice thing', +// price: '$9.99', +// }, +// { +// name: 'Product 2', +// desc: 'Another thing', +// price: '$3.45', +// }, +// { +// name: 'Product 3', +// desc: 'Something else', +// price: '$6.51', +// }, +// { +// name: 'Product 4', +// desc: 'Best thing of all', +// price: '$14.11', +// }, +// { name: 'Shipping', desc: '', price: 'Free' }, +// ]; const addresses = ['1 MUI Drive', 'Reactville', 'Anytown', '99999', 'USA']; const payments = [ @@ -38,14 +39,16 @@ const payments = [ ]; export default function Review() { + const { cartData } = useCartStore(); + return ( Order summary - {products.map((product) => ( - + {cartData?.cart?.map((product, index) => ( + {product.price} diff --git a/src/containers/deckBuilderPageContainers/DeckBuilderContainer.js b/src/containers/deckBuilderPageContainers/DeckBuilderContainer.js index 1febc05..14ee2e4 100644 --- a/src/containers/deckBuilderPageContainers/DeckBuilderContainer.js +++ b/src/containers/deckBuilderPageContainers/DeckBuilderContainer.js @@ -36,10 +36,10 @@ const DeckBuilderContainer = () => { return ( - + - + diff --git a/src/context/CollectionContext/CollectionContext.jsx b/src/context/CollectionContext/CollectionContext.jsx index e9ad5f1..5f9c437 100644 --- a/src/context/CollectionContext/CollectionContext.jsx +++ b/src/context/CollectionContext/CollectionContext.jsx @@ -15,8 +15,6 @@ import { handleError, removeDuplicatesFromCollection, updateCollectionArray, - filterNullPriceHistoryForCollection, - filterNullPriceHistory, handleCardAddition, handleCardRemoval, handleCardUpdate, @@ -24,8 +22,6 @@ import { updateCollectionDataEndpoint, getTotalQuantityOfSelectedCollection, getUpdatedCollectionPriceHistory, - getFilteredData2, - getFilteredData, calculateCollectionValue, getUpdatedCollectionData, initialCollectionState, @@ -34,10 +30,11 @@ import { convertToXYLabelData, getFilteredChartData, filterUniqueDataPoints, - getUniqueValidData, createPriceHistoryObject, determineCardPrice, + // convertToXYLabelData, } from './helpers.jsx'; +import { isEqual } from 'lodash'; export const CollectionContext = createContext(defaultContextValue); @@ -46,115 +43,126 @@ export const CollectionProvider = ({ children }) => { const user = cookies?.user; const userId = user?.id; // state for the collection context + const [allCollections, setAllCollections] = useState([]); + const [collectionData, setCollectionData] = useState(initialCollectionState); + const [selectedCollection, setSelectedCollection] = useState( initialCollectionState ); - const [collectionData, setCollectionData] = useState(initialCollectionState); - const [allCollections, setAllCollections] = useState([]); const [collectionsFetched, setCollectionsFetched] = useState(false); const [cardsUpdated, setCardsUpdated] = useState(false); // state for the collection context - const [collectionPriceHistory, setCollectionPriceHistory] = useState([]); - const [allXYValues, setAllXYValues] = useState([]); - const [currentChartDataSets2, setCurrentChartDataSets2] = useState([]); - const [totalPrice, setTotalPrice] = useState(0); - const [totalQuantity, setTotalQuantity] = useState(0); - - const [lastSavedPrice, setLastSavedPrice] = useState({ - num: 0, - timestamp: new Date(), - }); - const [latestPrice, setLatestPrice] = useState({ - num: 0, - timestamp: new Date(), - }); + const [collectionPriceHistory, setCollectionPriceHistory] = useState( + selectedCollection.collectionPriceHistory || [] + ); + const [allXYValues, setAllXYValues] = useState( + selectedCollection.allXYValues || [] + ); + const [currentChartDataSets2, setCurrentChartDataSets2] = useState( + selectedCollection?.currentChartDataSets2 || [] + ); + const [totalPrice, setTotalPrice] = useState( + selectedCollection?.totalPrice || 0 + ); + const [totalQuantity, setTotalQuantity] = useState( + selectedCollection?.totalQuantity || 0 + ); + const [lastSavedPrice, setLastSavedPrice] = useState( + selectedCollection?.lastSavedPrice || { + num: 0, + timestamp: new Date(), + } + ); + const [latestPrice, setLatestPrice] = useState( + selectedCollection?.latestPrice || { + num: 0, + timestamp: new Date(), + } + ); // state for the card in collection context const [quantityInCard, setQuantityInCard] = useState(0); const [priceInCard, setPriceInCard] = useState(0); - - const lastFetchedTime = useRef(null); const [unsavedCards, setUnsavedCards] = useState([]); - const [ - updatedPricesFromCombinedContext, - setUpdatedPricesFromCombinedContext, - ] = useState([]); - const [xyData, setXyData] = useState([]); - + const lastFetchedTime = useRef(null); // fetch all collections for user const fetchAndSetCollections = useCallback(async () => { - const shouldFetch = () => { - const fetchDelay = 60000; // 1 minute - const currentTime = Date.now(); - return ( - !lastFetchedTime.current || - currentTime - lastFetchedTime.current >= fetchDelay - ); - }; + if (!userId) { + console.warn('User ID is missing for fetching collections.'); + return; + } - if (!shouldFetch()) return; + const currentTime = Date.now(); + if ( + lastFetchedTime.current && + currentTime - lastFetchedTime.current < 30000 + ) { + console.log('Skipping fetch, last fetched less than 1 minute ago.'); + return; + } try { - if (!handleError(userId, 'User ID is missing.')) return; const response = await fetchWrapper( createApiUrl(`${userId}/collections`), 'GET' ); const collections = response?.data || []; - if (collections?.length > 0) { - const uniqueCollections = collections?.map( + + if (collections.length > 0) { + const uniqueCollections = collections.map( removeDuplicatesFromCollection ); - setAllCollections(uniqueCollections); - setCollectionData(uniqueCollections[0]); setSelectedCollection(uniqueCollections[0]); setCollectionsFetched(true); } + lastFetchedTime.current = Date.now(); } catch (error) { console.error(`Failed to fetch collections: ${error}`, error.message); } - }, [userId, setAllCollections, setCollectionData, setSelectedCollection]); - // set updated state for the collections + }, [userId]); + + // compare collections const updateCollectionData = useCallback( (newData) => { try { - // Function to safely check if an object has a property - const has = (obj, prop) => - Object.prototype.hasOwnProperty.call(obj, prop); - - // Determine the type of the new data - if ( - Array.isArray(newData) && - newData.every((item) => has(item, 'cards')) - ) { - // const filteredNewData = filterNullPriceHistory(newData); - - // If newData is an array of objects each containing 'cards', assume it's 'allCollections' - setAllCollections((prev) => updateCollectionArray(prev, newData)); - } else if ( - newData && - typeof newData === 'object' && - has(newData, 'cards') - ) { - // If newData is an object with a 'cards' property, assume it's 'selectedCollection' - // setSelectedCollection(filterNullPriceHistoryForCollection(newData)); - setSelectedCollection(newData); - } else if (newData && typeof newData === 'object') { - // If newData is a general object, assume it's 'collectionData' - // setCollectionData(filterNullPriceHistoryForCollection(newData)); - setCollectionData(newData); - } else { - console.warn( - 'Unable to determine the type of collection data for update.', - newData + if (Array.isArray(newData) && !isEqual(newData, allCollections)) { + setAllCollections(newData); + } + if (newData && typeof newData === 'object') { + console.log( + 'SETTING SELECTED CO IN UPDATEDCOLLECTIONDATA --------->' ); + if (newData.cards && !isEqual(newData, selectedCollection)) { + setSelectedCollection(newData); + setTotalPrice(calculateCollectionValue(newData)); + setTotalQuantity(getTotalQuantityOfSelectedCollection(newData)); + setLastSavedPrice({ + num: newData.lastSavedPrice?.num || 0, + timestamp: new Date(), + }); + setLatestPrice({ + num: newData.totalPrice || 0, + timestamp: new Date(), + }); + setCollectionPriceHistory( + getUpdatedCollectionPriceHistory(newData, newData.totalPrice || 0) + ); + setCurrentChartDataSets2( + filterUniqueDataPoints( + convertToXYLabelData(newData.collectionPriceHistory) + ) + ); + } else if (!isEqual(newData, collectionData)) { + setCollectionData(newData); + } } } catch (error) { console.error('Error updating collection data:', error); } }, - [setAllCollections, setSelectedCollection, setCollectionData] + [allCollections, selectedCollection, collectionData] ); + // create a new collection for user const createUserCollection = async ( userId, @@ -186,6 +194,7 @@ export const CollectionProvider = ({ children }) => { const url = createApiUrl(`${userId}/collections`); // console.log('Payload for user collection:', payload); const response = await fetchWrapper(url, 'POST', payload); + // updating with new collection updateCollectionData(response.data); }; // remove a collection for user @@ -207,35 +216,51 @@ export const CollectionProvider = ({ children }) => { } }; // update cards in collection - const getUpdatedCards = (collection, cardUpdate, operation) => { - if (!collection?.cards || !Array.isArray(collection.cards)) { - console.error('Invalid collection or cards data'); + const getUpdatedCards = (collection, cardToUpdate, operation) => { + if (!collection?.cards && !Array.isArray(collection?.cards)) { + console.error( + 'Invalid collection or cards data', + collection, + cardToUpdate, + operation + ); return []; } - let cardPrice; - for (const card of collection.cards) { - cardPrice = determineCardPrice(collection.cards, cardUpdate); - } - console.log('CPRICE', cardPrice); - switch (operation) { - case 'add': - return handleCardAddition(collection.cards, cardUpdate); - case 'remove': - return handleCardRemoval(collection.cards, cardUpdate); - case 'update': - return handleCardUpdate( - collection?.cards, - cardUpdate, - createPriceHistoryObject( - cardUpdate(cardPrice * cardUpdate?.quantity) - ), - collection?._id, - cardPrice - ); - default: - console.error('Unsupported operation:', operation); - return []; + try { + let cardPrice; + for (const card of collection.cards) { + cardPrice = determineCardPrice(card, cardToUpdate); // Ensure this function has its own error handling + } + + switch (operation) { + case 'add': + return handleCardAddition(collection.cards, cardToUpdate); // Ensure this function has its own error handling + case 'remove': + return handleCardRemoval(collection.cards, cardToUpdate); // Ensure this function has its own error handling + case 'update': + // Ensure cardPrice and cardToUpdate exist before proceeding + if (!cardPrice || !cardToUpdate) { + console.error('Missing card price or update information.'); + return collection.cards; // Return the unchanged cards as a safe default + } + return handleCardUpdate( + collection?.cards, + cardToUpdate, + createPriceHistoryObject(cardPrice), // Ensure this function has its own error handling + collection?._id, + cardPrice + ); // Ensure this function has its own error handling + default: + console.error('Unsupported operation:', operation); + return collection?.cards; // Return the unchanged cards as a safe default + } + } catch (error) { + console.error( + `Error in getting updated cards for operation ${operation}:`, + error + ); + return collection?.cards; // Return the unchanged cards as a safe default } }; // update collection data @@ -252,16 +277,56 @@ export const CollectionProvider = ({ children }) => { console.error('Invalid collection or cards data'); return; } + const collectionId = collectionWithCards?._id || ''; - if (!collectionId || !userId) { - console.error('Missing collection ID or user ID.'); + if (!collectionId) { + console.error( + 'Missing collection ID or user ID.', + collectionId, + collectionWithCards + ); + return; + } + + if (!userId) { + console.error('Missing user ID.', userId); + return; + } + + // Initialize updatedOperation variable to be used later + let updatedOperation = operation; + + // Define the method determination logic + const determineMethod = () => { + if ( + ['add', 'remove'].includes(operation) && + cardToUpdate?.quantity >= 1 + ) { + updatedOperation = 'update'; // Switch operation to 'update' + return 'PUT'; + } else if (operation === 'add') { + return 'POST'; + } else if (operation === 'remove') { + return 'DELETE'; + } else if (operation === 'update') { + return 'PUT'; + } else { + console.error(`Invalid operation: ${operation}`); + return null; // Or default to a safe method if appropriate + } + }; + + const method = determineMethod(); + if (!method) { + console.error(`No valid HTTP method for operation ${operation}`); return; } - const method = determineMethod(operation); + console.log('METHOD', method); const endpoint = createApiUrl( - `${userId}/collections/${collectionId}/${operation}` + `${userId}/collections/${collectionId}/${updatedOperation}` ); + let cardsPayload; const updatedCardsData = getUpdatedCards( collectionWithCards, @@ -269,17 +334,16 @@ export const CollectionProvider = ({ children }) => { operation ); - switch (operation) { + switch (updatedOperation) { case 'add': case 'update': cardsPayload = { cards: updatedCardsData }; break; case 'remove': - // console.log('UPDATED CARDS DATA', updatedCardsData.filter()); - cardsPayload = { updatedCards: [cardToUpdate] }; + cardsPayload = { cards: [updatedCardsData] }; break; default: - console.error('Unsupported operation:', operation); + console.error('Unsupported operation:', updatedOperation); return; } @@ -287,7 +351,6 @@ export const CollectionProvider = ({ children }) => { // REQUEST 1: CARDS const cardsResponse = await fetchWrapper(endpoint, method, cardsPayload); const updatedCards = cardsResponse?.cards || []; - const chartPayload = getFilteredChartData( collectionWithCards?.chartData, calculateCollectionValue(updatedCards) @@ -297,11 +360,8 @@ export const CollectionProvider = ({ children }) => { `${userId}/collections/${collectionId}/updateChartData` ); const chartResponse = await fetchWrapper(endpoint2, 'PUT', chartPayload); - console.log('UPDATED CHARTRESPONSE MESSAGE', chartResponse.message); - console.log('UPDATED CHARTRESPONSE DATA', chartResponse.allXYValues); const updatedChartData = chartResponse?.allXYValues || []; - console.log('UPDATED CHARTS', updatedChartData); const updatedCollectionUpdatedCards = getUpdatedCollectionData( collectionWithCards, @@ -320,34 +380,48 @@ export const CollectionProvider = ({ children }) => { collectionId, updatedCollectionUpdatedCards ); - const updatedCollection = { ...collectionResponse, cards: updatedCards }; - - setTotalPrice(calculateCollectionValue(updatedCollection)); - setTotalQuantity(getTotalQuantityOfSelectedCollection(updatedCollection)); - setLastSavedPrice({ - num: updatedCollection?.lastSavedPrice?.num || 0, - timestamp: new Date(), - }); - setLatestPrice({ - num: updatedCollection?.totalPrice || 0, - timestamp: new Date(), - }); - setCollectionPriceHistory( - getUpdatedCollectionPriceHistory( - updatedCollection, - updatedCollection?.totalPrice || 0 - ) - ); - setCurrentChartDataSets2( - filterUniqueDataPoints( - convertToXYLabelData(selectedCollection?.collectionPriceHistory) - ) - ); - updateCollectionData(updatedCollection); + const updatedCollection = { + ...collectionResponse, + cards: updatedCards, + chartData: { + ...collectionWithCards.chartData, + allXYValues: updatedChartData, + }, + }; - return updatedCollection; + // setTotalPrice(calculateCollectionValue(updatedCollection.collectionData)); + // setTotalQuantity( + // getTotalQuantityOfSelectedCollection(updatedCollection.collectionData) + // ); + // setLastSavedPrice({ + // num: updatedCollection?.collectionData?.lastSavedPrice?.num || 0, + // timestamp: new Date(), + // }); + // setLatestPrice({ + // num: updatedCollection?.collectionData?.totalPrice || 0, + // timestamp: new Date(), + // }); + // setCollectionPriceHistory( + // getUpdatedCollectionPriceHistory( + // updatedCollection?.collectionData, + // updatedCollection?.collectionData?.totalPrice || 0 + // ) + // ); + // setCurrentChartDataSets2( + // filterUniqueDataPoints( + // convertToXYLabelData(updatedCollection?.collectionPriceHistory) + // ) + // ); + console.log('UPDATED COLLECTION', updatedCollection?.collectionData); + updateCollectionData(updatedCollection?.collectionData); + + return updatedCollection?.collectionData; } catch (error) { - console.error(`Error in ${operation} operation:`, error); + // console.error(`Error in ${operation} operation:`, error); + console.error( + `Error in ${operation} for collection: ${collectionWithCards?._id} with card: ${cardToUpdate?.id}`, + error + ); } }; // update collection details (name, description, etc.) @@ -385,8 +459,13 @@ export const CollectionProvider = ({ children }) => { }; // handle card operations (add, remove, update) const handleCardOperation = async (card, operation, collection, userId) => { - if (!card || !collection) { - console.error('Card or collection is undefined.', card, collection); + if (!card || !collection || !userId) { + console.error( + 'Card or collection is undefined.', + card, + collection, + userId + ); return; } @@ -399,143 +478,116 @@ export const CollectionProvider = ({ children }) => { ); // console.log('UPDATED COLLECTION:', updatedCollection); - if (updatedCollection) { - const collectionToSet = { - ...updatedCollection.collectionData, - cards: updatedCollection.cards, - }; + if (updatedCollection?._id === collection?._id) { + // const collectionToSet = { + // ...updatedCollection, + // cards: updatedCollection.cards, + // }; + + // updateCollectionData(collectionToSet); + setSelectedCollection(updatedCollection); - updateCollectionData(collectionToSet); return updatedCollection; } else { - console.error('Failed to update collection.'); + console.error('Failed to update collection.', updatedCollection); } } catch (error) { - console.error(`Error handling card operation '${operation}':`, error); + console.error( + `Error handling card operation '${operation}' for card '${card}' in collection '${collection}':`, + error + ); } }; - // this useEffect is for updating currentChartDatasets with allXYValues in the state if values are added and then we filter them to remove duplicate values - useEffect(() => { - if ( - !selectedCollection?.chartData?.allXYValues || - !Array.isArray(selectedCollection?.chartData?.allXYValues) || - !selectedCollection?.chartData?.allXYValues?.length > 0 - ) - return; - setAllXYValues( - filterUniqueDataPoints( - convertToXYLabelData(selectedCollection?.collectionPriceHistory) - ) - ); - setCurrentChartDataSets2( - filterUniqueDataPoints( - convertToXYLabelData(selectedCollection?.collectionPriceHistory) - ) - // getUniqueValidData(selectedCollection?.chartData?.allXYValues) - ); - }, [selectedCollection?.chartData?.allXYValues]); - // useEffect to ensure a collection is always selected - useEffect(() => { - if (!allCollections || allCollections?.length === 0) return; - fetchAndSetCollections(); - if (!selectedCollection) { - setSelectedCollection(allCollections[0]); - } - }, []); - // useEffect for updating cards - useEffect(() => { - if (collectionsFetched === true) { - const updateCollectionWhenCardChanged = async (card, collection) => { - try { - if (!card || !collection) { - throw new Error('Card or collection is undefined.'); - } - - const updatedQuantity = card.quantity; - const updatedTotalPriceForCard = card.price * updatedQuantity; - const newCardValueWithUpdatedPrice = { - ...card, - quantity: updatedQuantity, - totalPrice: updatedTotalPriceForCard, - }; - // Update collection locally - const updatedCollection = collection.cards.map((c) => - c.card.id === card.id ? newCardValueWithUpdatedPrice : c - ); - - // console.log('UPDATED CO', updatedCollection); + // update collection when card is changed + const updateCollectionWhenCardChanged = async (card, collection, userId) => { + try { + if (!card || !collection) { + throw new Error('Card or collection is undefined.'); + } - // Reflect the change in the local state - updateCollectionData(updatedCollection); + // Assume the card object has a quantity and price property + const updatedQuantity = card?.quantity; + const updatedTotalPriceForCard = card?.price * updatedQuantity; + + // Ensure the card values are valid before proceeding + if ( + typeof updatedQuantity !== 'number' || + typeof updatedTotalPriceForCard !== 'number' + ) { + throw new Error( + `Invalid card details for card: ${JSON.stringify(card)}` + ); + } - // Send update to server - handleCardOperation( - newCardValueWithUpdatedPrice, - 'update', - updatedCollection, - userId - ); - } catch (error) { - console.error( - `Error updating total price for card ID ${card?.id}: ${error.message}`, - error - ); - } + const newCardValueWithUpdatedPrice = { + ...card, + quantity: updatedQuantity, + totalPrice: updatedTotalPriceForCard, }; - const processCollections = () => { - allCollections.forEach((collection) => { - if (!collection?._id) { - console.error('No collectionId found in collection.'); - return; - } - collection?.cards?.forEach((card) => { - if (!card) { - console.warn('Card in collection is undefined.'); - return; - } - const originalTotalPrice = card.price * card.quantity; - if (card.totalPrice !== originalTotalPrice) { - updateCollectionWhenCardChanged(card, collection); - } - }); - }); - }; + // Update collection locally, assuming the collection is a valid array of cards + const updatedCards = + collection?.cards?.map((c) => + c?.card?.id === card.id ? newCardValueWithUpdatedPrice : c + ) || []; + + console.log('UPDATED CARDS', updatedCards); + // Reflect the change in the local state and send update to server + handleCardOperation( + newCardValueWithUpdatedPrice, + 'update', + collection, + userId + ); + } catch (error) { + console.error( + `Error updating total price for card ID ${card?.id}: ${error.message}`, + error + ); + } + }; + // update collection of any of the cards in it have chanded price + const processCollections = useCallback(() => { + allCollections?.forEach((collection) => { + if (!collection?._id) { + console.error('No collectionId found in collection.'); + return; + } + collection?.cards?.forEach((card) => { + if (!card) { + console.warn('Card in collection is undefined.'); + return; + } + const originalTotalPrice = card.price * card.quantity; + if (card?.totalPrice !== originalTotalPrice) { + updateCollectionWhenCardChanged(card, collection, userId); + } + }); + }); + }, [allCollections, selectedCollection]); + // This useEffect is for processing collections when they are fetched + useEffect(() => { + if (collectionsFetched) { processCollections(); } - setCollectionsFetched(false); - setCardsUpdated(true); - }); - // useEffect to ensure allXYValues is always updated (and if it is deleted, this will set the values again) - function convertToLabelXYData(collectionPriceHistory) { - return collectionPriceHistory?.map((entry) => ({ - label: entry._id, - x: new Date(entry.timestamp), - y: entry.num, - })); - } - + }, [collectionsFetched, processCollections]); + // This useEffect is for updating allXYValues and currentChartDataSets2 useEffect(() => { - if ( - !selectedCollection?.chartData?.allXYValues || - !Array.isArray(selectedCollection?.chartData?.allXYValues) || - !selectedCollection?.chartData?.allXYValues?.length > 0 - ) { - setAllXYValues( - // ...selectedCollection.chartData.allXYValues, - filterUniqueDataPoints( - convertToLabelXYData(selectedCollection?.collectionPriceHistory) - ) - ); - setCurrentChartDataSets2( - filterUniqueDataPoints( - convertToXYLabelData(selectedCollection?.collectionPriceHistory) - ) + if (selectedCollection?.chartData?.allXYValues?.length) { + const data = filterUniqueDataPoints( + convertToXYLabelData(selectedCollection.collectionPriceHistory) ); + setAllXYValues(data); + setCurrentChartDataSets2(allXYValues); } }, [selectedCollection?.chartData?.allXYValues]); - + // This useEffect is for fetching collections when the user ID changes + useEffect(() => { + if (userId) { + fetchAndSetCollections(); + } + }, [userId, fetchAndSetCollections]); const contextValue = useMemo( () => ({ allCollections, @@ -552,8 +604,6 @@ export const CollectionProvider = ({ children }) => { setCurrentChartData2: (data) => setCurrentChartDataSets2(data), calculateCollectionValue, setTotalPrice, - setXyData, - setUpdatedPricesFromCombinedContext, getUpdatedCollection, updateCollectionState: updateCollectionData, updateCollectionDetails, @@ -570,8 +620,8 @@ export const CollectionProvider = ({ children }) => { handleCardOperation(card, 'add', collection, userId), removeOneFromCollection: (card, collection) => handleCardOperation(card, 'remove', collection, userId), - updateOneFromCollection: (card, collection) => - handleCardOperation(card, 'update', collection, userId), + // updateOneFromCollection: (card, collection) => + // handleCardOperation(card, 'update', collection, userId), updateCollection: (card, operation, collection) => handleCardOperation(card, operation, collection, userId), updateAllCollectionState: (newData) => updateCollectionData(newData), @@ -579,15 +629,13 @@ export const CollectionProvider = ({ children }) => { externalOperationHandler: (card, operation, collection) => handleCardOperation(card, operation, collection, userId), }), - [allCollections, selectedCollection, totalPrice, xyData] + [allCollections, selectedCollection, totalPrice] ); - useEffect(() => { console.log('COLLECTION CONTEXT:', { selectedCollection: contextValue.selectedCollection, allCollections: contextValue.allCollections, collectionData: contextValue.collectionData, - cards: contextValue.cards, totalPrice: contextValue.totalPrice, totalQuantity: contextValue.totalQuantity, @@ -597,12 +645,7 @@ export const CollectionProvider = ({ children }) => { latestPrice: contextValue.latestPrice, allXYValues: contextValue.allXYValues, }); - }, [selectedCollection]); - - useEffect(() => { - if (!userId) return; - fetchAndSetCollections(); - }, [userId]); + }, [selectedCollection.cards]); return ( @@ -620,124 +663,116 @@ export const useCollectionStore = () => { } return context; }; - -// ! This is the old code // useEffect(() => { -// const checkCardData = (card) => { -// const fields = [ -// // 'name', -// // 'type', -// // 'desc', -// // 'atk', -// // 'def', -// // 'level', -// // 'race', -// // 'attribute', -// 'card_images', -// 'card_prices', -// 'totalPrice', -// 'price', -// // 'card_sets', - -// // Include other card fields as necessary -// ]; -// return fields.filter( -// (field) => -// !card[field] || -// card[field] === 0 || -// (Array.isArray(card[field]) && card[field].length === 0) -// ); -// }; - -// const updateCardData = async (card) => { -// try { -// const cardId = card?.id; -// const response = await axios.patch( -// `${process.env.REACT_APP_SERVER}/api/cards/ygopro/${cardId}`, -// { -// id: cardId, -// user: user, -// card_images: card?.card_images, -// card_prices: card?.card_prices, -// card_sets: card?.card_sets, -// atk: card.atk, -// def: card.def, -// level: card.level, -// attribute: card.attribute, -// name: card.name, -// type: card.type, -// desc: card.desc, -// price: card?.card_prices[0].tcgplayer_price, -// totalPrice: card?.card_prices[0].tcgplayer_price * card?.quantity, -// // Include other card fields as necessary -// } -// ); -// return response.data.data; -// } catch (error) { -// console.error('Error updating card:', card.id, error); -// return null; -// } -// }; - -// const processCollections = async () => { -// for (const collection of allCollections) { -// for (const card of collection.cards) { -// const missingData = checkCardData(card); -// if (missingData?.length > 0) { -// console.log( -// `Card ID ${card?.id} is missing: ${missingData.join(', ')}` -// ); -// const updatedCardData = await updateCardData(card); -// if (updatedCardData) { -// // Update the card in the corresponding collection -// handleCardOperation( -// updatedCardData, -// 'update', -// collection, -// userId -// ); // This function needs to be defined to handle updating the card within the state or context -// } +// if ( +// !selectedCollection?.chartData?.allXYValues || +// !Array.isArray(selectedCollection?.chartData?.allXYValues) || +// !selectedCollection?.chartData?.allXYValues?.length > 0 +// ) +// return; +// setAllXYValues( +// filterUniqueDataPoints( +// convertToXYLabelData(selectedCollection?.collectionPriceHistory) +// ) +// ); +// setCurrentChartDataSets2( +// filterUniqueDataPoints( +// convertToXYLabelData(selectedCollection?.collectionPriceHistory) +// ) +// // getUniqueValidData(selectedCollection?.chartData?.allXYValues) +// ); +// }, [selectedCollection?.chartData?.allXYValues]); +// useEffect(() => { +// if (!allCollections || allCollections?.length === 0) return; +// fetchAndSetCollections(); +// if (!selectedCollection) { +// setSelectedCollection(allCollections[0]); +// } +// }, []); +// useEffect(() => { +// if (collectionsFetched === true) { +// const updateCollectionWhenCardChanged = async (card, collection) => { +// try { +// if (!card || !collection) { +// throw new Error('Card or collection is undefined.'); // } -// } -// } -// }; - -// processCollections(); -// }, [allCollections, userId]); // Add other dependencies as needed -// Function to compare and log changes in collections -// useEffect(() => { -// const compareAndLogChanges = (oldData, newData, collectionName) => { -// const report = {}; -// for (const key in newData) { -// if (JSON.stringify(oldData[key]) !== JSON.stringify(newData[key])) { -// report[key] = { -// oldValue: oldData[key], -// newValue: newData[key], +// const updatedQuantity = card.quantity; +// const updatedTotalPriceForCard = card.price * updatedQuantity; +// const newCardValueWithUpdatedPrice = { +// ...card, +// quantity: updatedQuantity, +// totalPrice: updatedTotalPriceForCard, // }; +// // Update collection locally +// const updatedCollection = collection.cards.map((c) => +// c.card.id === card.id ? newCardValueWithUpdatedPrice : c +// ); + +// // console.log('UPDATED CO', updatedCollection); + +// // Reflect the change in the local state +// updateCollectionData(updatedCollection); + +// // Send update to server +// handleCardOperation( +// newCardValueWithUpdatedPrice, +// 'update', +// updatedCollection, +// userId +// ); +// } catch (error) { +// console.error( +// `Error updating total price for card ID ${card?.id}: ${error.message}`, +// error +// ); // } -// } -// console.log(`Changes in ${collectionName}:`, report); -// }; - -// compareAndLogChanges( -// prevAllCollectionsRef.current, -// allCollections, -// 'All Collections' -// ); -// compareAndLogChanges( -// prevSelectedCollectionRef.current, -// selectedCollection, -// 'Selected Collection' -// ); -// compareAndLogChanges( -// prevCollectionDataRef.current, -// collectionData, -// 'Collection Data' -// ); +// }; -// // Update refs with the current state for the next comparison -// prevAllCollectionsRef.current = allCollections; -// prevSelectedCollectionRef.current = selectedCollection; -// prevCollectionDataRef.current = collectionData; -// }, [allCollections, selectedCollection, collectionData]); +// const processCollections = () => { +// allCollections.forEach((collection) => { +// if (!collection?._id) { +// console.error('No collectionId found in collection.'); +// return; +// } +// collection?.cards?.forEach((card) => { +// if (!card) { +// console.warn('Card in collection is undefined.'); +// return; +// } +// const originalTotalPrice = card.price * card.quantity; +// if (card.totalPrice !== originalTotalPrice) { +// updateCollectionWhenCardChanged(card, collection); +// } +// }); +// }); +// }; + +// processCollections(); +// } +// setCollectionsFetched(false); +// setCardsUpdated(true); +// }); +// useEffect(() => { +// if ( +// !selectedCollection?.chartData?.allXYValues || +// !Array.isArray(selectedCollection?.chartData?.allXYValues) || +// !selectedCollection?.chartData?.allXYValues?.length > 0 +// ) { +// setAllXYValues( +// // ...selectedCollection.chartData.allXYValues, +// filterUniqueDataPoints( +// convertToXYLabelData(selectedCollection?.collectionPriceHistory) +// ) +// ); +// setCurrentChartDataSets2( +// filterUniqueDataPoints( +// convertToXYLabelData(selectedCollection?.collectionPriceHistory) +// ) +// ); +// } +// }, [selectedCollection?.chartData?.allXYValues]); +// useEffect(() => { +// if (!userId) return; +// fetchAndSetCollections(); +// }, [userId]); diff --git a/src/context/CollectionContext/helpers.jsx b/src/context/CollectionContext/helpers.jsx index f48e8c1..c740034 100644 --- a/src/context/CollectionContext/helpers.jsx +++ b/src/context/CollectionContext/helpers.jsx @@ -22,6 +22,7 @@ export const initialCollectionState = { priceDifference: 0, // Initialize as 0 if not provided priceChange: 0, // Initialize as 0 if not provided collectionPriceHistory: [], + dailyCollectionPriceHistory: [], cards: [], // Initialize as empty array if not provided currentChartDataSets2: [], // Initialize as empty array if not provided chartData: { @@ -523,11 +524,17 @@ export const constructCardDifferencesPayload = ( }; export const determineMethod = (operation, cardUpdate, collection) => { - if (operation === 'remove' && cardUpdate?.quantity === 1) { + console.log('CARD UPDATE QUANTITY TEST', cardUpdate); + if (operation === 'remove' && cardUpdate?.quantity >= 1) { + return 'PUT'; + // } else if (collection?.cards?.some((card) => card?.id === cardUpdate?.id)) { + } else if (operation === 'remove') { return 'DELETE'; - } else if (collection?.cards?.some((card) => card?.id === cardUpdate?.id)) { + } else if (operation === 'update') { return 'PUT'; - } else { + } else if (operation === 'add' && cardUpdate?.quantity >= 1) { + return 'PUT'; + } else if (operation === 'add') { return 'POST'; } }; @@ -889,13 +896,15 @@ export const getAllCardPrices = (cards) => export const determineCardPrice = (card, update) => { let price = 0; // console.log('CARD UPDATE:', update); - if (card?.price) { + if (card?.price !== update?.price) { + price = update?.price; + } else { price = card?.price; } - if (update?.latestPrice?.num) { - price = update?.latestPrice?.num; - } + // if (update?.latestPrice?.num) { + // price = update?.latestPrice?.num; + // } return price || card?.card_prices[0]?.tcgplayer_price; }; @@ -908,3 +917,11 @@ export const convertToXYLabelData = (collectionPriceHistory) => { ).toLocaleTimeString()}`, // Combines price and time for the label })); }; + +// export const convertToXYLabelData = (collectionPriceHistory) => { +// return collectionPriceHistory?.map((entry) => ({ +// label: entry._id, +// x: new Date(entry.timestamp), +// y: entry.num, +// })); +// }; diff --git a/src/context/CombinedContext/CombinedProvider.jsx b/src/context/CombinedContext/CombinedProvider.jsx index 3b46b27..c162e19 100644 --- a/src/context/CombinedContext/CombinedProvider.jsx +++ b/src/context/CombinedContext/CombinedProvider.jsx @@ -357,10 +357,8 @@ export const CombinedProvider = ({ children }) => { // console.log('userId', user.userId); setDataFunctions.retrievedListOfMonitoredCards(listOfMonitoredCards); } - }, [user]); + }, [user, listOfMonitoredCards]); // ----------- CONTEXT VALUE ----------- - // if (state?.allCardPrices && state?.allCollectionData?.length > 0) { - // } const value = useMemo( () => ({ ...state, @@ -390,7 +388,9 @@ export const CombinedProvider = ({ children }) => { userId, setDataFunctions.allCollectionData, state.updatedCollection, - state, + // state.allCollectionData, + // state.collectionData, + state.cardPrices, ]); return ( diff --git a/src/context/DeckContext/DeckContext.js b/src/context/DeckContext/DeckContext.js index f02329b..2630c2e 100644 --- a/src/context/DeckContext/DeckContext.js +++ b/src/context/DeckContext/DeckContext.js @@ -73,10 +73,30 @@ export const DeckProvider = ({ children }) => { const updateAndSyncDeck = async (newDeckData) => { try { if (Array.isArray(newDeckData)) { - // If newDeckData is an array, update all decks - setAllDecks(newDeckData); + // Handle array of deck data + newDeckData.forEach(async (deck) => { + // Update each deck in the array + setAllDecks((prevDecks) => { + const updatedDecks = prevDecks.map((d) => + d._id === deck._id ? deck : d + ); + return prevDecks.some((d) => d._id === deck._id) + ? updatedDecks + : [...updatedDecks, deck]; + }); + + // Synchronize each deck with backend + if (deck._id && userId) { + const url = `${BASE_API_URL}/${userId}/decks/${deck._id}/updateDeck`; + console.log('Updating deck in backend:', deck); + await fetchWrapper(url, 'PUT', { deck }); + } else { + console.error('No deck ID or user ID found.'); + } + }); + setSelectedDeck(newDeckData[0]); } else if (newDeckData && typeof newDeckData === 'object') { - // If newDeckData is an object, update the selected deck and sync it with allDecks + // Handle single deck object setSelectedDeck(newDeckData); setDeckData(newDeckData); setAllDecks((prevDecks) => { @@ -87,6 +107,15 @@ export const DeckProvider = ({ children }) => { ? newAllDecks : [...newAllDecks, newDeckData]; }); + + // Synchronize with backend + if (newDeckData._id && userId) { + const url = `${BASE_API_URL}/${userId}/decks/${newDeckData._id}/updateDeck`; + console.log('Updating deck in backend:', newDeckData); + await fetchWrapper(url, 'PUT', { deck: newDeckData }); + } else { + console.error('No deck ID or user ID found.'); + } } else { console.warn( 'Unable to determine the type of deck data for update.', @@ -94,17 +123,6 @@ export const DeckProvider = ({ children }) => { ); return; } - - if (!userId) { - console.error('No user ID found.'); - return; - } - - const deckId = newDeckData._id || selectedDeck._id; - // Synchronize with backend - console.log('Updating deck in backend:', newDeckData); - const url = `${BASE_API_URL}/${userId}/decks/${deckId}/updateDeck`; - await fetchWrapper(url, 'PUT', { allDecks: [newDeckData] }); } catch (error) { console.error(`Failed to update deck in backend: ${error.message}`); } @@ -157,10 +175,15 @@ export const DeckProvider = ({ children }) => { }; const addCardToDeck = async (newCard, deckId) => { - if (!deckId || !newCard || !newCard.id) { - console.error('Invalid card data', deckId, newCard); + if (!newCard) { + console.error('Invalid card data', newCard); return; } + if (!deckId) { + setSelectedDeck(allDecks[0]); + console.warn('No deck ID provided. Adding card to first deck in list.'); + deckId = allDecks[0]._id; + } try { // Update deck data locally const url = `${BASE_API_URL}/${userId}/decks/${deckId}/add`; diff --git a/src/context/ModalContext/ModalContext.jsx b/src/context/ModalContext/ModalContext.jsx index d2a01a2..bec2e03 100644 --- a/src/context/ModalContext/ModalContext.jsx +++ b/src/context/ModalContext/ModalContext.jsx @@ -7,7 +7,70 @@ export const ModalProvider = ({ children }) => { const [isModalOpen, setModalOpen] = useState(false); const [clickedCard, setClickedCard] = useState(null); const [modalImgUrl, setModalImgUrl] = useState(null); + const [detailsModalShow, setDetailsModalShow] = useState(false); + const [featureData, setFeatureData] = useState({ + title: '', + description: '', + url: '', + readmeurl: '', + images: [], + startDate: '', + endDate: '', + technologies: [], + }); + const [allFeatureData, setAllFeatureData] = useState([ + { + title: 'Deck Builder', + description: + // eslint-disable-next-line max-len + 'A deck building application that allows users to search for cards and add them to their deck. Users can also create an account, save their decks, and view other users decks.', + url: 'https://github.com/reedoooo/enhanced-card-store', + readmeurl: 'deck-builder-frontend/README.md', + images: [ + 'public/images/pages/deckBuilder.png', + // eslint-disable-next-line max-len + // 'https://github.com/reedoooo/enhanced-card-store/blob/6af330ab4f553bb3279dfa8ed7d30bce098e770f/public/images/pages/deckBuilder.png', + ], + startDate: 'August 2021', + endDate: 'September 2021', + technologies: ['React', 'Node', 'Express', 'PostgreSQL'], + }, + { + title: 'Collection Tracker', + description: + // eslint-disable-next-line max-len + 'A collection tracker application that allows users to search for cards and add them to their collection. Users can also create an account, save their collection, and view other users collections.', + url: 'https://github.com/reedoooo/enhanced-card-store', + readmeurl: 'collection-tracker-frontend/README.md', + images: [ + 'public/images/pages/portfolio.png', + // eslint-disable-next-line max-len + // 'https://github.com/reedoooo/enhanced-card-store/blob/6af330ab4f553bb3279dfa8ed7d30bce098e770f/public/images/pages/portfolio.png', + ], + startDate: 'August 2021', + endDate: 'September 2021', + technologies: ['React', 'Node', 'Express', 'PostgreSQL'], + }, + { + title: 'Store', + description: + // eslint-disable-next-line max-len + 'A store application that allows users to search for cards and add them to their cart. Users can also create an account, save their cart, and view other users carts.', + url: 'https://github.com/reedoooo/enhanced-card-store', + readmeurl: 'store-frontend/README.md', + images: [ + 'public/images/pages/cart.png', + // eslint-disable-next-line max-len + // 'https://github.com/reedoooo/enhanced-card-store/blob/6af330ab4f553bb3279dfa8ed7d30bce098e770f/public/images/pages/cart.png', + // eslint-disable-next-line max-len + // '../assets/images/pages/cart.png', + ], + startDate: 'August 2021', + endDate: 'September 2021', + technologies: ['React', 'Node', 'Express', 'PostgreSQL'], + }, + ]); const openModalWithCard = (card) => { setModalContent(card); setModalOpen(true); @@ -18,6 +81,15 @@ export const ModalProvider = ({ children }) => { setModalOpen(false); }; + const showDetailsModal = (project) => { + setFeatureData(project); + setDetailsModalShow(true); + }; + + const closeDetailsModal = () => { + setDetailsModalShow(false); + }; + return ( { isModalOpen, clickedCard, modalImgUrl, + detailsModalShow, + featureData, + allFeatureData, + setModalContent, + setFeatureData, + setAllFeatureData, + showDetailsModal, + closeDetailsModal, + setDetailsModalShow, setModalImgUrl, setClickedCard, openModalWithCard, diff --git a/src/context/StylesContext/StylesContext.jsx b/src/context/StylesContext/StylesContext.jsx new file mode 100644 index 0000000..e5a5d64 --- /dev/null +++ b/src/context/StylesContext/StylesContext.jsx @@ -0,0 +1,34 @@ +// import { useMediaQuery } from '@mui/material'; +// import DeckOfCardsIcon from '../components/reusable/icons/DeckOfCardsIcon'; +// import MoneyIcon from '../components/reusable/icons/MoneyIcon'; +// import ChartsIcon from '../components/reusable/icons/ChartsIcon'; + +// const useResponsiveStyles = (theme) => { +// const isXSmall = useMediaQuery(theme.breakpoints.down('xs')); +// const isSmall = useMediaQuery(theme.breakpoints.down('sm')); +// const isMedium = useMediaQuery(theme.breakpoints.down('md')); + +// const getTypographyVariant = () => { +// if (isXSmall) return 'h4'; +// if (isSmall) return 'h3'; +// if (isMedium) return 'h2'; +// return 'h2'; +// }; + +// const getIconForTitle = (title) => { +// switch (title) { +// case 'Deck Builder': +// return ; +// case 'Collection Tracker': +// return ; +// case 'Store': +// return ; +// default: +// return null; +// } +// }; + +// return { getTypographyVariant, getIconForTitle }; +// }; + +// export default useResponsiveStyles; diff --git a/src/context/hooks/useCardActions.jsx b/src/context/hooks/useCardActions.jsx new file mode 100644 index 0000000..e0e8aaa --- /dev/null +++ b/src/context/hooks/useCardActions.jsx @@ -0,0 +1,58 @@ +import { useCallback } from 'react'; + +export const useCardActions = ( + context, + card, + selectedCollection, + selectedDeck, + addOneToCollection, + removeOneFromCollection, + addOneToDeck, + removeOneFromDeck, + addOneToCart, + removeOneFromCart, + onSuccess, + onFailure +) => { + const performAction = useCallback( + (action) => { + const actionFunctions = { + Collection: { + add: () => addOneToCollection(card, selectedCollection), + remove: () => removeOneFromCollection(card, selectedCollection), + }, + Deck: { + add: () => addOneToDeck(card, selectedDeck?._id), + remove: () => removeOneFromDeck(selectedDeck?._id, card), + }, + Cart: { + add: () => addOneToCart(card), + remove: () => removeOneFromCart(card), + }, + }; + + try { + actionFunctions[context][action]?.(); + onSuccess?.(); + } catch (error) { + onFailure?.(error); + } + }, + [ + context, + card, + selectedCollection, + selectedDeck, + addOneToCollection, + removeOneFromCollection, + addOneToDeck, + removeOneFromDeck, + addOneToCart, + removeOneFromCart, + onSuccess, + onFailure, + ] + ); + + return performAction; +}; diff --git a/src/context/hooks/useCardListStyles.jsx b/src/context/hooks/useCardListStyles.jsx new file mode 100644 index 0000000..54cb43f --- /dev/null +++ b/src/context/hooks/useCardListStyles.jsx @@ -0,0 +1,53 @@ +import { + ButtonGroup, + Container, + TableCell, + TableHead, + // useTheme, +} from '@mui/material'; +import { styled } from '@mui/styles'; + +const useCardListStyles = () => { + // const theme = useTheme(); + const StyledContainer = styled(Container)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + height: '100%', + width: '100%', + alignItems: 'center', + // background: theme.palette.background.main, + background: theme.palette.background.dark, + padding: theme.spacing(2), + color: '#fff', // White text color + // padding: 2, + borderRadius: 4, + })); + + const StyledButtonGroup = styled(ButtonGroup)(({ theme }) => ({ + display: 'flex', + width: '100%', + borderRadius: '5px', + overflow: 'hidden', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'center', + boxShadow: '0 4px 8px rgba(0, 0, 0, 0.2)', + })); + const StyledTableHeader = styled(TableHead)(({ theme }) => ({ + backgroundColor: '#555', // Darker shade for header + color: theme.palette.success.main, + })); + const StyledTableCell = styled(TableCell)(({ theme }) => ({ + color: '#ddd', // Lighter text for better readability + })); + + // Return all styled components + return { + StyledContainer, + StyledButtonGroup, + StyledTableHeader, + StyledTableCell, + }; +}; + +export default useCardListStyles; diff --git a/src/context/hooks/useCartStyles.jsx b/src/context/hooks/useCartStyles.jsx new file mode 100644 index 0000000..b7d7815 --- /dev/null +++ b/src/context/hooks/useCartStyles.jsx @@ -0,0 +1,34 @@ +// useCartStyles.js +import { useTheme } from '@mui/material'; + +const useCartStyles = () => { + const theme = useTheme(); + + const cartContainerStyles = { + width: '100%', + flexGrow: 1, + backgroundColor: theme.palette.background.default, + borderRadius: '5px', + padding: '0.5rem', + overflowY: 'auto', + [theme.breakpoints.up('sm')]: { + padding: '1rem', + }, + [theme.breakpoints.up('md')]: { + padding: '1.5rem', + }, + }; + + const cartTitleStyles = { + fontWeight: 'bold', + marginBottom: '1rem', + color: theme.palette.text.primary, + [theme.breakpoints.down('sm')]: { + fontSize: '1.75rem', + }, + }; + + return { cartContainerStyles, cartTitleStyles }; +}; + +export default useCartStyles; diff --git a/src/context/hooks/useDeckStyles.jsx b/src/context/hooks/useDeckStyles.jsx new file mode 100644 index 0000000..ef8934d --- /dev/null +++ b/src/context/hooks/useDeckStyles.jsx @@ -0,0 +1,77 @@ +// useDeckDisplayStyles.js +import { useTheme } from '@mui/material'; +import { useMode } from './colormode'; + +const useDeckStyles = () => { + // const theme = useTheme(); + const { theme } = useMode(); + const mainBoxStyles = { + padding: theme.spacing(3), + backgroundColor: theme.palette.background.default, + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + margin: 'auto', + maxWidth: '800px', // Max-width for larger screens + }; + + const paperStyles = { + padding: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shadows[4], + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, + display: 'flex', + flexDirection: 'column', + }; + + const titleTypographyStyles = { + fontWeight: 'bold', + color: theme.palette.text.primary, + marginBottom: theme.spacing(2), + }; + + const buttonStyles = { + margin: theme.spacing(1), + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + '&:hover': { + backgroundColor: theme.palette.primary.dark, + }, + display: 'flex', + alignItems: 'center', + }; + + const switchControlStyles = { + margin: theme.spacing(2), + }; + + const cardsContainerStyles = { + display: 'flex', + flexGrow: 1, + justifyContent: 'space-between', + alignItems: 'center', + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(2), + }; + + const noCardsTypographyStyles = { + marginTop: theme.spacing(2), + textAlign: 'center', + fontStyle: 'italic', + }; + + return { + mainBoxStyles, + paperStyles, + titleTypographyStyles, + buttonStyles, + switchControlStyles, + cardsContainerStyles, + noCardsTypographyStyles, + }; +}; + +export default useDeckStyles; diff --git a/src/context/hooks/useGenericActionButtonStyles.jsx b/src/context/hooks/useGenericActionButtonStyles.jsx new file mode 100644 index 0000000..2e9815c --- /dev/null +++ b/src/context/hooks/useGenericActionButtonStyles.jsx @@ -0,0 +1,53 @@ +import { useTheme } from '@mui/material/styles'; +import { useMode } from './colormode'; + +export const useGenericActionButtonStyles = () => { + const { theme } = useMode(); + const theme2 = useTheme(); + + return { + contextText: { + color: theme.palette.text.primary, + fontWeight: 'bold', + fontSize: '0.8rem', + [theme2.breakpoints.up('sm')]: { + fontSize: '1rem', + }, + }, + addButton: { + color: theme.palette.success.contrastText, + flexGrow: 1, + backgroundColor: theme.palette.success.main, + '&:hover': { + backgroundColor: theme.palette.success.darker, + }, + marginRight: theme2.spacing(0.5), + }, + removeButton: { + color: theme.palette.error.contrastText, + flexGrow: 1, + + backgroundColor: theme.palette.error.main, + '&:hover': { + backgroundColor: theme.palette.error.dark, + }, + marginRight: theme2.spacing(0.5), + }, + actionRow: { + display: 'flex', + // justifyContent: 'space-between', + alignItems: 'center', + width: '100%', + flexWrap: 'nowrap', // Prevents wrapping to the new line + }, + circleButtonContainer: { + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + width: '100%', + flexGrow: 1, + padding: theme2.spacing(1), + gap: theme2.spacing(1), + }, + }; +}; diff --git a/src/context/hooks/useResponsiveStyles.jsx b/src/context/hooks/useResponsiveStyles.jsx new file mode 100644 index 0000000..61111e8 --- /dev/null +++ b/src/context/hooks/useResponsiveStyles.jsx @@ -0,0 +1,124 @@ +import { useMediaQuery } from '@mui/material'; +import DeckOfCardsIcon from '../../components/reusable/icons/DeckOfCardsIcon'; +import MoneyIcon from '../../components/reusable/icons/MoneyIcon'; +import ChartsIcon from '../../components/reusable/icons/ChartsIcon'; + +const useResponsiveStyles = (theme) => { + const isXSmall = useMediaQuery(theme.breakpoints.down('xs')); + const isSmall = useMediaQuery(theme.breakpoints.down('sm')); + const isSmallMedium = useMediaQuery(theme.breakpoints.up('sm')); + const isMedium = useMediaQuery(theme.breakpoints.down('md')); + const isMediumLarge = useMediaQuery(theme.breakpoints.up('md')); + const isLarge = useMediaQuery(theme.breakpoints.up('lg')); + + const getTypographyVariant = () => { + if (isXSmall) return 'h4'; + if (isSmall) return 'h3'; + if (isMedium) return 'h2'; + return 'h2'; + }; + const getButtonTypographyVariant = () => { + if (isXSmall) return 'body1'; + if (isSmall) return 'body2'; + if (isMedium) return 'body3'; + if (isLarge) return 'body3'; + return 'body1'; + }; + const getButtonTypographyVariant2 = () => { + if (isXSmall) return 'h6'; + if (isSmall) return 'h6'; + if (isMedium) return 'h6'; + if (isLarge) return 'body1'; + return 'body1'; + }; + const getIconForTitle = (title) => { + switch (title) { + case 'Deck Builder': + return ; + case 'Collection Tracker': + return ; + case 'Store': + return ; + default: + return null; + } + }; + + const getProductGridContainerStyle = () => ({ + maxWidth: 'lg', + maxHeight: '100%', + display: 'flex', + flexDirection: 'column', + marginTop: theme.spacing(4), + }); + + const getHeaderStyle = (theme) => ({ + fontWeight: 'bold', + color: theme.palette.text.primary, + marginBottom: theme.spacing(2), + // Correct media query syntax in JS object + '@media (maxWidth:599.95px)': { + fontSize: '1.25rem', + }, + '@media (minWidth:600px) and (maxWidth:899.95px)': { + fontSize: '1.5rem', + }, + '@media (minWidth:900px)': { + fontSize: '1.75rem', + }, + }); + + const getStyledGridStyle = (theme) => ({ + '@media (maxWidth:599.95px)': { + margin: theme.spacing(0.5), + }, + '@media (minWidth:600px) and (maxWidth:1199.95px)': { + margin: theme.spacing(1), + }, + '@media (minWidth:1200px)': { + margin: theme.spacing(2), + }, + '@media (minWidth:1800px)': { + margin: theme.spacing(2), + }, + }); + + const getStyledGridItemStyle = (theme) => ({ + display: 'flex', + flexDirection: 'column', + alignItems: 'stretch', + // Correct media query syntax in JS object + '@media (maxWidth:599.95px)': { + padding: theme.spacing(1), + }, + '@media (minWidth:600px)': { + padding: theme.spacing(0.25), + }, + '@media (minWidth:1200px)': { + padding: theme.spacing(1), + }, + '@media (minWidth:1800px)': { + padding: theme.spacing(2), + }, + }); + + return { + isMobile: isSmall, + isXSmall, + isSmall, + isSmallMedium, + isMedium, + isMediumLarge, + isLarge, + getTypographyVariant, + getButtonTypographyVariant, + getButtonTypographyVariant2, + getIconForTitle, + getProductGridContainerStyle, + getHeaderStyle, + getStyledGridStyle, + getStyledGridItemStyle, + }; +}; + +export default useResponsiveStyles; diff --git a/src/context/hooks/useSelectCollectionListStyles.jsx b/src/context/hooks/useSelectCollectionListStyles.jsx new file mode 100644 index 0000000..970504f --- /dev/null +++ b/src/context/hooks/useSelectCollectionListStyles.jsx @@ -0,0 +1,77 @@ +import { makeStyles } from '@mui/styles'; + +const useSelectCollectionListStyles = () => { + const useStyles = makeStyles((theme) => ({ + listItemText: { + flex: 1, + textAlign: 'left', + marginLeft: theme.spacing(3), + }, + loadingContainer: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + height: '100vh', + }, + editButton: { + marginLeft: theme.spacing(2), + backgroundColor: theme.palette.primary.main, + color: '#ffffff', + '&:hover': { + backgroundColor: theme.palette.primary.dark, + }, + }, + listItem: { + position: 'relative', // Added to position the menu button absolutely + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: theme.spacing(1), + backgroundColor: '#ffffff', + borderRadius: '8px', + width: '100%', + marginBottom: theme.spacing(1), + boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)', + [theme.breakpoints.up('sm')]: { + flexDirection: 'row', + padding: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + }, + gridItem: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + width: '50%', // Half width for xs breakpoint + justifyContent: 'center', + padding: theme.spacing(0.5), // Reduced padding + [theme.breakpoints.up('sm')]: { + width: '100%', // Full width for larger screens + padding: theme.spacing(1), + }, + }, + gridItemText: { + fontWeight: 'bold', + fontSize: '0.8rem', // Smaller text size + [theme.breakpoints.up('sm')]: { + fontSize: '1rem', // Larger text size for larger screens + }, + }, + positivePerformance: { + color: 'green', + }, + negativePerformance: { + color: 'red', + }, + menuButton: { + position: 'absolute', + top: 0, + right: 0, + // Adjust padding and margin as needed + }, + })); + + return useStyles(); +}; + +export default useSelectCollectionListStyles; diff --git a/src/context/hooks/useSelectedContext.jsx b/src/context/hooks/useSelectedContext.jsx new file mode 100644 index 0000000..f4fda06 --- /dev/null +++ b/src/context/hooks/useSelectedContext.jsx @@ -0,0 +1,28 @@ +import { useState, useCallback, useEffect } from 'react'; + +const useSelectedContext = () => { + // State to hold the current selected context + const [selectedContext, setSelectedContext] = useState(null); + const [isContextSelected, setIsContextSelected] = useState(false); + + // // Method to set a new context + const setContext = useCallback((newContext) => { + console.log('Context selected:', newContext); + setSelectedContext(newContext); + }, []); + + // Method to clear the context + const clearContext = useCallback(() => { + setSelectedContext(null); + }, []); + + return { + selectedContext, + setContext, + isContextSelected, + setIsContextSelected, + clearContext, + }; +}; + +export default useSelectedContext; diff --git a/src/context/hooks/useSelectionDialog.jsx b/src/context/hooks/useSelectionDialog.jsx new file mode 100644 index 0000000..dc63e68 --- /dev/null +++ b/src/context/hooks/useSelectionDialog.jsx @@ -0,0 +1,40 @@ +import { useState, useCallback } from 'react'; +import { useCollectionStore } from '../CollectionContext/CollectionContext'; +import { useDeckStore } from '../DeckContext/DeckContext'; + +export const useSelectionDialog = ( + context, + selectedCollection, + selectedDeck, + allCollections, + allDecks +) => { + const { setSelectedCollection } = useCollectionStore(); + const { setSelectedDeck } = useDeckStore(); + const [selectDialogOpen, setSelectDialogOpen] = useState(false); + const [itemsForSelection, setItemsForSelection] = useState([]); + + const openSelectionDialog = useCallback(() => { + if (!selectedCollection || !selectedDeck) { + setItemsForSelection( + context === 'Collection' ? allCollections : allDecks + ); + setSelectDialogOpen(true); + } + }, [selectedCollection, selectedDeck, allCollections, allDecks, context]); + + const handleSelection = (item) => { + context === 'Collection' + ? setSelectedCollection(item) + : setSelectedDeck(item); + setSelectDialogOpen(false); + }; + + return { + selectDialogOpen, + itemsForSelection, + openSelectionDialog, + handleSelection, + setSelectDialogOpen, + }; +}; diff --git a/src/layout/CartContainer.jsx b/src/layout/CartContainer.jsx new file mode 100644 index 0000000..7545609 --- /dev/null +++ b/src/layout/CartContainer.jsx @@ -0,0 +1,19 @@ +// CartContainer.js +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import useCartStyles from '../context/hooks/useCartStyles'; + +const CartContainer = ({ children }) => { + const { cartContainerStyles, cartTitleStyles } = useCartStyles(); + + return ( + + + Your Cart + + {children} + + ); +}; + +export default CartContainer; diff --git a/src/layout/CartContent.js b/src/layout/CartContent.js new file mode 100644 index 0000000..1f45d6b --- /dev/null +++ b/src/layout/CartContent.js @@ -0,0 +1,68 @@ +import React from 'react'; +import { Typography, Skeleton, Box, Grid, Container } from '@mui/material'; +import CartContainer from './CartContainer'; +import CartItem from '../components/grids/CartItem'; +import CartTotal from '../components/other/dataDisplay/CartTotal'; +import { useCartStore } from '../context/CartContext/CartContext'; +import Checkout from '../containers/cartPageContainers/Checkout'; +import useResponsiveStyles from '../context/hooks/useResponsiveStyles'; +import { useMode } from '../context'; + +const CartContent = () => { + const { theme } = useMode(); + const { getProductGridContainerStyle } = useResponsiveStyles(theme); + const containerStyles = getProductGridContainerStyle(); + const { cartData, isLoading } = useCartStore(); + + const renderCartItems = () => { + if (isLoading) { + return ; + } + + return ( + + + {cartData?.cart?.map((card, index) => ( + + + + ))} + + + ); + }; + + if (!isLoading && (!cartData?.cart || cartData.cart.length === 0)) { + return ( + + + Your cart is empty. + + + ); + } + + return {renderCartItems()}; +}; + +export default CartContent; + +const SkeletonCartItem = () => ( + + + + + + +); diff --git a/src/layout/DeckDisplay.js b/src/layout/DeckDisplay.js index 5a28d08..ed1dddc 100644 --- a/src/layout/DeckDisplay.js +++ b/src/layout/DeckDisplay.js @@ -7,46 +7,55 @@ import { FormControlLabel, Switch, } from '@mui/material'; +import AppsIcon from '@mui/icons-material/Apps'; import SelectDeckList from '../components/grids/deckBuilderGrids/SelectDeckList'; import CardsGrid from '../components/grids/deckBuilderGrids/CardsGrid'; import DeckEditPanel from '../components/other/InputComponents/DeckEditPanel'; -import { useMode } from '../context/hooks/colormode'; -import AppsIcon from '@mui/icons-material/Apps'; import { useDeckStore } from '../context/DeckContext/DeckContext'; import { useUserContext } from '../context'; +import useDeckStyles from '../context/hooks/useDeckStyles'; + const DeckDisplay = () => { - const { theme } = useMode(); const { setSelectedDeck, selectedDeck, allDecks, createUserDeck, - updateAndSyncDeck, updateDeckDetails, deleteUserDeck, selectedCards, setSelectedCards, } = useDeckStore(); + const { userId } = useUserContext(); const [showAllDecks, setShowAllDecks] = useState(false); - const [isEditing, setIsEditing] = useState(true); // New state to track edit mode + const [isEditing, setIsEditing] = useState(true); const [isLoading, setIsLoading] = useState(false); + + const { + mainBoxStyles, + paperStyles, + titleTypographyStyles, + buttonStyles, + switchControlStyles, + cardsContainerStyles, + noCardsTypographyStyles, + } = useDeckStyles(); + useEffect(() => { - let isMounted = true; // flag to track whether the component is mounted + let isMounted = true; setIsLoading(true); - // Simulated delay to fetch data const timeoutId = setTimeout(() => { if (isMounted) { - // Only update state if the component is still mounted setSelectedCards(selectedDeck?.cards?.slice(0, 30) || []); setIsLoading(false); } }, 1000); return () => { - isMounted = false; // Set the flag to false when the component unmounts - clearTimeout(timeoutId); // Clear the timeout + isMounted = false; + clearTimeout(timeoutId); }; }, [selectedDeck]); @@ -56,89 +65,41 @@ const DeckDisplay = () => { setSelectedDeck(foundDeck); setSelectedCards(foundDeck?.cards?.slice(0, 30) || []); handleToggleEdit({ target: { checked: true } }); - // Reset edit mode when switching decks } }; const handleToggleEdit = (event) => { setIsEditing(event.target.checked); if (!event.target.checked) { - // Reset selected deck when switching to create mode setSelectedDeck(null); } }; return ( - - - + + + Your Decks - - {showAllDecks && ( + {showAllDecks && ( + - )} - - {/* Rest of the component content */} + + )} } label={isEditing ? 'Edit Deck' : 'Create New Deck'} - sx={{ margin: theme.spacing(2) }} + sx={switchControlStyles} /> - {isEditing ? ( selectedDeck && ( { /> ) ) : ( - // For creating a new deck createUserDeck(userId, newDeck)} isEditing={isEditing} /> )} - + {selectedDeck?.cards?.length > 0 ? ( ) : ( - + No cards to display )} diff --git a/src/pages/CartPage.js b/src/pages/CartPage.js index 0c5a5f4..0d18863 100644 --- a/src/pages/CartPage.js +++ b/src/pages/CartPage.js @@ -4,9 +4,11 @@ import { Box, Card, CardContent, Grid, useTheme } from '@mui/material'; import LoadingIndicator from '../components/reusable/indicators/LoadingIndicator'; import ErrorIndicator from '../components/reusable/indicators/ErrorIndicator'; import CustomerForm from '../components/forms/customerCheckoutForm/CustomerForm'; -import CartContent from '../containers/cartPageContainers/CartContent'; +import CartContent from '../layout/CartContent'; import { useCartStore, useMode, usePageContext } from '../context'; import PageLayout from '../layout/PageLayout'; +import Checkout from '../containers/cartPageContainers/Checkout'; +import CartSummary from '../components/other/dataDisplay/CartSummary'; const CartPage = () => { const [cookies] = useCookies(['user']); @@ -20,6 +22,8 @@ const CartPage = () => { removeOneFromCart, fetchUserCart, getTotalCost, + cartCardQuantity, + totalCost, } = useCartStore(); const { @@ -65,14 +69,31 @@ const CartPage = () => { const calculateTotalPrice = getTotalCost(); return ( - + - + { /> - + {/* */} + {/* Include Checkout component */} + + + {/* */} + diff --git a/src/pages/CollectionPage.js b/src/pages/CollectionPage.js index 2bb8eee..f192040 100644 --- a/src/pages/CollectionPage.js +++ b/src/pages/CollectionPage.js @@ -1,15 +1,13 @@ import React, { useContext, useEffect, useState } from 'react'; -import Box from '@mui/material/Box'; import CardPortfolio from '../components/collection/CardPortfolio'; import Subheader from '../components/reusable/Subheader'; import { useCollectionStore } from '../context/CollectionContext/CollectionContext'; import { ModalContext } from '../context/ModalContext/ModalContext'; -import GenericCardModal from '../components/modals/cardModal/GenericCardModal'; import { useMode } from '../context/hooks/colormode'; import { usePageContext } from '../context/PageContext/PageContext'; import HeroCenter from './pageStyles/HeroCenter'; -import { Paper } from '@mui/material'; import PageLayout from '../layout/PageLayout'; +import GenericCardDialog from '../components/dialogs/cardDialog/GenericCardDialog'; const CollectionPage = () => { const { theme } = useMode(); @@ -30,6 +28,7 @@ const CollectionPage = () => { useEffect(() => { try { + if (!selectedCollection || !selectedCollection?._id) return; // If no collection is selected, do nothing logPageData('CollectionPage', selectedCollection); // Log collection data } catch (e) { setPageError(e); // Handle any errors @@ -59,88 +58,14 @@ const CollectionPage = () => { onCollectionSelect={handleCollectionSelected} /> {isModalOpen && ( - )} - // - // - // - // {!isCollectionSelected && ( - // - // - // - // - // )} - // - // - // - // {isModalOpen && ( - // - // )} - // - // ); }; diff --git a/src/pages/DeckBuilderPage.js b/src/pages/DeckBuilderPage.js index 38a3651..744f3c0 100644 --- a/src/pages/DeckBuilderPage.js +++ b/src/pages/DeckBuilderPage.js @@ -5,7 +5,6 @@ import ErrorIndicator from '../components/reusable/indicators/ErrorIndicator'; import DeckBuilderContainer from '../containers/deckBuilderPageContainers/DeckBuilderContainer'; import { Box, Grid, Typography } from '@mui/material'; import { ModalContext } from '../context/ModalContext/ModalContext'; -import GenericCardModal from '../components/modals/cardModal/GenericCardModal'; import { useMode } from '../context/hooks/colormode'; import { DeckBuilderBanner } from './pageStyles/StyledComponents'; import UserContext, { @@ -15,6 +14,7 @@ import { usePageContext } from '../context/PageContext/PageContext'; import HeroCenter from './pageStyles/HeroCenter'; import { Container } from 'react-bootstrap'; import PageLayout from '../layout/PageLayout'; +import GenericCardDialog from '../components/dialogs/cardDialog/GenericCardDialog'; const DeckBuilderPage = () => { const [userDecks, setUserDecks] = useState([]); @@ -28,8 +28,7 @@ const DeckBuilderPage = () => { logPageData, } = usePageContext(); const { user } = useUserContext(); - const { openModalWithCard, closeModal, isModalOpen, modalContent } = - useContext(ModalContext); + const { isModalOpen, modalContent } = useContext(ModalContext); const userId = user?.id; useEffect(() => { fetchAllDecksForUser().catch((err) => @@ -67,9 +66,9 @@ const DeckBuilderPage = () => { {pageError && } {isModalOpen && ( - )} diff --git a/src/pages/HomePage.js b/src/pages/HomePage.js index 1e656a6..6963d07 100644 --- a/src/pages/HomePage.js +++ b/src/pages/HomePage.js @@ -2,74 +2,59 @@ import React, { useContext } from 'react'; import { Carousel } from 'react-responsive-carousel'; import 'react-responsive-carousel/lib/styles/carousel.min.css'; import { - Container, Typography, - Box, Stack, - Button, CssBaseline, CardActions, CardContent, CardHeader, - Card, Grid, - Paper, - useMediaQuery, + Box, } from '@mui/material'; import { useMode } from '../context/hooks/colormode'; -import useStyles from './pageStyles/styles'; -import DeckOfCardsIcon from '../components/reusable/icons/DeckOfCardsIcon'; -import MoneyIcon from '../components/reusable/icons/MoneyIcon'; -import ChartsIcon from '../components/reusable/icons/ChartsIcon'; -import { ModalContext } from '../context/ModalContext/ModalContext'; -import GenericCardModal from '../components/modals/cardModal/GenericCardModal'; import { - TertiaryContainer, - SecondaryContainer, -} from './pageStyles/StyledComponents'; + ModalContext, + useModalContext, +} from '../context/ModalContext/ModalContext'; +import GenericCardDialog from '../components/dialogs/cardDialog/GenericCardDialog'; import pages from '../assets/pages.json'; import { CarouselImage } from './pageStyles/CarouselImage'; +import DetailsModal from '../components/dialogs/homeDetailsModal/DetailsModal'; +import useResponsiveStyles from '../context/hooks/useResponsiveStyles'; +import { + HomePageBox, + CarouselContainer, + ActionButton, + FeatureCard, + MainContentContainer, + TertiaryContentContainer, + SecondaryContentContainer, + CardListItem, + CardUnorderedList, +} from './pageStyles/StyledComponents'; const HomePage = () => { const { theme } = useMode(); - const classes = useStyles(theme); - const { closeModal, isModalOpen, modalContent } = useContext(ModalContext); + const { isModalOpen, modalContent } = useContext(ModalContext); + const { allFeatureData, showDetailsModal, detailsModalShow } = + useModalContext(); const { carouselImages, tiers, introText } = pages; - const isXSmall = useMediaQuery(theme.breakpoints.down('xs')); - const isSmall = useMediaQuery(theme.breakpoints.down('sm')); - const isMedium = useMediaQuery(theme.breakpoints.down('md')); + const { getTypographyVariant, getIconForTitle } = useResponsiveStyles(theme); - const getTypographyVariant = () => { - if (isXSmall) return 'h4'; - if (isSmall) return 'h3'; - if (isMedium) return 'h2'; - return 'h2'; - }; - const getIconForTitle = (title) => { - switch (title) { - case 'Deck Builder': - return ; - case 'Collection Tracker': - return ; - case 'Store': - return ; - default: - return null; + const handleOpenModal = (itemTitle) => { + const selectedItem = allFeatureData.find( + (item) => item.title === itemTitle + ); + if (selectedItem) { + showDetailsModal(selectedItem); } }; return ( - - + + { > {introText.description} - - - - - {carouselImages?.map(({ image, caption }, index) => ( - - ))} - - - - + + + + + + {carouselImages?.map(({ image, caption }, index) => ( + + ))} + + + - - - {tiers.map((tier) => ( - - + + + {tiers.map((tier) => ( + - - - -
    - {tier.description.map((line) => ( - - {line} - - ))} -
-
- - - -
-
- ))} -
-
+ + + + + {tier.description.map((line, index) => ( + + {line} {/* Directly using line as content */} + + ))} + + + + handleOpenModal(tier.title)} + > + {tier.buttonText} + + + + + ))} + + +
{isModalOpen && ( - )} + {detailsModalShow && }
); }; diff --git a/src/pages/StorePage.js b/src/pages/StorePage.js index de59437..8c5729c 100644 --- a/src/pages/StorePage.js +++ b/src/pages/StorePage.js @@ -7,16 +7,15 @@ import ErrorIndicator from '../components/reusable/indicators/ErrorIndicator'; import HeroCenter from './pageStyles/HeroCenter'; import { useModalContext, useMode, usePageContext } from '../context'; import { gridContainerStyles, gridItemStyles } from './pageStyles/styles'; -import GenericCardModal from '../components/modals/cardModal/GenericCardModal'; import PageLayout from '../layout/PageLayout'; +import GenericCardDialog from '../components/dialogs/cardDialog/GenericCardDialog'; const StorePage = () => { const { theme } = useMode(); const { isPageLoading, pageError } = usePageContext(); const [containerHeight, setContainerHeight] = useState(0); const [searchBarFocused, setSearchBarFocused] = useState(false); - const { openModalWithCard, closeModal, isModalOpen, modalContent } = - useModalContext(); + const { openModalWithCard, isModalOpen, modalContent } = useModalContext(); if (isPageLoading) return ; if (pageError) return ; @@ -95,9 +94,9 @@ const StorePage = () => { {isModalOpen && ( - )} diff --git a/src/pages/pageStyles/StyledComponents.jsx b/src/pages/pageStyles/StyledComponents.jsx index 2837521..9187c3a 100644 --- a/src/pages/pageStyles/StyledComponents.jsx +++ b/src/pages/pageStyles/StyledComponents.jsx @@ -2,8 +2,11 @@ import { Avatar, Box, Button, + Card, Container, IconButton, + ListItemText, + Paper, Typography, } from '@mui/material'; import { styled } from '@mui/styles'; @@ -127,25 +130,25 @@ export const MainContainer3 = styled('div')(({ theme }) => ({ boxShadow: theme.shadows[3], borderRadius: theme.shape.borderRadius, })); -export const SecondaryContainer = styled('div')(({ theme }) => ({ - background: '#b7ebde', - padding: theme.spacing(2), - marginBottom: theme.spacing(2), - maxHeight: '100%', - width: '100%', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - boxShadow: theme.shadows[3], - borderRadius: theme.shape.borderRadius, -})); -export const TertiaryContainer = styled('div')(({ theme }) => ({ - padding: 3, - borderRadius: 2, - background: theme.palette.success.main, - boxShadow: theme.shadows[10], - mb: 4, -})); +// export const SecondaryContainer = styled('div')(({ theme }) => ({ +// background: '#b7ebde', +// padding: theme.spacing(2), +// marginBottom: theme.spacing(2), +// maxHeight: '100%', +// width: '100%', +// display: 'flex', +// flexDirection: 'column', +// alignItems: 'center', +// boxShadow: theme.shadows[3], +// borderRadius: theme.shape.borderRadius, +// })); +// export const TertiaryContainer = styled('div')(({ theme }) => ({ +// padding: 3, +// borderRadius: 2, +// background: theme.palette.success.main, +// boxShadow: theme.shadows[10], +// mb: 4, +// })); export const MainPaperContainer = styled('div')(({ theme }) => ({ elevation: 3, borderRadius: 2, @@ -191,7 +194,6 @@ export const DeckBuilderBanner = styled(Box)(({ theme }) => ({ // align-items: center; // background-color: #f9f9f9; // width: 100%; -// max-width: 1600px; // margin: auto; // `; export const AvatarStyled = styled(Avatar)({ @@ -404,3 +406,84 @@ export const ProfileFormContainer = styled(Box)({ boxShadow: '0 4px 8px rgba(0, 0, 0, 0.1)', borderRadius: '8px', }); + +// HOME PAGE STYLED COMPONENTS +export const HomePageBox = styled(Box)(({ theme }) => ({ + background: theme.palette.primary.light, + padding: theme.spacing(2, 4, 8), + margin: theme.spacing(1, 2, 4), + borderRadius: theme.shape.borderRadius, +})); + +export const CarouselContainer = styled(Paper)(({ theme }) => ({ + boxShadow: theme.shadows[10], + overflow: 'hidden', // ensuring the carousel stays within bounds + '&:hover': { + transform: 'scale(1.02)', // slightly reduced scale for subtlety + transition: 'transform 0.3s ease-in-out', + }, +})); + +export const FeatureCard = styled(Card)(({ theme }) => ({ + background: theme.palette.success.light, + boxShadow: theme.shadows[5], + transition: 'box-shadow 0.3s ease-in-out', // smooth transition for shadow + '&:hover': { + boxShadow: theme.shadows[15], // more prominent shadow on hover + transform: 'translateY(-3px)', // slight upward lift + }, +})); + +export const ActionButton = styled(Button)(({ theme }) => ({ + color: 'white', // Ensuring text is white for better contrast + background: theme.palette.success.main, + '&:hover': { + background: theme.palette.success.dark, // Darken button on hover for feedback + }, +})); + +export const MainContentContainer = styled(Container)(({ theme }) => ({ + padding: theme.spacing(2, 4, 6), + borderRadius: theme.shape.borderRadius, + backgroundColor: theme.palette.background.paper, + boxShadow: theme.shadows[10], + margin: theme.spacing(2, 0), // added vertical spacing +})); + +export const SecondaryContentContainer = styled('div')(({ theme }) => ({ + background: theme.palette.secondary.light, + padding: theme.spacing(2), + marginBottom: theme.spacing(2), + width: '100%', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + boxShadow: theme.shadows[3], + borderRadius: theme.shape.borderRadius, + transition: 'background-color 0.3s', // smooth background color transition +})); + +export const TertiaryContentContainer = styled('div')(({ theme }) => ({ + padding: theme.spacing(3), + borderRadius: theme.shape.borderRadius, + background: theme.palette.success.main, + boxShadow: theme.shadows[10], + marginBottom: theme.spacing(4), + transition: 'all 0.3s ease-in-out', // smooth all transitions +})); + +// Define a styled component for the unordered list +export const CardUnorderedList = styled('ul')(({ theme }) => ({ + listStyleType: 'disc', // or 'circle' or 'square' for different bullet styles + paddingLeft: theme.spacing(4), // Adjust based on theme spacing for indentation + margin: 0, // Remove default margins +})); + +// Define a styled component for the list item text +export const CardListItem = styled('li')(({ theme }) => ({ + // Apply your desired typography styles + color: theme.palette.text.primary, + paddingBottom: theme.spacing(1), // Space between list items + textAlign: 'left', // Align text to the left + fontSize: '1rem', // Adjust font size as needed +})); diff --git a/src/pages/utils/pageUtils.jsx b/src/pages/utils/pageUtils.jsx new file mode 100644 index 0000000..2867600 --- /dev/null +++ b/src/pages/utils/pageUtils.jsx @@ -0,0 +1,18 @@ +export const getTypographyVariant = () => { + if (isXSmall) return 'h4'; + if (isSmall) return 'h3'; + if (isMedium) return 'h2'; + return 'h2'; +}; +export const getIconForTitle = (title) => { + switch (title) { + case 'Deck Builder': + return ; + case 'Collection Tracker': + return ; + case 'Store': + return ; + default: + return null; + } +};