From 3186d4840634f9afebd5cfc64f92f267e8447040 Mon Sep 17 00:00:00 2001 From: Reed Vogt Date: Sat, 2 Dec 2023 12:40:27 -0800 Subject: [PATCH 1/3] Refactor and optimize collection update mechanism - Enhanced PATCH endpoint in router to ensure comprehensive data integrity checks and preservation of key card attributes like quantity, priceHistory, and latestPrice. - Implemented useEffect in context to automatically update local and server state when cards are added, removed, or their quantity changes in a collection. - Improved cron job logic to trigger only when specific conditions are met, enhancing efficiency. - Fixed issue with disappearing header title in the PortfolioContent component during chart re-renders. --- src/App.js | 10 + .../actionButtons/CardActionButtons.jsx | 8 +- src/components/chart/PortfolioChart.jsx | 44 +- .../collection/PortfolioContent.jsx | 14 +- .../collectionGrids/SelectCollectionList.jsx | 27 +- src/components/media/CardMediaAndDetails.jsx | 21 +- .../other/dataDisplay/TopCardsDisplay.jsx | 12 +- src/context/CardContext/CardStore.js | 12 +- src/context/CartContext/CartContext.js | 6 +- .../CollectionContext/CardUpdateEffect.jsx | 50 + .../CollectionContext/CollectionContext.jsx | 885 +++++++++++++----- src/context/CollectionContext/cardHelpers.jsx | 80 -- .../CollectionContext/collectionUtility.jsx | 239 +---- src/context/CollectionContext/helpers.jsx | 236 ++++- .../CombinedContext/CombinedProvider.jsx | 133 ++- src/context/CronJobContext/CronJobContext.jsx | 15 +- src/pages/LoginPage.jsx | 142 +++ src/pages/StorePage.js | 2 +- src/pages/index.js | 1 + 19 files changed, 1248 insertions(+), 689 deletions(-) create mode 100644 src/context/CollectionContext/CardUpdateEffect.jsx delete mode 100644 src/context/CollectionContext/cardHelpers.jsx create mode 100644 src/pages/LoginPage.jsx diff --git a/src/App.js b/src/App.js index 2f7d1a1..45ba8b0 100644 --- a/src/App.js +++ b/src/App.js @@ -22,6 +22,7 @@ import { // ThreeJsCube, // CardDeckAnimation, NotFoundPage, + LoginPage, } from './pages'; import { @@ -107,6 +108,14 @@ const App = () => { } }, [userId, fetchAllCollectionsForUser, fetchAllDecksForUser, fetchUserCart]); + useEffect(() => { + // if the user is redirected to the login page, show the login dialog and set the loading state to false + if (window.location.pathname === '/login') { + setShowLoginDialog(true); + setIsLoading(false); + } + }, []); + return ( <> {/* Helmet Configuration */} @@ -158,6 +167,7 @@ const App = () => { } /> } /> + } /> {/* } /> */} {/* } /> */} } />{' '} diff --git a/src/components/buttons/actionButtons/CardActionButtons.jsx b/src/components/buttons/actionButtons/CardActionButtons.jsx index 55d8658..b3f0f58 100644 --- a/src/components/buttons/actionButtons/CardActionButtons.jsx +++ b/src/components/buttons/actionButtons/CardActionButtons.jsx @@ -48,9 +48,7 @@ const CardActionButtons = ({ const isCardInContext = useCallback(() => { switch (context) { case 'Collection': - return !!selectedCollection?.cards?.find( - (c) => c?.card?.id === card?.id - ); + return !!selectedCollection?.cards?.find((c) => c?.id === card?.id); case 'Deck': return !!selectedDeck?.cards?.find((c) => c?.id === card?.id); case 'Cart': @@ -126,9 +124,9 @@ const CardActionButtons = ({ switch (context) { case 'Collection': if (action === 'add') { - addOneToCollection(card, card.id); + addOneToCollection(card); } else if (action === 'removeOne') { - removeOneFromCollection(card, card.id); + removeOneFromCollection(card); } break; case 'Deck': diff --git a/src/components/chart/PortfolioChart.jsx b/src/components/chart/PortfolioChart.jsx index c235d3f..fd68170 100644 --- a/src/components/chart/PortfolioChart.jsx +++ b/src/components/chart/PortfolioChart.jsx @@ -37,12 +37,12 @@ function handleThresholdUpdate(lastUpdateTime, setLastUpdateTime) { } return lastUpdateTime; } -const getFilteredData2 = (collection) => { +const getFilteredData2 = (collection, timeRange) => { if (!collection) { console.error('Invalid input: selectedCollection should not be null'); return []; } - return getUniqueValidData(collection.currentChartDataSets2 || []); + return getUniqueValidData(collection?.chartData?.allXYValues || []); }; const PortfolioChart = () => { const theme = useTheme(); @@ -59,36 +59,33 @@ const PortfolioChart = () => { () => handleThresholdUpdate(lastUpdateTime, setLastUpdateTime), [lastUpdateTime] ); - // const filteredChartData2 = useMemo( - // () => getFilteredData2(selectedCollection), - // [selectedCollection] - // ); - // const rawData2 = useMemo( - // () => groupAndAverageData(filteredChartData2, threshold), - // [filteredChartData2, threshold] - // ); - // const nivoReadyData2 = useMemo( - // () => convertDataForNivo2(rawData2), - // [rawData2] - // ); + const filteredChartData2 = useMemo( () => getFilteredData2(selectedCollection, timeRange), // Adjust to filter data based on timeRange [selectedCollection, timeRange] ); + if (!filteredChartData2 || filteredChartData2?.length === 0) { + console.warn( + 'Invalid input: filteredChartData2 should not be null or empty' + ); + return filteredChartData2; + } const rawData2 = useMemo( () => groupAndAverageData(filteredChartData2, threshold, timeRange), // Adjust to group and average data based on timeRange [filteredChartData2, threshold, timeRange] ); + if (!rawData2 || rawData2?.length === 0) { + console.warn('Invalid input: rawData2 should not be null or empty'); + return rawData2; + } + const nivoReadyData2 = useMemo( () => convertDataForNivo2(rawData2), [rawData2] ); - console.log('Selected Collection:', selectedCollection); - console.log('Filtered Chart Data:', filteredChartData2); - console.log('Raw Data:', rawData2); - console.log('Nivo Ready Data:', nivoReadyData2); + const HEIGHT_TO_WIDTH_RATIO = 7 / 10; useEffect(() => { @@ -118,23 +115,14 @@ const PortfolioChart = () => { alignItems: 'center', padding: theme.spacing(2), gap: theme.spacing(2), - // backgroundColor: theme.palette.background.paper, background: '#333', // Darker background for Paper - color: '#fff', // White text color border: '1px solid #555', borderRadius: 2, - // color: theme.palette.text.primary, }} > - + { const { theme } = useMode(); const { selectedCollection } = useCollectionStore(); + const [collectionName, setCollectionName] = useState( + selectedCollection?.name + ); + + // Update collectionName when selectedCollection changes + useEffect(() => { + if (selectedCollection?.name) { + setCollectionName(selectedCollection.name); + } + }, [selectedCollection]); return ( { }} > { - // if (!collection) { - // console.error('Invalid input: selectedCollection should not be null'); - // return []; - // } - // if (!statsByCollectionId) { - // console.error('Invalid input: statsByCollectionId should not be null'); - // return []; - // } - - // collectionStats = statsByCollectionId[collection?._id]; - // twentyFourHourChange = collectionStats?.twentyFourHourAverage; - - // return { collectionStats, twentyFourHourChange }; - // }; - console.log('STATS:', stats); - console.log('STATS BY COLLECTION ID:', statsByCollectionId); - console.log('SELECTED COLLECTION:', selectedCollection); - console.log('ALL COLLECTIONS:', allCollections); + // console.log('STATS:', stats); + // console.log('STATS BY COLLECTION ID:', statsByCollectionId); function roundToNearestTenth(num) { return Math.round(num * 10) / 10; } @@ -142,7 +121,7 @@ const SelectCollectionList = ({ const handleOpenDialog = useCallback( (collection) => { setSelectedCollection(collection); - console.log('SELECTED COLLECTION:', collection); + console.log('SELECTED COLLECTION IN LIST:', collection); openDialog(true); }, [openDialog, setSelectedCollection] diff --git a/src/components/media/CardMediaAndDetails.jsx b/src/components/media/CardMediaAndDetails.jsx index 081959d..71d3e75 100644 --- a/src/components/media/CardMediaAndDetails.jsx +++ b/src/components/media/CardMediaAndDetails.jsx @@ -3,22 +3,33 @@ import axios from 'axios'; import { Grid } from '@mui/material'; import CardMediaSection from './CardMediaSection'; import CardDetailsContainer from '../modals/cardModal/CardDetailsContainer'; -import { useCardStore } from '../../context'; +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/${card.id}` + `${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); - updateCardInCollection(response.data.data); // Update the card in the corresponding collection + setImgUrl(response?.data?.data?.card_images[0]?.image_url); // [1 + // updateCardInCollection(response.data.data); // Update the card in the corresponding collection + updateCollection(response.data.data, 'update', selectedCollection); } } catch (err) { console.error('Error fetching card images:', err); @@ -35,7 +46,7 @@ const CardMediaAndDetails = ({ card }) => { diff --git a/src/components/other/dataDisplay/TopCardsDisplay.jsx b/src/components/other/dataDisplay/TopCardsDisplay.jsx index 3812892..dd2746c 100644 --- a/src/components/other/dataDisplay/TopCardsDisplay.jsx +++ b/src/components/other/dataDisplay/TopCardsDisplay.jsx @@ -37,7 +37,6 @@ const CarouselCard = ({ card }) => { const { theme } = useMode(); const classes = useStyles(); const placeholderImage = '../../assets/images/placeholder.jpeg'; - const imgUrl = card?.card_images?.[0]?.image_url || placeholderImage; const { openModalWithCard, closeModal, isModalOpen, modalContent } = useContext(ModalContext); const cardRef = useRef(null); @@ -86,12 +85,7 @@ const TopCardsDisplay = () => { const latestPrice = card?.latestPrice?.num ?? 0; const lastSavedPrice = card?.lastSavedPrice?.num ?? 0; - if ( - latestPrice === 0 || - latestPrice === undefined || - lastSavedPrice === 0 || - lastSavedPrice === undefined - ) { + if (latestPrice === undefined) { console.warn(`Price missing for card: ${card.name}`); return { ...card, diff: 0 }; } @@ -107,7 +101,7 @@ const TopCardsDisplay = () => { setTop5Cards(sortedCards); }, [selectedCollection]); - const maxSteps = top5Cards.length; + const maxSteps = top5Cards?.length; const handleNext = () => setActiveStep((prevActiveStep) => prevActiveStep + 1); @@ -126,7 +120,7 @@ const TopCardsDisplay = () => { width: '100%', // Use full width of the container }} > - {top5Cards.map((card, index) => ( + {top5Cards?.map((card, index) => ( diff --git a/src/context/CardContext/CardStore.js b/src/context/CardContext/CardStore.js index 0f31636..9d78dac 100644 --- a/src/context/CardContext/CardStore.js +++ b/src/context/CardContext/CardStore.js @@ -65,7 +65,7 @@ export const CardProvider = ({ children }) => { setOrganizedSearchData(limitedCardsToRender); if (limitedCardsToRender.length >= 1) { - console.log('LIMITED CARDS TO RENDER: ', limitedCardsToRender[0]); + // console.log('LIMITED CARDS TO RENDER: ', limitedCardsToRender[0]); setSlicedSearchData(limitedCardsToRender); } } @@ -95,11 +95,17 @@ export const CardProvider = ({ children }) => { for (const collection of allCollections) { for (const card of collection.cards) { - if (!card.card_images || !card.card_sets || !card.card_prices) { + if ( + !user.id || + !card.card_images || + !card.card_sets || + !card.card_prices + ) { needsUpdate = true; const response = await axios.patch( `${process.env.REACT_APP_SERVER}/api/cards/ygopro/${card.id}`, - { id: card.id, user: user, _id: user._id } + { id: card.id, user: user.id }, + { withCredentials: true } ); if (response.data && response.data.data) { const updatedCard = response.data.data; diff --git a/src/context/CartContext/CartContext.js b/src/context/CartContext/CartContext.js index c9dea80..5f00342 100644 --- a/src/context/CartContext/CartContext.js +++ b/src/context/CartContext/CartContext.js @@ -227,9 +227,9 @@ export const CartProvider = ({ children }) => { createUserCart, }; - useEffect(() => { - console.log('CART CONTEXT: ', value.cartData); - }, [addOneToCart, removeOneFromCart, deleteFromCart]); + // useEffect(() => { + // console.log('CART CONTEXT: ', value.cartData); + // }, [addOneToCart, removeOneFromCart, deleteFromCart]); return {children}; }; diff --git a/src/context/CollectionContext/CardUpdateEffect.jsx b/src/context/CollectionContext/CardUpdateEffect.jsx new file mode 100644 index 0000000..0b71535 --- /dev/null +++ b/src/context/CollectionContext/CardUpdateEffect.jsx @@ -0,0 +1,50 @@ +import { useEffect } from 'react'; +import axios from 'axios'; +import { useCollectionStore } from './CollectionContext'; + +const CardUpdateEffect = ({ allCollections, user, updateCollectionData }) => { + const { updateCollection } = useCollectionStore(); + useEffect(() => { + const updateCardTotalPrice = async (card, collection) => { + try { + const updatedTotalPrice = card.price * card.quantity; + const response = await axios.patch( + `${process.env.REACT_APP_SERVER}/api/cards/ygopro/${card.id}`, + { + id: card.id, + user: user, + totalPrice: updatedTotalPrice, + // Include other fields as necessary + } + ); + + if (response.data && response.data.data) { + // Update card in the local state or context + updateCollection(response.data.data, 'update', collection, user.id); + } + } catch (error) { + console.error( + `Error updating total price for card ID ${card.id}:`, + error + ); + } + }; + + const processCollections = () => { + allCollections.forEach((collection) => { + collection.cards.forEach((card) => { + const originalTotalPrice = card.price * card.quantity; + if (card.totalPrice !== originalTotalPrice) { + updateCardTotalPrice(card, collection); + } + }); + }); + }; + + processCollections(); + }, [allCollections, user, updateCollectionData]); + + return null; // Since this component doesn't render anything +}; + +export default CardUpdateEffect; diff --git a/src/context/CollectionContext/CollectionContext.jsx b/src/context/CollectionContext/CollectionContext.jsx index e80f81c..713fc8d 100644 --- a/src/context/CollectionContext/CollectionContext.jsx +++ b/src/context/CollectionContext/CollectionContext.jsx @@ -16,7 +16,6 @@ import { getCardPrice, defaultContextValue, validateUserIdAndData, - getPriceChange, calculateCollectionValue, } from './collectionUtility.jsx'; import { fetchWrapper, createApiUrl } from '../Helpers.jsx'; @@ -36,7 +35,15 @@ import { filterNullPriceHistory, handleCardAddition, handleCardRemoval, + determineMethod, + determineEndpointSuffix, + createCardsPayload, + constructCardDifferencesPayload, + updateChartData, + getPriceChange, + updateCollectionDataEndpoint, } from './helpers.jsx'; +import axios from 'axios'; export const CollectionContext = createContext(defaultContextValue); @@ -57,7 +64,8 @@ export const CollectionProvider = ({ children }) => { const [currentChartDataSets2, setCurrentChartDataSets2] = useState([]); const [openChooseCollectionDialog, setOpenChooseCollectionDialog] = useState(false); - const userId = cookies?.user?.id; + const user = cookies?.user; + const userId = user?.id; // console.log('USER ID:', userId); const lastFetchedTime = useRef(null); @@ -76,16 +84,16 @@ export const CollectionProvider = ({ children }) => { try { if (!handleError(userId, 'User ID is missing.')) return; - lastFetchedTime.current = Date.now(); + // lastFetchedTime.current = Date.now(); const response = await fetchWrapper( createApiUrl(`${userId}/collections`), 'GET' ); - const collections = response.data || []; + const collections = response?.data || []; console.log('FETCHED COLLECTIONS:', collections); - if (collections.length > 0) { + if (collections?.length > 0) { const uniqueCollections = collections.map( removeDuplicatesFromCollection ); @@ -201,59 +209,51 @@ export const CollectionProvider = ({ children }) => { break; case 'remove': cardsToUpdate = handleCardRemoval(collection?.cards, cardUpdate); - console.log('CARD REMOVAL:', cardUpdate); - console.log('CARD REMOVAL:', cardsToUpdate); break; case 'update': - if (!collection?.cards) { - console.error('No cards found in the collection.'); + if ( + !collection?.cards && + !cardUpdate?.id && + !Array.isArray(collection) + ) { + console.error( + 'Missing card or collection data for update.', + collection + ); + const returnValue = Array.isArray(collection) + ? collection + : collection?.cards; return collection?.cards; } - if (!cardUpdate?.id) { - console.warn('Card ID is missing.', cardUpdate); - } // eslint-disable-next-line no-case-declarations - const cards = collection?.cards; - - for (let i = 0; i < cards?.length; i++) { - // eslint-disable-next-line no-case-declarations - if (!cards[i]?.id) { - console.warn('Card ID is missing.', cards[i]); - continue; - } - cardUpdate = cards[i]; - const cardIndex = selectedCollection?.cards?.findIndex( - (c) => c?.id === cardUpdate?.id - ); - - if (cardIndex === -1) { - console.error( - 'Card not found in the collection.', - collection?.cards[cardIndex] - ); - return collection?.cards; - } + const cardIndex = collection?.cards?.findIndex( + (c) => c?.id?.toString() === cardUpdate?.id?.toString() + ); - // eslint-disable-next-line no-case-declarations - const existingCard = collection?.cards[cardIndex]; - // eslint-disable-next-line no-case-declarations - const updatedPriceHistory = updatePriceHistory( - existingCard, - cardUpdate - ); - // eslint-disable-next-line no-case-declarations - const updatedCard = getUpdatedCard( - existingCard, - cardUpdate, - updatedPriceHistory, - collection?._id - ); - cardsToUpdate = replaceCardInArray( - collection?.cards, - updatedCard, - cardIndex - ); + if (cardIndex === -1) { + console.error('Card not found in collection for update.'); + throw new Error('Card not found in collection for update.'); // Throw an error instead of returning } + // eslint-disable-next-line no-case-declarations + const existingCard = collection?.cards[cardIndex]; + // console.log('EXISTING CARD:', existingCard); + // eslint-disable-next-line no-case-declarations + const updatedPriceHistory = updatePriceHistory( + existingCard, + cardUpdate + ); + // eslint-disable-next-line no-case-declarations + const updatedCard = getUpdatedCard( + existingCard, + cardUpdate, + updatedPriceHistory, + collection?._id + ); + cardsToUpdate = replaceCardInArray( + collection.cards, + updatedCard, + cardIndex + ); break; default: console.error('Unsupported operation:', operation); @@ -265,15 +265,16 @@ export const CollectionProvider = ({ children }) => { function getUpdatedCard(card, update, priceHistory, collectionId) { const cardPrice = determineCardPrice(card, update); - const newChartDataEntry = createChartDataEntry(totalPrice); + const newChartDataEntry = createChartDataEntry( + cardPrice * (update?.quantity || card?.quantity) + ); return { ...card, - price: cardPrice, - quantity: update.quantity || card.quantity, + price: cardPrice || card.price, + quantity: update.quantity || card.quantity || 1, collectionId: collectionId, - totalPrice: cardPrice * (update.quantity || card.quantity), - // lastSavedPrice: update.lastSavedPrice, + totalPrice: cardPrice * update.quantity || card.quantity, lastSavedPrice: { num: card.price || card.card_prices[0].tcgplayer_price, timestamp: new Date(), @@ -281,163 +282,102 @@ export const CollectionProvider = ({ children }) => { latestPrice: update.latestPrice, tag: 'monitored', chart_datasets: [...(card.chart_datasets || []), newChartDataEntry], + card_prices: update.card_prices, + card_sets: update.card_sets, + card_images: update.card_images, priceHistory: priceHistory, }; } - // Helper function to get updated collection data const getUpdatedCollectionData = ( collectionWithCards, updatedTotalPrice, newCollectionPriceHistoryObject, - updatedChartData, updatedTotalQuantity, updatedCards ) => { - // Check for null or undefined collectionWithCards if (!collectionWithCards) { console.error('No collection data provided'); - return null; // or an appropriate default object + return null; } const { allCardPrices = [], - description = '', - name = '', - // _id = '', - collectionPriceHistory = [], - cards = [], + description = selectedCollection.description || '', + name = selectedCollection.name || '', + collectionPriceHistory = selectedCollection.collectionPriceHistory || [], + chartData = selectedCollection.chartData || {}, + cards = selectedCollection.cards || [], } = collectionWithCards; - + if (!Array.isArray(collectionPriceHistory)) { + console.error( + 'collectionPriceHistory is not an array', + collectionPriceHistory + ); + return; + } return { allCardPrices, description, name, - userId: userId, // Make sure 'userId' is defined in the scope - totalPrice: updatedTotalPrice || 0, + userId: userId, + totalPrice: updatedTotalPrice, totalCost: getTotalCost(collectionWithCards), - // totalCost: updatedTotalPrice ? updatedTotalPrice?.toString() : '0', - totalQuantity: cards?.reduce( - (acc, card) => acc + (card?.quantity || 0), - 0 - ), + totalQuantity: updatedTotalQuantity || 0, quantity: cards?.length, lastSavedPrice: { num: collectionWithCards?.totalPrice || 0, - timestamp: collectionWithCards?.lastSavedPrice?.timeStamp || new Date(), + timestamp: new Date(), }, latestPrice: { num: updatedTotalPrice || 0, timestamp: new Date(), }, dailyPriceChange: - getPriceChange(currentChartDataSets2)[0]?.priceChange || '', + getPriceChange(collectionWithCards.chartData)[0]?.priceChange || '', currentChartDataSets2: filterUniqueDataPoints( - transformPriceHistoryToXY(collectionPriceHistory) + chartData?.allXYValues + // transformPriceHistoryToXY(chartData?.allXYValues) ), collectionPriceHistory: [ ...collectionPriceHistory, - newCollectionPriceHistoryObject, + newCollectionPriceHistoryObject || { + num: updatedTotalPrice, + timestamp: new Date(), + }, ], + cards: updatedCards, }; }; - const constructCardDifferencesPayload = ( - oldCollection, - newCollection, - debug = false - ) => { - const differences = {}; - - newCollection.cards.forEach((newCard) => { - const oldCard = - oldCollection.cards.find((card) => card.id === newCard.id) || {}; - - Object.keys(newCard).forEach((key) => { - if (newCard[key] !== oldCard[key]) { - if (!differences[newCard.id]) { - differences[newCard.id] = { old: {}, new: {} }; - } - differences[newCard.id].old[key] = oldCard[key]; - differences[newCard.id].new[key] = newCard[key]; - } - }); - }); - - if (debug && Object.keys(differences).length > 0) { - console.log('Card Differences:', differences); - } - - return differences; - }; const getUpdatedCollection = async ( - collectionWithCards, // updated cards - cardUpdate, // updated card + collectionWithCards, + cardUpdate, operation, userId ) => { - const collectionId = collectionWithCards?._id || collectionData?._id; - if (!collectionId) { - console.error('Collection ID is missing.', collectionId); - return; - } - - if (!userId) { - console.error('User ID is missing.', userId); + const collectionId = + collectionWithCards?._id || collectionData?._id || selectedCollection._id; + if (!collectionId || !userId) { + console.error('Missing collection ID or user ID.', collectionId, userId); return; } - const cardExists = collectionWithCards?.cards?.some( - (card) => card?.id === cardUpdate?.id - ); - - let multipleOfSameCard = []; - let cardQuantity = 0; - if (cardExists) { - multipleOfSameCard = collectionWithCards?.cards?.filter( - (card) => card?.id === cardUpdate?.id - ); - cardQuantity = multipleOfSameCard[0]?.quantity; - } - console.log('MULTIPLE OF SAME CARD:', multipleOfSameCard); - console.log('CARD QUANTITY:', cardQuantity); - console.log('CARD EXISTS:', cardExists); - - let method; - if (operation === 'remove' && cardQuantity === 1) { - method = 'DELETE'; - } else if (cardExists) { - method = 'PUT'; - } else { - method = 'POST'; - } - - let endpointSuffix; - if (operation === 'remove' && cardQuantity === 1) { - endpointSuffix = 'removeCards'; - } else { - endpointSuffix = 'updateCards'; - } + const method = determineMethod(operation, cardUpdate, collectionWithCards); const endpoint = createApiUrl( - `${userId}/collections/${collectionId}/${endpointSuffix}` + `${userId}/collections/${collectionId}/${determineEndpointSuffix( + operation + )}` ); - console.log('CARDS BEFORE: ', collectionWithCards); const updatedCards = getUpdatedCards( collectionWithCards, cardUpdate, operation - // collectionId ); - // console.log('CARDS AFTER: ', updatedCards); - // Call the function to get differences in cards const updatedTotalPrice = calculateCollectionValue(updatedCards); setTotalPrice(updatedTotalPrice); - // const updatedTotalPrice = updatedCards.reduce( - // (total, card) => total + card.price * card.quantity, - // 0 - // ); const updatedTotalQuantity = updatedCards?.reduce( (total, card) => total + card?.quantity, 0 @@ -445,109 +385,52 @@ export const CollectionProvider = ({ children }) => { const newCollectionPriceHistoryObject = createPriceHistoryObject(updatedTotalPrice); - let cardsResponse; - let cardsPayload; - if (operation === 'remove') { - const allCardsWithIds = []; - for (const card of updatedCards) { - // const cardIds = updatedCards.map((card) => card.id); - // const cardObjIds = updatedCards.map((card) => card._id); - const cardIds = { - id: card?.id, - _id: card?._id, - }; - - allCardsWithIds?.push(cardIds); - } - const removeCard = allCardsWithIds?.find( - (idPair) => idPair?.id === cardUpdate?.id - ); - cardsPayload = { cardIds: removeCard }; - } else { - const allCardsWithIds = []; - for (const card of updatedCards) { - // const cardIds = updatedCards.map((card) => card.id); - // const cardObjIds = updatedCards.map((card) => card._id); - const cardIds = { - id: card.id, - _id: card._id, - }; - - allCardsWithIds?.push(cardIds); - } - const removeCard = allCardsWithIds?.find( - (idPair) => idPair?.id === cardUpdate?.id - ); - cardsPayload = { cards: updatedCards, cardIds: removeCard }; - } - console.log('CARDS PAYLOAD:', cardsPayload); + let cardsPayload = createCardsPayload(operation, updatedCards, cardUpdate); - cardsResponse = await fetchWrapper(endpoint, method, cardsPayload); + let cardsResponse = await fetchWrapper(endpoint, method, cardsPayload); const { cardMessage } = cardsResponse; - console.log('CARDS AFTER: ', cardsResponse.cards); - const cardDifferences = constructCardDifferencesPayload( - collectionWithCards, - { cards: cardsResponse.cards }, - true - ); - console.log('CARD DIFFERENCES:', cardDifferences); const updatedChartData = getFilteredChartData( collectionWithCards.chartData, updatedTotalPrice ); - const chartDataPayload = { chartData: updatedChartData }; - const chartDataEndpoint = createApiUrl( - `${userId}/collections/${collectionId}/updateChartData` - ); - const chartDataResponse = await fetchWrapper( - chartDataEndpoint, - 'PUT', - chartDataPayload + const chartDataResponse = await updateChartData( + userId, + collectionId, + updatedChartData ); - const { chartMessage } = chartDataResponse; - const updatedCollection = getUpdatedCollectionData( selectedCollection, updatedTotalPrice, newCollectionPriceHistoryObject, - // updatedChartData, - updatedTotalQuantity - // updatedCards + updatedTotalQuantity, + updatedCards ); - const collectionEndpoint = createApiUrl( - `${userId}/collections/${collectionId}` + const collectionResponse = await updateCollectionDataEndpoint( + userId, + collectionId, + updatedCollection ); - const collectionResponse = await fetchWrapper(collectionEndpoint, 'PUT', { - updatedCollection, - }); - - const { collectionMessage } = collectionResponse; + // console.log('COLLECTION RESPONSE', collectionResponse); setTotalPrice(calculateCollectionValue(updatedCards)); + const restructuredCollection = { - ...collectionResponse.collectionData, - cards: cardsResponse.cards, - chartData: chartDataResponse.chartData, + ...collectionResponse?.collectionData, + cards: cardsResponse?.cards, + chartData: chartDataResponse?.chartData, }; - console.log('COLLECTION MESSAGE:', collectionMessage); - console.log('RESTRUCTURED COLLECTION:', restructuredCollection); - const filteredRestructuredCollection = filterNullPriceHistoryForCollection( restructuredCollection ); - - console.log( - 'FILTERED RESTRUCTURED COLLECTION:', - filteredRestructuredCollection + setCurrentChartDataSets2( + filteredRestructuredCollection?.chartData?.allXYValues ); - return { - filteredRestructuredCollection, - }; - }; + return { filteredRestructuredCollection }; + }; const updateCollectionDetails = async (updatedInfo, userId, collectionId) => { const { name, description } = updatedInfo; if (selectedCollection && collectionId) { @@ -578,7 +461,7 @@ export const CollectionProvider = ({ children }) => { ); const { collectionMessage } = collectionResponse; - console.log(collectionMessage); + // console.log(collectionMessage); // Optionally handle the response here, e.g., update the state with the response data if necessary } catch (error) { @@ -588,35 +471,25 @@ export const CollectionProvider = ({ children }) => { console.error('Selected collection or collection ID is missing.'); } }; - - const handleCardOperation = async ( - card, - operation, - collection, - userId, - allCollections - ) => { + const handleCardOperation = async (card, operation, collection, userId) => { if (!card) { console.error('Card is undefined.', card); return; } - if (!collection) { console.error( - `Collection with ID ${collection._id} not found in allCollections.` + `Collection with ID ${collection?._id} not found in allCollections.` ); return; } - const updatedCollection = await getUpdatedCollection( collection, card, operation, userId ); - if (updatedCollection) { - console.log('UPDATED COLLECTION:', updatedCollection); + // console.log('UPDATED COLLECTION:', updatedCollection); updateCollectionData(updatedCollection?.filteredRestructuredCollection); } else { console.error('Failed to update collection.'); @@ -624,6 +497,178 @@ export const CollectionProvider = ({ children }) => { return updatedCollection; }; + // CardUpdateEffect(allCollections, user, updateCollectionData); + // useEffect(() => { + // const updateCardTotalPrice = async (card, collection) => { + // try { + // const updatedTotalPriceForCard = card.price * card.quantity; + // const newCardValueWithUpdatedPrice = { + // ...card, + // totalPrice: updatedTotalPriceForCard, + // }; + // handleCardOperation( + // newCardValueWithUpdatedPrice, + // 'update', + // collection, + // userId + // ); + // } catch (error) { + // console.error( + // `Error updating total price for card ID ${card.id}:`, + // error + // ); + // } + // }; + + // const processCollections = () => { + // allCollections?.forEach((collection) => { + // collection?.cards?.forEach((card) => { + // const originalTotalPrice = card?.price * card?.quantity; + // if (card?.totalPrice !== originalTotalPrice) { + // updateCardTotalPrice(card, collection); + // } + // }); + // }); + // }; + + // processCollections(); + // }, [allCollections, user, updateCollectionData]); + useEffect(() => { + if (!allCollections || !user) return; + const updateCollectionWhenCardChanged = async (card, collection) => { + try { + const updatedQuantity = card.quantity; + const updatedTotalPriceForCard = card.price * card.quantity; + const newCardValueWithUpdatedPrice = { + ...card, + quantity: updatedQuantity, + totalPrice: updatedTotalPriceForCard, + }; + + // Update collection locally + const updatedCollection = collection?.cards?.map((c) => + c.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 + ); + } + }; + + const processCollections = () => { + allCollections?.forEach((collection) => { + if (!collection?._id) { + console.error('No collectionId found in collection.'); + return; + } + collection?.cards?.forEach((card) => { + const originalTotalPrice = card?.price * card?.quantity; + if (card?.totalPrice !== originalTotalPrice) { + updateCollectionWhenCardChanged(card, collection); + } + }); + }); + }; + + processCollections(); + }, []); + + 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 + } + } + } + } + }; + + processCollections(); + }, [allCollections, user.id]); // Add other dependencies as needed const contextValue = useMemo( () => ({ @@ -662,7 +707,7 @@ export const CollectionProvider = ({ children }) => { updateCollectionState: updateCollectionData, updateCollectionDetails, fetchAllCollectionsForUser: fetchAndSetCollections, - fetchAllCollections: fetchAndSetCollections, + // fetchAllCollections: fetchAndSetCollections, setSelectedCollection, setAllCollections, }), @@ -683,27 +728,22 @@ export const CollectionProvider = ({ children }) => { }, [userId, selectedCollection]); useEffect(() => { - if (userId) fetchAndSetCollections(); + if (userId && typeof userId === 'string') fetchAndSetCollections(); }, [userId]); useEffect(() => { + if (!userId || !allCollections) return; if (selectedCollection?.cards) { setTotalPrice(calculateCollectionValue(selectedCollection)); setTotalCost(getTotalCost(selectedCollection)); } }, [selectedCollection, setTotalPrice]); - // useEffect(() => { - // if (allCollections?.length === 0 || allCollections === undefined) { - // fetchAndSetCollections(); - // } - // }, [allCollections, fetchAndSetCollections]); - useEffect(() => { - if (selectedCollection === null || selectedCollection === undefined) { - setSelectedCollection(allCollections?.[0]); + if (allCollections?.length === 0 || allCollections === undefined) { + fetchAndSetCollections(); } - }, [userId]); + }, [allCollections, fetchAndSetCollections]); return ( @@ -721,3 +761,332 @@ export const useCollectionStore = () => { } return context; }; + +// const getUpdatedCards = (collection, cardUpdate, operation) => { +// let cardsToUpdate; + +// switch (operation) { +// case 'add': +// cardsToUpdate = handleCardAddition(collection?.cards, cardUpdate); +// break; +// case 'remove': +// cardsToUpdate = handleCardRemoval(collection?.cards, cardUpdate); +// console.log('CARD REMOVAL:', cardUpdate); +// console.log('CARD REMOVAL:', cardsToUpdate); +// break; +// case 'update': +// if (!collection?.cards) { +// console.error('No cards found in the collection.'); +// return collection?.cards; +// } +// if (!cardUpdate?.id) { +// console.warn('Card ID is missing.', cardUpdate); +// } +// // eslint-disable-next-line no-case-declarations +// const cards = collection?.cards; + +// for (let i = 0; i < cards?.length; i++) { +// // eslint-disable-next-line no-case-declarations +// if (!cards[i]?.id) { +// console.warn('Card ID is missing.', cards[i]); +// continue; +// } +// cardUpdate = cards[i]; +// const cardIndex = selectedCollection?.cards?.findIndex( +// (c) => c?.id === cardUpdate?.id +// ); + +// if (cardIndex === -1) { +// console.error( +// 'Card not found in the collection.', +// collection?.cards[cardIndex] +// ); +// return collection?.cards; +// } + +// // eslint-disable-next-line no-case-declarations +// const existingCard = collection?.cards[cardIndex]; +// // eslint-disable-next-line no-case-declarations +// const updatedPriceHistory = updatePriceHistory( +// existingCard, +// cardUpdate +// ); +// // eslint-disable-next-line no-case-declarations +// const updatedCard = getUpdatedCard( +// existingCard, +// cardUpdate, +// updatedPriceHistory, +// collection?._id +// ); +// cardsToUpdate = replaceCardInArray( +// collection?.cards, +// updatedCard, +// cardIndex +// ); +// } +// break; +// default: +// console.error('Unsupported operation:', operation); +// return collection?.cards; +// } + +// return cardsToUpdate; +// }; + +// function getUpdatedCard(card, update, priceHistory, collectionId) { +// const cardPrice = determineCardPrice(card, update); +// const newChartDataEntry = createChartDataEntry(totalPrice); + +// return { +// ...card, +// price: cardPrice, +// quantity: update.quantity || card.quantity, +// collectionId: collectionId, +// totalPrice: cardPrice * (update?.quantity || card?.quantity), +// // totalPrice: cardPrice * (update.quantity || card.quantity), +// // lastSavedPrice: update.lastSavedPrice, +// lastSavedPrice: { +// num: card.price || card.card_prices[0].tcgplayer_price, +// timestamp: new Date(), +// }, +// latestPrice: update.latestPrice, +// tag: 'monitored', +// chart_datasets: [...(card.chart_datasets || []), newChartDataEntry], +// card_prices: update.card_prices, +// card_sets: update.card_sets, +// card_images: update.card_images, +// priceHistory: priceHistory, +// }; +// } +// const getUpdatedCollectionData = ( +// collectionWithCards, +// updatedTotalPrice, +// newCollectionPriceHistoryObject, +// updatedChartData, +// updatedTotalQuantity, +// updatedCards +// ) => { +// // Check for null or undefined collectionWithCards +// if (!collectionWithCards) { +// console.error('No collection data provided'); +// return null; // or an appropriate default object +// } + +// const { +// allCardPrices = [], +// description = '', +// name = '', +// // _id = '', +// collectionPriceHistory = [], +// cards = [], +// } = collectionWithCards; + +// return { +// allCardPrices, +// description, +// name, +// userId: userId, // Make sure 'userId' is defined in the scope +// totalPrice: updatedTotalPrice || 0, +// totalCost: getTotalCost(collectionWithCards), +// // totalCost: updatedTotalPrice ? updatedTotalPrice?.toString() : '0', +// totalQuantity: cards?.reduce( +// (acc, card) => acc + (card?.quantity || 0), +// 0 +// ), +// quantity: cards?.length, +// lastSavedPrice: { +// num: collectionWithCards?.totalPrice || 0, +// timestamp: collectionWithCards?.lastSavedPrice?.timeStamp || new Date(), +// }, +// latestPrice: { +// num: updatedTotalPrice || 0, +// timestamp: new Date(), +// }, +// dailyPriceChange: +// getPriceChange(currentChartDataSets2)[0]?.priceChange || '', +// currentChartDataSets2: filterUniqueDataPoints( +// transformPriceHistoryToXY(collectionPriceHistory) +// ), +// collectionPriceHistory: [ +// ...collectionPriceHistory, +// newCollectionPriceHistoryObject, +// ], +// }; +// }; +// const getUpdatedCollection = async ( +// collectionWithCards, // updated cards +// cardUpdate, // updated card +// operation, +// userId +// ) => { +// const collectionId = collectionWithCards?._id || collectionData?._id; +// if (!collectionId) { +// console.error('Collection ID is missing.', collectionId); +// return; +// } + +// if (!userId) { +// console.error('User ID is missing.', userId); +// return; +// } +// const cardExists = collectionWithCards?.cards?.some( +// (card) => card?.id === cardUpdate?.id +// ); + +// let multipleOfSameCard = []; +// let cardQuantity = 0; +// if (cardExists) { +// multipleOfSameCard = collectionWithCards?.cards?.filter( +// (card) => card?.id === cardUpdate?.id +// ); +// cardQuantity = multipleOfSameCard[0]?.quantity; +// } +// console.log('MULTIPLE OF SAME CARD:', multipleOfSameCard); +// console.log('CARD QUANTITY:', cardQuantity); +// console.log('CARD EXISTS:', cardExists); + +// let method; +// if (operation === 'remove' && cardQuantity === 1) { +// method = 'DELETE'; +// } else if (cardExists) { +// method = 'PUT'; +// } else { +// method = 'POST'; +// } + +// let endpointSuffix; +// if (operation === 'remove' && cardQuantity === 1) { +// endpointSuffix = 'removeCards'; +// } else { +// endpointSuffix = 'updateCards'; +// } + +// const endpoint = createApiUrl( +// `${userId}/collections/${collectionId}/${endpointSuffix}` +// ); +// console.log('CARDS BEFORE: ', collectionWithCards); +// const updatedCards = getUpdatedCards( +// collectionWithCards, +// cardUpdate, +// operation +// // collectionId +// ); +// // console.log('CARDS AFTER: ', updatedCards); +// // Call the function to get differences in cards + +// const updatedTotalPrice = calculateCollectionValue(updatedCards); +// setTotalPrice(updatedTotalPrice); + +// // const updatedTotalPrice = updatedCards.reduce( +// // (total, card) => total + card.price * card.quantity, +// // 0 +// // ); +// const updatedTotalQuantity = updatedCards?.reduce( +// (total, card) => total + card?.quantity, +// 0 +// ); +// const newCollectionPriceHistoryObject = +// createPriceHistoryObject(updatedTotalPrice); + +// let cardsResponse; +// let cardsPayload; +// if (operation === 'remove') { +// const allCardsWithIds = []; +// for (const card of updatedCards) { +// // const cardIds = updatedCards.map((card) => card.id); +// // const cardObjIds = updatedCards.map((card) => card._id); +// const cardIds = { +// id: card?.id, +// _id: card?._id, +// }; + +// allCardsWithIds?.push(cardIds); +// } +// const removeCard = allCardsWithIds?.find( +// (idPair) => idPair?.id === cardUpdate?.id +// ); +// cardsPayload = { cardIds: removeCard }; +// } else { +// const allCardsWithIds = []; +// for (const card of updatedCards) { +// // const cardIds = updatedCards.map((card) => card.id); +// // const cardObjIds = updatedCards.map((card) => card._id); +// const cardIds = { +// id: card.id, +// _id: card._id, +// }; + +// allCardsWithIds?.push(cardIds); +// } +// const removeCard = allCardsWithIds?.find( +// (idPair) => idPair?.id === cardUpdate?.id +// ); +// cardsPayload = { cards: updatedCards, cardIds: removeCard }; +// } +// console.log('CARDS PAYLOAD:', cardsPayload); + +// cardsResponse = await fetchWrapper(endpoint, method, cardsPayload); +// const { cardMessage } = cardsResponse; +// console.log('CARDS AFTER: ', cardsResponse.cards); +// const cardDifferences = constructCardDifferencesPayload( +// collectionWithCards, +// { cards: cardsResponse.cards }, +// true +// ); +// console.log('CARD DIFFERENCES:', cardDifferences); + +// const updatedChartData = getFilteredChartData( +// collectionWithCards.chartData, +// updatedTotalPrice +// ); +// const chartDataPayload = { chartData: updatedChartData }; +// const chartDataEndpoint = createApiUrl( +// `${userId}/collections/${collectionId}/updateChartData` +// ); +// const chartDataResponse = await fetchWrapper( +// chartDataEndpoint, +// 'PUT', +// chartDataPayload +// ); + +// const { chartMessage } = chartDataResponse; + +// const updatedCollection = getUpdatedCollectionData( +// selectedCollection, +// updatedTotalPrice, +// newCollectionPriceHistoryObject, +// // updatedChartData, +// updatedTotalQuantity +// // updatedCards +// ); + +// const collectionEndpoint = createApiUrl( +// `${userId}/collections/${collectionId}` +// ); +// const collectionResponse = await fetchWrapper(collectionEndpoint, 'PUT', { +// updatedCollection, +// }); + +// const { collectionMessage } = collectionResponse; + +// setTotalPrice(calculateCollectionValue(updatedCards)); +// const restructuredCollection = { +// ...collectionResponse.collectionData, +// cards: cardsResponse.cards, +// chartData: chartDataResponse.chartData, +// }; +// console.log('COLLECTION MESSAGE:', collectionMessage); +// console.log('RESTRUCTURED COLLECTION:', restructuredCollection); + +// const filteredRestructuredCollection = filterNullPriceHistoryForCollection( +// restructuredCollection +// ); + +// console.log( +// 'FILTERED RESTRUCTURED COLLECTION:', +// filteredRestructuredCollection +// ); +// return { +// filteredRestructuredCollection, +// }; +// }; diff --git a/src/context/CollectionContext/cardHelpers.jsx b/src/context/CollectionContext/cardHelpers.jsx deleted file mode 100644 index c1ba28b..0000000 --- a/src/context/CollectionContext/cardHelpers.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import moment from 'moment'; -import { useCollectionStore } from './CollectionContext'; - -export const getCollectionId = (selectedCollection, allCollections) => { - return selectedCollection?._id || allCollections[0]?._id; -}; - -export const calculatePriceDifference = (updatedPrice, selectedCollection) => { - return updatedPrice - (selectedCollection.chartData?.updatedPrice || 0); -}; - -export const createNewDataSet = (updatedPrice, selectedCollection) => { - return { - data: [ - { - xys: [ - { - label: `Update Number ${ - selectedCollection?.chartData?.datasets?.length + 1 || 1 - }`, - data: { - x: moment().format('YYYY-MM-DD HH:mm'), - y: updatedPrice, - }, - }, - ], - additionalPriceData: { - priceChanged: - calculatePriceDifference(updatedPrice, selectedCollection) !== 0, - initialPrice: selectedCollection?.totalPrice, - updatedPrice: updatedPrice, - priceDifference: calculatePriceDifference( - updatedPrice, - selectedCollection - ), - priceChange: - Math.round( - (calculatePriceDifference(updatedPrice, selectedCollection) / - (selectedCollection?.totalPrice || 1)) * - 100 - ) / 100, - }, - }, - ], - }; -}; - -// export const createUpdateInfo = ( -// updatedCards, -// updatedPrice, -// cardInfo, -// userId, -// selectedCollection, -// collectionId, -// newDataSet, -// xyData -// ) => { -// // const { updateCollectionChartData } = useCollectionStore(); -// return { -// ...cardInfo, -// name: selectedCollection?.name, -// description: selectedCollection?.description, -// cards: updatedCards, -// userId: userId, -// totalCost: updatedPrice, -// totalPrice: updatedPrice, -// xys: xyData, -// quantity: updatedCards.length, -// totalQuantity: updatedCards.reduce((acc, card) => acc + card.quantity, 0), -// chartData: updateCollectionChartData( -// updatedPrice, -// selectedCollection, -// newDataSet -// ), -// allCardPrices: updatedCards.flatMap((card) => -// Array(card.quantity).fill(card.card_prices?.[0]?.tcgplayer_price) -// ), -// _id: collectionId, -// }; -// }; diff --git a/src/context/CollectionContext/collectionUtility.jsx b/src/context/CollectionContext/collectionUtility.jsx index ed9a205..a1103fa 100644 --- a/src/context/CollectionContext/collectionUtility.jsx +++ b/src/context/CollectionContext/collectionUtility.jsx @@ -215,61 +215,6 @@ const ensureNumber = (value, defaultValue = 0) => { const findCollectionIndex = (collections, id) => collections?.findIndex((collection) => collection?._id === id) ?? -1; -// To prevent making the same type of request within 10 seconds -const lastRequestTime = { - POST: 0, - PUT: 0, - DELETE: 0, - GET: 0, - // Add other methods if needed -}; - -/** - * Checks whether a new request can be made based on the last request's timestamp. - * @param {String} method - The HTTP method for the request. - */ -const canMakeRequest = (method) => { - const currentTime = Date.now(); - // The comment mentioned 10 seconds, but the code was checking for 5 seconds. Adjust as necessary. - return currentTime - lastRequestTime[method] > 1000; // Now it's 10 seconds -}; - -/** - * Updates the last request timestamp for a given method. - * @param {String} method - The HTTP method for the request. - */ -const updateLastRequestTime = (method) => { - lastRequestTime[method] = Date.now(); -}; -/** - * Wraps fetch API calls and implements a rate limit for each HTTP method type. - * @param {String} url - The API URL to make the request to. - * @param {String} method - The HTTP method for the request. - * @param {Object} [body=null] - The request payload if needed. - * @returns {Promise} - The response from the API call. - */ -const fetchWrapper = async (url, method, body = null) => { - const options = { - method, - headers: { 'Content-Type': 'application/json' }, - ...(body && { body: JSON.stringify(body) }), - }; - - try { - const response = await fetch(url, options); - if (!response.ok) { - // We handle non-ok responses immediately - throw new Error(`API request failed with status ${response.status}`); - } - updateLastRequestTime(method); // Assumed to be a function that updates some kind of state - return await response.json(); // Directly returning the JSON response - } catch (error) { - console.error(`Fetch failed: ${error}`); - console.trace(); - throw error; // Re-throwing the error for upstream catch blocks to handle - } -}; - const getTotalCost = (selectedCollection) => { if (!selectedCollection || !Array.isArray(selectedCollection.cards)) return 0; @@ -285,11 +230,6 @@ const getTotalCost = (selectedCollection) => { return total + cardPrice * card.quantity; }, 0); }; -// const getCardQuantity = (cardId) => { -// const card = selectedCollection?.cards?.find((c) => c.id === cardId); -// return card?.quantity || 0; -// } - const defaultContextValue = { allCollections: [], allCardPrices: [], @@ -323,7 +263,6 @@ const logError = (source, action, error) => { ); }; -// Reusable validation and error logging const validateUserIdAndData = (userId, data, actionDescription) => { if (!userId) { logError( @@ -344,86 +283,8 @@ const validateUserIdAndData = (userId, data, actionDescription) => { return true; }; -const determineHttpMethod = (isCreatingNew, endpoint) => { - return isCreatingNew ? 'POST' : 'PUT'; -}; - -// Abstracted payload creation to reduce repetition -// const createPayload = (info, data, defaultXyData) => ({ -// userId: info.userId || data.userId, // Assuming this is an ObjectId string -// name: info.name || data.name || '', -// description: info.description || data.description || '', -// totalCost: info.totalCost || data.totalCost || '', -// totalPrice: info.totalPrice || data.totalPrice || 0, -// quantity: info.quantity || data.quantity || 0, -// totalQuantity: info.totalQuantity || data.totalQuantity || 0, -// previousDayTotalPrice: -// info.previousDayTotalPrice || data.previousDayTotalPrice || 0, -// dailyPriceChange: info.dailyPriceChange || data.dailyPriceChange || '', -// priceDifference: info.priceDifference || data.priceDifference || 0, -// priceChange: info.priceChange || data.priceChange || 0, -// collectionPriceHistory: Array.isArray(info.collectionPriceHistory) -// ? info.collectionPriceHistory -// : Array.isArray(data.collectionPriceHistory) -// ? data.collectionPriceHistory -// : [], -// allCardPrices: Array.isArray(info.allCardPrices) -// ? info.allCardPrices -// : Array.isArray(data.allCardPrices) -// ? data.allCardPrices -// : [], -// cards: Array.isArray(info.cards) -// ? info.cards -// : Array.isArray(data.cards) -// ? data.cards -// : [], -// currentChartDatasets: Array.isArray(info.currentChartDatasets) -// ? info.currentChartDatasets -// : Array.isArray(data.currentChartDatasets) -// ? data.currentChartDatasets -// : [], -// xys: -// defaultXyData || -// (Array.isArray(info.xys) -// ? info.xys -// : Array.isArray(data.xys) -// ? data.xys -// : []), -// chartData: { -// name: -// info.chartData?.name || -// data.chartData?.name || -// `Chart for ${info.name || data.name || 'Collection'}`, -// userId: -// info.chartData?.userId || -// data.chartData?.userId || -// info.userId || -// data.userId, -// datasets: Array.isArray(info.chartData?.datasets) -// ? info.chartData.datasets -// : Array.isArray(data.chartData?.datasets) -// ? data.chartData.datasets -// : [], -// allXYValues: Array.isArray(info.chartData?.allXYValues) -// ? info.chartData.allXYValues -// : Array.isArray(data.chartData?.allXYValues) -// ? data.chartData.allXYValues -// : [], -// xys: Array.isArray(info.chartData?.xys) -// ? info.chartData.xys -// : Array.isArray(data.chartData?.xys) -// ? data.chartData.xys -// : [], -// }, -// }); - const createPayload = (info) => { - // Merge the 'info' and 'data' objects - // const mergedData = { ...data, ...info }; - - // console.log('MERGED DATA:', mergedData); console.log('INFO:', info); - // console.log('DATA:', data); return { ...info, userId: info.userId, @@ -650,97 +511,6 @@ const calculateCollectionValue = (cards) => { }, 0); }; -// const getPriceChange = (collectionPriceHistory) => { -// if ( -// !Array.isArray(collectionPriceHistory) || -// collectionPriceHistory.length === 0 -// ) { -// console.warn('Invalid or empty price history', collectionPriceHistory); -// return 'n/a'; -// } - -// const mostRecentPrice = -// collectionPriceHistory[collectionPriceHistory.length - 1]?.num; -// const currentDate = new Date(); - -// // Get the first price from the last 24 hours -// const firstPriceFromLastDay = collectionPriceHistory -// .slice() -// .reverse() -// .find((priceHistory) => { -// const historyDate = new Date(priceHistory.timestamp); -// return currentDate - historyDate <= 24 * 60 * 60 * 1000; // less than 24 hours -// })?.num; - -// if (mostRecentPrice && firstPriceFromLastDay) { -// const priceChange = -// ((mostRecentPrice - firstPriceFromLastDay) / firstPriceFromLastDay) * 100; -// console.log( -// `Price change over the last 24 hours is: ${priceChange.toFixed(2)}%` -// ); -// return priceChange.toFixed(2); -// } else { -// console.error('Could not calculate price change due to missing data'); -// return null; -// } -// }; - -function getPriceChange(currentChartDataSets2) { - if ( - !Array.isArray(currentChartDataSets2) || - currentChartDataSets2.length === 0 - ) { - console.warn('Invalid or empty chart data sets provided'); - return []; - } - - const sortedData = currentChartDataSets2 - .filter((dataPoint) => dataPoint && dataPoint.x && dataPoint.y != null) // Filter out invalid data points - .sort((a, b) => new Date(a.x) - new Date(b.x)); - - if (sortedData.length === 0) { - console.error('No valid chart data points after filtering'); - return []; - } - - const latestDataPoint = sortedData[sortedData.length - 1]; - const latestTime = new Date(latestDataPoint.x).getTime(); - const twentyFourHoursAgo = latestTime - 24 * 60 * 60 * 1000; - - let closestIndex = -1; - let closestTimeDifference = Number.MAX_SAFE_INTEGER; - - for (let i = 0; i < sortedData.length - 1; i++) { - const time = new Date(sortedData[i].x).getTime(); - const timeDifference = Math.abs(time - twentyFourHoursAgo); - - if (timeDifference < closestTimeDifference) { - closestTimeDifference = timeDifference; - closestIndex = i; - } - } - - if (closestIndex !== -1) { - const pastPrice = sortedData[closestIndex].y; - const priceChange = latestDataPoint.y - pastPrice; - const percentageChange = ((priceChange / pastPrice) * 100).toFixed(2); - - return [ - { - startDate: sortedData[closestIndex].x, - lowPoint: pastPrice.toFixed(2), - highPoint: latestDataPoint?.y?.toFixed(2), - endDate: latestDataPoint?.x, - priceChange: priceChange.toFixed(2), - percentageChange: `${percentageChange}%`, - priceIncreased: priceChange > 0, - }, - ]; - } - - return []; -} - const getUpdatedChartData = (collection, newPrice) => { const newXYValue = { label: `Update - ${new Date().toISOString()}`, @@ -820,23 +590,22 @@ module.exports = { calculateTotalFromAllCardPrices, ensureNumber, findCollectionIndex, - fetchWrapper, + // fetchWrapper, getTotalCost, getCardPrice, removeDuplicateCollections, initialCollectionState, defaultContextValue, validateUserIdAndData, - determineHttpMethod, + // determineHttpMethod, createPayload, logError, constructPayloadWithDifferences, // getCurrentChartDataSets, - getPriceChange, getUpdatedChartData, mergeCards, updateCardInCollection, - canMakeRequest, - updateLastRequestTime, + // canMakeRequest, + // updateLastRequestTime, calculateCollectionValue, }; diff --git a/src/context/CollectionContext/helpers.jsx b/src/context/CollectionContext/helpers.jsx index ce5a9bb..0b80ef0 100644 --- a/src/context/CollectionContext/helpers.jsx +++ b/src/context/CollectionContext/helpers.jsx @@ -1,4 +1,5 @@ import moment from 'moment'; +import { createApiUrl, fetchWrapper } from '../Helpers'; export const transformPriceHistoryToXY = (collectionPriceHistory) => { return collectionPriceHistory?.map((entry) => ({ @@ -23,11 +24,25 @@ export const filterUniqueDataPoints = (dataArray) => { return Array.from(uniqueRecords.values()); }; - +/** + * Calculates the difference between the updated price and the initial price. + * @param {Number} updatedPrice - The updated price. + * @param {Object} selectedCollection - The selected collection. + * @returns {Number} The difference between the updated price and the initial price. + * @example calculatePriceDifference(100, { totalPrice: 200 }); + * calculatePriceDifference(100, { totalPrice: 100 }); + **/ export const determineCardPrice = (card, update) => { - if (update?.latestPrice?.num) return update.latestPrice.num; - if (card.price) return card.price; - return card.card_prices[0].tcgplayer_price; + let price = 0; + console.log('CARD UPDATE:', update); + if (card?.price) { + price = card?.price; + } + + if (update?.latestPrice?.num) { + price = update?.latestPrice?.num; + } + return price || card?.card_prices[0]?.tcgplayer_price; }; export const updatePriceHistory = (card, update) => { @@ -160,10 +175,10 @@ export const filterNullPriceHistory = (allCollections) => { }; export const filterNullPriceHistoryForCollection = (collection) => { - const filteredCards = collection.cards.map((card) => { + const filteredCards = collection?.cards?.map((card) => { if (card.priceHistory) { // Remove null values, duplicates with less than 24 hours difference, and entries with num = 0 - const filteredPriceHistory = card.priceHistory.filter( + const filteredPriceHistory = card?.priceHistory?.filter( (price, index, array) => { if (!price || price.num === 0) return false; // Filter out null values and num = 0 @@ -253,3 +268,212 @@ export const handleCardRemoval = (currentCards, cardToRemove) => { return currentCards.filter((card) => card.id !== cardToRemoveId); } }; + +export const constructCardDifferencesPayload = ( + oldCollection, + newCollection, + debug = false +) => { + const differences = {}; + + newCollection.cards.forEach((newCard) => { + const oldCard = + oldCollection.cards.find((card) => card.id === newCard.id) || {}; + + Object.keys(newCard).forEach((key) => { + if (newCard[key] !== oldCard[key]) { + if (!differences[newCard.id]) { + differences[newCard.id] = { old: {}, new: {} }; + } + differences[newCard.id].old[key] = oldCard[key]; + differences[newCard.id].new[key] = newCard[key]; + } + }); + }); + + if (debug && Object.keys(differences).length > 0) { + console.log('Card Differences:', differences); + } + + return differences; +}; + +export const determineMethod = (operation, cardUpdate, collection) => { + if (operation === 'remove' && cardUpdate?.quantity === 1) { + return 'DELETE'; + } else if (collection?.cards?.some((card) => card?.id === cardUpdate?.id)) { + return 'PUT'; + } else { + return 'POST'; + } +}; + +export const determineEndpointSuffix = (operation) => { + return operation === 'remove' ? 'removeCards' : 'updateCards'; +}; + +/** + * Creates cards payload. + * @param {Object} collection - The collection to update. + * @param {Object} cardUpdate - The card update object. + * @param {String} operation - The operation to perform. + * @returns {Promise} The updated collection. + */ +export const createCardsPayload = (operation, updatedCards, cardUpdate) => { + if (operation === 'remove') { + const cardIds = updatedCards + .filter((card) => card?.id !== cardUpdate?.id) + .map((card) => ({ id: card?.id, _id: card?._id })); + return { cardIds }; + } else { + const allCardsWithIds = updatedCards.map((card) => ({ + id: card?.id, + _id: card?._id, + })); + return { cards: updatedCards, cardIds: allCardsWithIds }; + } +}; + +/** + * Handles the updating of a collection chart variable data + * @param {Object} collection - The collection to update. + * @param {Object} cardUpdate - The card update object. + * @param {String} operation - The operation to perform. + * @returns {Promise} The updated collection. + */ +export const updateChartData = async ( + userId, + collectionId, + updatedChartData +) => { + const chartDataPayload = { chartData: updatedChartData }; + const chartDataEndpoint = createApiUrl( + `${userId}/collections/${collectionId}/updateChartData` + ); + return await fetchWrapper(chartDataEndpoint, 'PUT', chartDataPayload); +}; + +/** + * Handles the updating of a collection. + * @param {Object} collection - The collection to update. + * @param {Object} cardUpdate - The card update object. + * @param {String} operation - The operation to perform. + * @returns {Promise} The updated collection. + */ +export const updateCollectionDataEndpoint = async ( + userId, + collectionId, + updatedCollection +) => { + const collectionEndpoint = createApiUrl( + `${userId}/collections/${collectionId}` + ); + return await fetchWrapper(collectionEndpoint, 'PUT', { updatedCollection }); +}; + +/** + * Handles the updating of a collection. + * @param {Object} collection - The collection to update. + * @param {Object} cardUpdate - The card update object. + * @param {String} operation - The operation to perform. + * @returns {Promise} The updated collection. + */ +export const getPriceChange = (currentChartDataSets2) => { + if ( + !Array.isArray(currentChartDataSets2) || + currentChartDataSets2?.length === 0 + ) { + console.warn('Invalid or empty chart data sets provided'); + return []; + } + + const sortedData = currentChartDataSets2 + .filter((dataPoint) => dataPoint && dataPoint?.x && dataPoint?.y != null) // Filter out invalid data points + .sort((a, b) => new Date(a.x) - new Date(b.x)); + + if (sortedData?.length === 0) { + console.error('No valid chart data points after filtering'); + return []; + } + + const latestDataPoint = sortedData[sortedData.length - 1]; + const latestTime = new Date(latestDataPoint.x).getTime(); + const twentyFourHoursAgo = latestTime - 24 * 60 * 60 * 1000; + + let closestIndex = -1; + let closestTimeDifference = Number.MAX_SAFE_INTEGER; + + for (let i = 0; i < sortedData.length - 1; i++) { + const time = new Date(sortedData[i].x).getTime(); + const timeDifference = Math.abs(time - twentyFourHoursAgo); + + if (timeDifference < closestTimeDifference) { + closestTimeDifference = timeDifference; + closestIndex = i; + } + } + + if (closestIndex !== -1) { + const pastPrice = sortedData[closestIndex].y; + const priceChange = latestDataPoint.y - pastPrice; + const percentageChange = ((priceChange / pastPrice) * 100).toFixed(2); + + return [ + { + startDate: sortedData[closestIndex].x, + lowPoint: pastPrice.toFixed(2), + highPoint: latestDataPoint?.y?.toFixed(2), + endDate: latestDataPoint?.x, + priceChange: priceChange.toFixed(2), + percentageChange: `${percentageChange}%`, + priceIncreased: priceChange > 0, + }, + ]; + } + + return []; +}; + +export const getCollectionId = (selectedCollection, allCollections) => { + return selectedCollection?._id || allCollections[0]?._id; +}; + +export const calculatePriceDifference = (updatedPrice, selectedCollection) => { + return updatedPrice - (selectedCollection.chartData?.updatedPrice || 0); +}; + +export const createNewDataSet = (updatedPrice, selectedCollection) => { + return { + data: [ + { + xys: [ + { + label: `Update Number ${ + selectedCollection?.chartData?.datasets?.length + 1 || 1 + }`, + data: { + x: moment().format('YYYY-MM-DD HH:mm'), + y: updatedPrice, + }, + }, + ], + additionalPriceData: { + priceChanged: + calculatePriceDifference(updatedPrice, selectedCollection) !== 0, + initialPrice: selectedCollection?.totalPrice, + updatedPrice: updatedPrice, + priceDifference: calculatePriceDifference( + updatedPrice, + selectedCollection + ), + priceChange: + Math.round( + (calculatePriceDifference(updatedPrice, selectedCollection) / + (selectedCollection?.totalPrice || 1)) * + 100 + ) / 100, + }, + }, + ], + }; +}; diff --git a/src/context/CombinedContext/CombinedProvider.jsx b/src/context/CombinedContext/CombinedProvider.jsx index 94add7e..6ec62a4 100644 --- a/src/context/CombinedContext/CombinedProvider.jsx +++ b/src/context/CombinedContext/CombinedProvider.jsx @@ -55,6 +55,7 @@ export const CombinedProvider = ({ children }) => { updateCollection, allCollections, getNewTotalPrice, + updateCollectionState, getUpdatedCollection, } = useContext(CollectionContext); const socket = useSocketContext(); @@ -152,7 +153,6 @@ export const CombinedProvider = ({ children }) => { const originalCard = cardsWithCollectionId.find( (card) => card?.id === id ); - return { ...originalCard, priceHistory: originalCard?.priceHistory || [], @@ -170,7 +170,8 @@ export const CombinedProvider = ({ children }) => { // If latestPrice is different, update lastSavedPrice and priceHistory if (updatedCardInfo.latestPrice?.num !== originalCard.latestPrice?.num) { - console.log('ORIGINAL PRICE HISTOY', originalCard.priceHistory); + console.log('ORIGINAL CARD: ', originalCard); + console.log('UPDATED CARD: ', updatedCardInfo); return { ...originalCard, ...updatedCardInfo, @@ -200,10 +201,6 @@ export const CombinedProvider = ({ children }) => { [allCollections] ); - const emitUpdatedCards = (socket, updatedCards) => { - socket.emit('UPDATED_MONITORED_CARDS', updatedCards); - }; - const handleStatusUpdateCharts = async (newData) => { console.log('[STATUS_UPDATE_CHARTS] Data:', newData); let updatedCollection = { ...selectedCollection }; @@ -272,11 +269,12 @@ export const CombinedProvider = ({ children }) => { console.log('Existing collection data:', collectionData); setDataFunctions.collectionData(collectionData); }; - const handleCardPricesUpdated = async (priceData) => { console.log('Card prices retrieved:', priceData); const updatedCardPrices = priceData.data.data; const userId = user?.id; + + // Generate list of monitored cards from all collections const currentListOfMonitoredCards = generateListOfMonitoredCards(allCollections); console.log( @@ -285,6 +283,8 @@ export const CombinedProvider = ({ children }) => { )}] | `, currentListOfMonitoredCards ); + + // Update card prices in the list of monitored cards const updatedListOfMonitoredCards = updateCardPricesInList( currentListOfMonitoredCards, updatedCardPrices @@ -296,7 +296,7 @@ export const CombinedProvider = ({ children }) => { updatedListOfMonitoredCards ); - // Update the selectedCollection with new card prices + // Update the selected collection with new card prices const updatedSelectedCollectionCards = selectedCollection.cards.map( (card) => { const updatedCardPrice = updatedListOfMonitoredCards.find( @@ -306,40 +306,123 @@ export const CombinedProvider = ({ children }) => { } ); + // Create an updated collection object const updatedCollection = { ...selectedCollection, cards: updatedSelectedCollectionCards, }; + // Filter out collections with null price history const filteredUpdatedCollection = filterNullPriceHistoryForCollection(updatedCollection); - console.log('FILTERED UPDATED COLLECTION:', filteredUpdatedCollection); try { - const updatedCollectionResult = await getUpdatedCollection( - filteredUpdatedCollection, - null, // No specific card to update - 'update', // Operation type - userId - ); - // for (const card of filteredUpdatedCollection.cards) { - // const updatedCollectionResult = await updateCollection( - // card, - // 'update', - // selectedCollection - // ); - if (updatedCollectionResult) { - console.log('UPDATED COLLECTION RESULT:', updatedCollectionResult); - // setDataFunctions.collectionData(updatedCollectionResult); - setDataFunctions.listOfSimulatedCards(updatedCollectionResult); + // Update each card in the collection + for (const card of filteredUpdatedCollection.cards) { + const updatedCollectionResult = await getUpdatedCollection( + filteredUpdatedCollection, + card, // No specific card to update + 'update', // Operation type + userId + ); + + if (updatedCollectionResult) { + console.log( + 'UPDATED COLLECTION RESULT IN COMBINED:', + updatedCollectionResult.filteredRestructuredCollection + ); + updateCollectionState( + updatedCollectionResult.filteredRestructuredCollection + ); + setDataFunctions.listOfSimulatedCards( + updatedCollectionResult.filteredRestructuredCollection.cards + ); + } } } catch (error) { console.error('Failed to update collection:', error); } + // Update the global state with the new card prices setDataFunctions.allCardPrices(updatedListOfMonitoredCards); }; + // const handleCardPricesUpdated = async (priceData) => { + // console.log('Card prices retrieved:', priceData); + // const updatedCardPrices = priceData.data.data; + // const userId = user?.id; + // const currentListOfMonitoredCards = + // generateListOfMonitoredCards(allCollections); + // console.log( + // `[currentListOfMonitoredCards: $${getNewTotalPrice( + // currentListOfMonitoredCards + // )}] | `, + // currentListOfMonitoredCards + // ); + // const updatedListOfMonitoredCards = updateCardPricesInList( + // currentListOfMonitoredCards, + // updatedCardPrices + // ); + // console.log( + // `[updatedListOfMonitoredCards: $${getNewTotalPrice( + // updatedListOfMonitoredCards + // )}] | `, + // updatedListOfMonitoredCards + // ); + + // // Update the selectedCollection with new card prices + // const updatedSelectedCollectionCards = selectedCollection.cards.map( + // (card) => { + // const updatedCardPrice = updatedListOfMonitoredCards.find( + // (updatedCard) => updatedCard.id === card.id + // ); + // return updatedCardPrice ? { ...card, ...updatedCardPrice } : card; + // } + // ); + + // const updatedCollection = { + // ...selectedCollection, + // cards: updatedSelectedCollectionCards, + // }; + + // const filteredUpdatedCollection = + // filterNullPriceHistoryForCollection(updatedCollection); + + // try { + // for (const card of filteredUpdatedCollection.cards) { + // const updatedCollectionResult = await getUpdatedCollection( + // filteredUpdatedCollection, + // card, // No specific card to update + // 'update', // Operation type + // userId + // ); + // // for (const card of filteredUpdatedCollection.cards) { + // // const updatedCollectionResult = await updateCollection( + // // card, + // // 'update', + // // selectedCollection + // // ); + // if (updatedCollectionResult) { + // console.log( + // 'UPDATED COLLECTION RESULT:', + // updatedCollectionResult.filteredRestructuredCollection + // ); + // updateCollectionState( + // updatedCollectionResult.filteredRestructuredCollection + // ); + // // setDataFunctions.collectionData(updatedCollectionResult); + // setDataFunctions.listOfSimulatedCards( + // updatedCollectionResult.filteredRestructuredCollection.cards + // ); + // } + // } + // } catch (error) { + // console.error('Failed to update collection:', error); + // } + + // setDataFunctions.allCardPrices(updatedListOfMonitoredCards); + // }; + const handleNoPricesChanged = () => { console.log('No prices changed'); // Perform any necessary actions when no prices have changed diff --git a/src/context/CronJobContext/CronJobContext.jsx b/src/context/CronJobContext/CronJobContext.jsx index 4a6e49c..09dbfa0 100644 --- a/src/context/CronJobContext/CronJobContext.jsx +++ b/src/context/CronJobContext/CronJobContext.jsx @@ -9,6 +9,7 @@ export const useCronJobContext = () => useContext(CronJobContext); export const CronJobProvider = ({ children }) => { const { user } = useUserContext(); + const { selectedCollection } = useCollectionStore(); // Assuming this is where you get your selectedCollection const { handleSendAllCardsInCollections, listOfMonitoredCards } = useCombinedContext(); const [lastCronJobTriggerTime, setLastCronJobTriggerTime] = useState( @@ -20,23 +21,27 @@ export const CronJobProvider = ({ children }) => { const currentTime = new Date().getTime(); const timeDifference = currentTime - lastCronJobTriggerTime; - if (timeDifference >= 60000) { - // 60 seconds + // Check the conditions for triggering the cron job + if ( + timeDifference >= 120000 && + listOfMonitoredCards.length > 5 && + selectedCollection?.chartData?.allXYValues?.length > 10 + ) { setLastCronJobTriggerTime(currentTime); - - if (user?.id && listOfMonitoredCards?.length > 0) { + if (user?.id) { console.log('Triggering cron job actions'); handleSendAllCardsInCollections(user.id, listOfMonitoredCards); } } }; - const interval = setInterval(handleTriggerCronJob, 60000); // Trigger every minute + const interval = setInterval(handleTriggerCronJob, 120000); // Trigger every 2 minutes (120000 ms) return () => clearInterval(interval); }, [ lastCronJobTriggerTime, user, listOfMonitoredCards, + selectedCollection, handleSendAllCardsInCollections, ]); diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx new file mode 100644 index 0000000..0100fcc --- /dev/null +++ b/src/pages/LoginPage.jsx @@ -0,0 +1,142 @@ +import * as React from 'react'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import CssBaseline from '@mui/material/CssBaseline'; +import TextField from '@mui/material/TextField'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Link from '@mui/material/Link'; +import Paper from '@mui/material/Paper'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; +import Typography from '@mui/material/Typography'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +function Copyright(props) { + return ( + + {'Copyright © '} + + Your Website + {' '} + {new Date().getFullYear()} + {'.'} + + ); +} + +// TODO remove, this demo shouldn't need to reset the theme. + +const defaultTheme = createTheme(); + +export default function LoginPage() { + const handleSubmit = (event) => { + event.preventDefault(); + const data = new FormData(event.currentTarget); + console.log({ + email: data.get('email'), + password: data.get('password'), + }); + }; + + return ( + + + + + t.palette.mode === 'light' + ? t.palette.grey[50] + : t.palette.grey[900], + backgroundSize: 'cover', + backgroundPosition: 'center', + }} + /> + + + + + + + Sign in + + + + + } + label="Remember me" + /> + + + + + Forgot password? + + + + + {"Don't have an account? Sign Up"} + + + + + + + + + + ); +} diff --git a/src/pages/StorePage.js b/src/pages/StorePage.js index 025a2c2..3b25982 100644 --- a/src/pages/StorePage.js +++ b/src/pages/StorePage.js @@ -36,7 +36,7 @@ const StorePage = () => { setIsPageLoading(true); try { - fetchUserCart(userId); + fetchUserCart(); if (searchData && slicedAndMergedSearchData) { logPageData('StorePage', slicedAndMergedSearchData); } diff --git a/src/pages/index.js b/src/pages/index.js index a28ad10..495e68e 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -7,3 +7,4 @@ export { default as NotFoundPage } from './NotFoundPage'; export { default as CartPage } from './CartPage'; export { default as SplashPage } from './SplashPage'; export { default as StorePage } from './StorePage'; +export { default as LoginPage } from './LoginPage'; From 00b410d3c840b55b58237cefcca7ea5eba5b0e1d Mon Sep 17 00:00:00 2001 From: Reed Vogt Date: Sun, 3 Dec 2023 14:37:13 -0800 Subject: [PATCH 2/3] feat: Integrate ChartContext with CollectionStore and add async data handling - Implement StatisticsProvider to use currentChartDataSets2 from CollectionStore. - Create and utilize waitForChartDataUpdate function for async context updates. - Refactor ChartProvider to handle dynamic timeRange updates. - Optimize StatisticsContext with useMemo for efficient data processing. - Add error handling and performance considerations in async operations. - Adjust components to consume updated context values and handle state changes. --- README.md | 87 ++++- package.json | 1 + src/App.js | 196 +++++++----- src/assets/GlobalStyles.js | 2 +- src/{pages => assets}/pages.json | 0 src/assets/themeSettings.jsx | 7 + .../actionButtons/CardActionButtons.jsx | 232 ++++++++------ .../actionButtons/GenericActionButtons.jsx | 30 +- src/components/cards/GenericCard.jsx | 21 +- src/components/collection/CardPortfolio.jsx | 34 +- .../collection/PortfolioContent.jsx | 29 +- .../collection/SelectCollection.jsx | 254 ++++++++++----- .../grids/collectionGrids/CardList.jsx | 15 +- .../collectionGrids/SelectCollectionList.jsx | 227 ++++++++----- src/components/headings/navigation/TopBar.jsx | 5 +- src/components/headings/navigation/styled.js | 10 +- .../CollectionStatisticsSelector.jsx | 108 +------ src/components/other/dataDisplay/StatCard.jsx | 2 +- .../other/dataDisplay/TopCardsDisplay.jsx | 301 +++++++++++++----- src/components/reusable/LongMenu.jsx | 73 +++++ .../CollectionPortfolioChartContainer.jsx | 141 ++++++-- .../CollectionPortfolioListContainer.jsx | 19 +- src/context/AuthContext/authContext.js | 31 +- .../CollectionContext/CollectionContext.jsx | 151 +++++---- .../CollectionContext/collectionUtility.jsx | 103 ------ src/context/CollectionContext/helpers.jsx | 54 ++++ .../CombinedContext/CombinedProvider.jsx | 285 ++--------------- src/context/CombinedContext/helpers.jsx | 86 +++++ src/context/CronJobContext/CronJobContext.jsx | 5 +- src/context/PageContext/PageContext.jsx | 49 ++- .../StatisticsContext/StatisticsContext.jsx | 29 +- src/context/StatisticsContext/helpers.jsx | 3 +- src/context/UserContext/UserContext.js | 8 +- src/context/UtilityContext/UtilityContext.jsx | 105 +++--- src/index.js | 94 +++--- src/pages/CollectionPage.js | 205 ++++++++++-- src/pages/HomePage.js | 34 +- src/pages/index.js | 2 +- src/pages/{ => otherPages}/SplashPage.js | 4 +- src/pages/pageStyles/CarouselImage.jsx | 30 ++ src/pages/pageStyles/HeroCenter.jsx | 110 ++++--- src/pages/pageStyles/StyledComponents.jsx | 30 +- 42 files changed, 1972 insertions(+), 1240 deletions(-) rename src/{pages => assets}/pages.json (100%) create mode 100644 src/components/reusable/LongMenu.jsx create mode 100644 src/context/CombinedContext/helpers.jsx rename src/pages/{ => otherPages}/SplashPage.js (94%) diff --git a/README.md b/README.md index 5b962db..9396cd3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Reed Vogt ## Features +### User Features + 1. **Search Functionality** Users can search for their favorite cards by name. The search functionality returns all the cards matching the search query, helping users quickly find the desired cards to add to their decks. @@ -38,6 +40,84 @@ Reed Vogt The collection section of the app provides users a variety of statistics about their collection performance as well as specific cards which enable users to make advanced insights into trends and patterns. +### Developer Features + +1. **Backend Cronjob** + + The backend of the application is integrated with a cronjob that updates the card database every 10 minutes, ensuring that the application is always up-to-date with the latest cards. + +2. **Secure Payment Processing** + + The application is integrated with Stripe, enabling developers to leverage the secure payment processing capabilities of Stripe. + +3. **Responsive Design** + + The application is built with a responsive design, enabling developers to leverage the vast ecosystem of responsive design libraries and packages. + +4. **Secure File Conversion** + + The application is integrated with Convertio, enabling developers to leverage the secure file conversion capabilities of Convertio. + +### Frontend Technologies + +1. **React** + + The application is built with React, enabling developers to leverage the vast ecosystem of React libraries and packages. + +2. **Stripe** + + The application is integrated with Stripe, enabling developers to leverage the secure payment processing capabilities of Stripe. + +3. **mui** + + The application is built with mui, enabling developers to leverage the vast ecosystem of mui libraries and packages. + +4. **Convertio** + + The application is integrated with Convertio, enabling developers to leverage the secure file conversion capabilities of Convertio. + +5. **YGOProDeck-api** + +The application is built with YGOProDeck-api, enabling developers to leverage the vast ecosystem of YGOProDeck-api libraries and packages. + +6. **MongoDB** + + The application is built with MongoDB, enabling developers to leverage the vast ecosystem of MongoDB libraries and packages. + +7. **Netlify** + + The application is deployed with Netlify, enabling developers to leverage the vast ecosystem of Netlify libraries and packages. + +8. **Nivo Charts** + + The application is built with Nivo Charts, enabling developers to leverage the vast ecosystem of Nivo Charts libraries and packages. + +### Backend Technologies + +1. **Node.js** + + The application is built with Node.js, enabling developers to leverage the vast ecosystem of Node.js libraries and packages. + +2. **Express.js** + + The application is built with Express.js, enabling developers to leverage the vast ecosystem of Express.js libraries and packages. + +3. **MongoDB** + + The application is built with MongoDB, enabling developers to leverage the vast ecosystem of MongoDB libraries and packages. + +4. **Netlify** + + The application is deployed with Netlify, enabling developers to leverage the vast ecosystem of Netlify libraries and packages. + +5. **Heroku** + + The application is deployed with Heroku, enabling developers to leverage the vast ecosystem of Heroku libraries and packages. + +6. **Cronjob** + + The application is integrated with a cronjob that updates the card database every 24 hours, ensuring that the application is always up-to-date with the latest cards. + ## Acceptance Tests To ensure the best user experience and seamless functionality, the following acceptance tests are in place: @@ -64,7 +144,7 @@ To ensure the best user experience and seamless functionality, the following acc ## Installation -To get started with TCG eCommerce-frontEnd, follow these steps: +To get started with the enhanced card store, follow these steps: 1. Clone the repository with `git clone https://github.com/your_username_/Project-Name.git`. 2. Navigate to the project directory with `cd Project-Name`. @@ -102,4 +182,9 @@ Project Repo: - [React](https://reactjs.org/) - [Stripe](https://stripe.com/) +- [mui](https://mui.com/) - [Convertio](https://convertio.co/download/bde422f6082917756106e52b556e7245cfcfbe/) +- [YGOProDeck-api](https://ygoprodeck.com/api-guide/) +- [MongoDB](https://cloud.mongodb.com/v2#/org/641ca5d870f5f76cbe646c47/projects) +- [Netlify](https://app.netlify.com/teams/reedoooo/overview) +- [Nivo Charts](https://nivo.rocks/) diff --git a/package.json b/package.json index e4ea0cf..07a9e96 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@emotion/styled": "^11.11.0", "@fortawesome/fontawesome-free": "^6.4.0", "@mui/icons-material": "^5.14.1", + "@mui/joy": "^5.0.0-beta.16", "@mui/material": "^5.14.1", "@mui/styles": "^5.14.13", "@mui/system": "^5.14.1", diff --git a/src/App.js b/src/App.js index 45ba8b0..4f5b067 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,11 @@ // Note: Main App Component import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import { + BrowserRouter as Router, + Route, + Routes, + useNavigate, +} from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { debounce } from 'lodash'; @@ -36,44 +41,32 @@ import { usePageContext, } from './context'; import { AppContainer } from './pages/pageStyles/StyledComponents'; +import { MainContent } from './components/headings/navigation/styled'; const App = () => { const { fetchAllCollectionsForUser, selectedCollection } = useCollectionStore(); const { fetchAllDecksForUser, selectedDeck } = useDeckStore(); + const { fetchUserCart } = useCartStore(); const { user } = useUserContext(); - const { logout } = useAuthContext(); - const { isLoading, setIsLoading } = useUtilityContext(); - const { isPageLoading, setIsPageLoading } = usePageContext(); + const { logout, logoutTimerRef, resetLogoutTimer } = useAuthContext(); + const { + isPageLoading, + setIsPageLoading, + displaySplashPage, + handleLoadingTimeout, + } = usePageContext(); + const navigate = useNavigate(); + const loadingTimeoutRef = useRef(null); const userId = user?.id; const [showLoginDialog, setShowLoginDialog] = useState(!userId); - const logoutTimerRef = useRef(null); - - const handleUserActivity = debounce(() => { - if (logoutTimerRef.current) clearTimeout(logoutTimerRef.current); - logoutTimerRef.current = setTimeout(logout, 1800000); // 30 minutes - }, 500); - - const debouncedLogout = useCallback( - debounce(() => { - if (logoutTimerRef.current) { - clearTimeout(logoutTimerRef.current); - } - logoutTimerRef.current = setTimeout(logout, 1800000); // 30 minutes - }, 500), - [logout] // Dependency for useCallback - ); - - // Call this function to reset the logout timer - const resetLogoutTimer = useCallback(() => { - debouncedLogout(); - }, [debouncedLogout]); + // const [toolbarHeight, setToolbarHeight] = useState('64px'); // Default height const handleLoginSuccess = (isLoggedIn, userId) => { setShowLoginDialog(false); setIsPageLoading(false); - setIsLoading(false); + // setIsLoading(false); if (isLoggedIn && userId) { resetLogoutTimer(); } @@ -97,6 +90,7 @@ const App = () => { }, [userId, resetLogoutTimer]); useEffect(() => { + // Fetch all collections and decks for the user if (userId) { Promise.all([ fetchAllCollectionsForUser(), @@ -104,78 +98,110 @@ const App = () => { // fetchUserCart(), ]) .catch((error) => console.error('Error fetching data:', error)) - .finally(() => setIsLoading(false) && setIsPageLoading(false)); + .finally(() => setIsPageLoading(false)); } }, [userId, fetchAllCollectionsForUser, fetchAllDecksForUser, fetchUserCart]); + useEffect(() => { + // Check if loading takes more than 45 seconds + if (isPageLoading) { + loadingTimeoutRef.current = setTimeout(() => { + handleLoadingTimeout(); + navigate('/login'); + }, 45000); // 45 seconds + } + + // Clear the timeout if loading finishes or when unmounting + return () => { + if (loadingTimeoutRef.current) { + clearTimeout(loadingTimeoutRef.current); + } + }; + }, [isPageLoading, navigate, handleLoadingTimeout]); + useEffect(() => { // if the user is redirected to the login page, show the login dialog and set the loading state to false if (window.location.pathname === '/login') { setShowLoginDialog(true); - setIsLoading(false); + setIsPageLoading(false); + // setIsLoading(false); } }, []); + // useEffect(() => { + // if (toolbarRef.current) { + // setToolbarHeight(`${toolbarRef.current.clientHeight}px`); + // } + // }, []); + return ( <> - {/* Helmet Configuration */} - {isLoading || isPageLoading ? ( - + + + + + {' '} + {displaySplashPage()} + {!userId ? ( + setShowLoginDialog(false)} + onLogin={handleLoginSuccess} + /> ) : ( - <> - setShowLoginDialog(false)} - onLogin={handleLoginSuccess} - /> - -
- - } /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - {/* } /> */} - {/* } /> */} - } />{' '} - {/* 404 Not Found Route */} - - {/*