diff --git a/.eslintrc.json b/.eslintrc.json index 7473a73..cc9a03d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,12 +20,7 @@ "ecmaVersion": 2021, "sourceType": "module" }, - "plugins": [ - "react", - // "react-hooks", - "@typescript-eslint", - "prettier" - ], + "plugins": ["react", "react-hooks", "@typescript-eslint", "prettier"], "rules": { "no-console": "off", "@typescript-eslint/indent": ["error", 2], @@ -38,7 +33,7 @@ ], "react/react-in-jsx-scope": "off", "react/prop-types": "off", - // "react-hooks/rules-of-hooks": "error", + "react-hooks/rules-of-hooks": "error", // "react-hooks/exhaustive-deps": "warn", "eol-last": ["error", "always"], "object-curly-spacing": ["error", "always"], diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a3095a..e676fe3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: CI +name: CI/CD on: push: diff --git a/.gitignore b/.gitignore index 4fa3ceb..6af3306 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ package-lock.json # production /build /future-additions +/dev # misc .DS_Store diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 2f165b1..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2023 Reed Vogt - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/MIT-LICENSE.txt b/MIT-LICENSE.txt new file mode 100644 index 0000000..46737c3 --- /dev/null +++ b/MIT-LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2012-2024 Scott Chacon and others + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/__mocks__/fileMock.js b/__mocks__/fileMock.js new file mode 100644 index 0000000..86059f3 --- /dev/null +++ b/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = 'test-file-stub'; diff --git a/__mocks__/swiper/react.js b/__mocks__/swiper/react.js new file mode 100644 index 0000000..9a3f907 --- /dev/null +++ b/__mocks__/swiper/react.js @@ -0,0 +1,9 @@ +// /__mocks__/swiper/react.js +import React from 'react'; + +export const Swiper = ({ children }) => ( +
{children}
+); +export const SwiperSlide = ({ children }) => ( +
{children}
+); diff --git a/package.json b/package.json index ca1f9a0..1bb9293 100644 --- a/package.json +++ b/package.json @@ -2,39 +2,67 @@ "name": "enhanced-card-store", "version": "0.1.0", "private": true, + "keywords": [ + "react", + "stripe", + "store", + "ecommerce", + "shopping", + "cart", + "checkout", + "payment", + "subscription", + "web", + "app", + "pwa", + "progressive", + "enhanced", + "card", + "store" + ], + "author": "Reed Vogt", + "license": "MIT", + "bugs": { + "url": "https://github.com/reedvogt/enhanced-card-store/issues" + }, "dependencies": { - "@emotion/react": "^11.11.3", - "@emotion/styled": "^11.11.0", - "@floating-ui/react": "^0.26.9", - "@fortawesome/fontawesome-free": "^6.4.0", + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@floating-ui/react": "^0.26.13", + "@fortawesome/fontawesome-free": "^6.5.2", "@hookform/resolvers": "^3.3.4", - "@mui/icons-material": "^5.14.5", - "@mui/joy": "^5.0.0-beta.16", - "@mui/lab": "^5.0.0-alpha.162", - "@mui/material": "^5.15.6", - "@mui/styled-engine-sc": "^6.0.0-alpha.12", - "@mui/system": "^5.15.5", - "@mui/x-charts": "^6.18.4", - "@mui/x-data-grid": "^6.16.2", - "@mui/x-date-pickers": "^6.10.1", - "@nivo/line": "^0.83.0", - "@stripe/react-stripe-js": "^2.1.1", - "@stripe/stripe-js": "^1.54.2", - "axios": "^1.4.0", + "@mui/icons-material": "^5.15.16", + "@mui/joy": "^5.0.0-beta.36", + "@mui/lab": "^5.0.0-alpha.170", + "@mui/material": "^5.15.16", + "@mui/styled-engine-sc": "^6.0.0-alpha.18", + "@mui/system": "^5.15.15", + "@mui/x-charts": "^7.3.2", + "@mui/x-data-grid": "^7.3.2", + "@mui/x-date-pickers": "^7.3.2", + "@nivo/line": "^0.86.0", + "@stripe/react-stripe-js": "^2.7.1", + "@stripe/stripe-js": "^3.4.0", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.6", + "@testing-library/user-event": "^14.5.2", + "axios": "^1.6.8", "chroma-js": "^2.4.2", - "jwt-decode": "^3.1.2", + "dotenv": "^16.4.5", + "eslint-config-react-app": "^7.0.1", + "jwt-decode": "^4.0.0", "lodash": "^4.17.21", - "mathjs": "^12.4.1", + "mathjs": "^12.4.2", "nanoid": "^5.0.7", "notistack": "^3.0.1", "react": "^18.3.1", - "react-cookie": "^4.1.1", + "react-cookie": "^7.1.4", "react-device-detect": "^2.2.3", "react-dom": "^18.3.1", "react-error-boundary": "^4.0.13", "react-helmet-async": "^2.0.4", - "react-hook-form": "^7.50.0", - "react-icons": "^4.10.1", + "react-hook-form": "^7.51.4", + "react-icons": "^5.2.1", "react-perfect-scrollbar": "^1.5.8", "react-router-dom": "^6.23.0", "react-scroll-parallax": "^3.4.5", @@ -42,21 +70,42 @@ "react-stripe-checkout": "^2.6.3", "react-table": "^7.8.0", "react-transition-group": "^4.4.5", - "recharts": "^2.12.3", - "sass": "^1.69.7", - "stripe": "^12.14.0", - "styled-components": "^6.1.8", - "swiper": "^11.0.6", - "three": "^0.156.1", - "web-vitals": "^2.1.4", - "zod": "^3.22.4" + "recharts": "^2.12.6", + "sass": "^1.77.0", + "stripe": "^15.5.0", + "styled-components": "^6.1.9", + "swiper": "^11.1.1", + "three": "^0.164.1", + "web-vitals": "^3.5.2", + "zod": "^3.23.6" }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject", - "lint": "eslint src --ext .js,.jsx,.ts,.tsx" + "devDependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-proposal-optional-chaining": "^7.21.0", + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@babel/preset-env": "^7.24.5", + "@babel/preset-react": "^7.24.1", + "babel-jest": "^29.7.0", + "depcheck": "^1.4.7", + "eslint": "^8.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-react-hooks": "^4.6.2", + "http-proxy-middleware": "^3.0.0", + "identity-obj-proxy": "^3.0.0", + "jest": "^29.7.0", + "prettier": "^3.2.5", + "react-scripts": "5.0.1" + }, + "babel": { + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-private-property-in-object" + ] }, "eslintConfig": { "extends": [ @@ -64,6 +113,16 @@ "react-app/jest" ] }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "predeploy": "npm run build", + "deploy": "npx netlify-cli deploy --dir=build", + "lint": "eslint src --ext .js,.jsx" + }, + "config": {}, "browserslist": { "production": [ ">0.2%", @@ -75,16 +134,5 @@ "last 1 firefox version", "last 1 safari version" ] - }, - "devDependencies": { - "@babel/plugin-proposal-optional-chaining": "^7.21.0", - "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "eslint": "^8.46.0", - "eslint-config-prettier": "^8.9.0", - "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-react-hooks": "^4.6.2", - "http-proxy-middleware": "^2.0.6", - "prettier": "^3.0.0", - "react-scripts": "5.0.1" } } diff --git a/setupTests.js b/setupTests.js new file mode 100644 index 0000000..47fa4b5 --- /dev/null +++ b/setupTests.js @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom/extend-expect'; +// Directly in your test file or in src/setupTests.js diff --git a/src/App.js b/src/App.js index f804cba..10567ca 100644 --- a/src/App.js +++ b/src/App.js @@ -1,25 +1,20 @@ // App.js -import React, { Suspense, lazy, useEffect } from 'react'; -import { - Route, - Routes, - useNavigate, - BrowserRouter as Router, -} from 'react-router-dom'; +import React, { Suspense, useEffect } from 'react'; +import { Route, Routes, useNavigate } from 'react-router-dom'; import { CSSTransition, TransitionGroup } from 'react-transition-group'; import LoginDialog from 'layout/dialogs/LoginDialog'; import Navigation from 'layout/navigation'; -import { HelmetMetaData, ROUTES } from 'data'; +import { HelmetMetaData, getRoutes } from 'data'; import { Configurator, LoadingOverlay, PageLayout, } from 'layout/REUSABLE_COMPONENTS'; import { - useUserData, useConfigurator, useManageCookies, useMode, + useAuthManager, } from 'context'; import { ThemeProvider } from 'styled-components'; import { CssBaseline, GlobalStyles } from '@mui/material'; @@ -27,57 +22,63 @@ import { CssBaseline, GlobalStyles } from '@mui/material'; // ==============================|| APP ||============================== // const PrivateRoute = ({ children }) => { - const { user } = useUserData(); + const { getCookie } = useManageCookies(); + const { authUser, isLoggedIn } = getCookie(['authUser', 'isLoggedIn']); const navigate = useNavigate(); useEffect(() => { - if (user === null) { + if (authUser === null) { navigate('/login', { replace: true }); } - }, [navigate, user]); + }, [navigate, authUser]); return children; }; -const LazyRoute = ({ componentName, ...rest }) => { - const Component = lazy(() => import(`./pages/${componentName}`)); - return ; -}; const App = () => { const { getCookie } = useManageCookies(); + const { authUser, isLoggedIn } = getCookie(['authUser', 'isLoggedIn']); + const { theme } = useMode(); - const { isLoggedIn } = getCookie(['isLoggedIn']); + const { logout } = useAuthManager(); const { isConfiguratorOpen } = useConfigurator(); + const ROUTES = getRoutes(); return ( - + {isConfiguratorOpen && } - }> - - {ROUTES.map( - ({ path, component: Component, isPrivate }, index) => ( - - - - ) : ( + {/* }> */} + + {ROUTES.map( + ({ routerPath, component: Component, isPrivate }, index) => ( + }> + {}{' '} + + ) : ( + }> - ) - } - /> - ) - )} - - + + ) + } + /> + ) + )} + + {/* */} diff --git a/src/__tests__/App.test.js b/src/__tests__/App.test.js new file mode 100644 index 0000000..e5aabb4 --- /dev/null +++ b/src/__tests__/App.test.js @@ -0,0 +1,63 @@ +// App.test.js +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import App from 'App'; + +// Mocking modules and context +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => jest.fn(), +})); +jest.mock('context', () => ({ + useManageCookies: () => ({ + getCookie: jest.fn().mockReturnValue({ authUser: null, isLoggedIn: false }), + }), +})); +jest.mock('context', () => ({ + useMode: () => ({ theme: {} }), +})); +jest.mock('context', () => ({ + useAuthManager: () => ({ + logout: jest.fn(), + }), +})); +jest.mock('context', () => ({ + useConfigurator: () => ({ + isConfiguratorOpen: false, + }), +})); +jest.mock('layout/dialogs/LoginDialog', () => () =>
LoginDialog
); +jest.mock('layout/navigation', () => () =>
Navigation
); +jest.mock('layout/REUSABLE_COMPONENTS/Configurator', () => () => ( +
Configurator
+)); +jest.mock('layout/REUSABLE_COMPONENTS', () => () => ( +
LoadingOverlay
+)); +jest.mock('layout/REUSABLE_COMPONENTS', () => () => ( +
PageLayout
+)); + +// Helper to wrap component with router context +const renderWithRouter = (ui, { route = '/' } = {}) => { + window.history.pushState({}, 'Test page', route); + return render(ui, { wrapper: Router }); +}; + +describe('App Component', () => { + it('should redirect to login if not authenticated', async () => { + renderWithRouter(); + await waitFor(() => + expect(screen.getByText('LoginDialog')).toBeInTheDocument() + ); + }); + + it('should display the navigation and page layout', () => { + renderWithRouter(); + expect(screen.getByText('Navigation')).toBeInTheDocument(); + expect(screen.getByText('PageLayout')).toBeInTheDocument(); + }); + + // Add more tests as needed for different scenarios +}); diff --git a/src/assets/themes/components/card/root.jsx b/src/assets/themes/components/card/root.jsx index 72e749c..ea94481 100644 --- a/src/assets/themes/components/card/root.jsx +++ b/src/assets/themes/components/card/root.jsx @@ -16,7 +16,7 @@ export default { minWidth: 0, wordWrap: 'break-word', backgroundImage: 'none', - backgroundColor: success.main_lighter, + // backgroundColor: success.main_lighter, backgroundClip: 'border-box', border: `${borderWidth[0]} solid ${rgba(black.main, 0.125)}`, borderRadius: borderRadius.xl, diff --git a/src/context/ColorModeProvider.jsx b/src/context/ColorModeProvider.jsx index cafba2e..33b1f45 100644 --- a/src/context/ColorModeProvider.jsx +++ b/src/context/ColorModeProvider.jsx @@ -1,7 +1,7 @@ import { useState, useMemo, createContext, useEffect } from 'react'; import { createTheme } from '@mui/material'; import { themeSettings } from 'assets/themeSettings'; -import { useManageCookies } from './hooks'; +import { useManageCookies } from 'context'; export const ColorModeContext = createContext({ mode: 'dark', diff --git a/src/context/hooks/index.jsx b/src/context/hooks/index.jsx index 9460963..6a5e405 100644 --- a/src/context/hooks/index.jsx +++ b/src/context/hooks/index.jsx @@ -6,10 +6,8 @@ import useEventHandlers from './useEventHandlers'; import useFetchWrapper from './useFetchWrapper'; import useGridItems from './useGridItems'; import useLoading from './useLoading'; -import useLogger from './useLogger'; import useManageCookies from './useManageCookies'; import usePagination from './usePagination'; -import useSelectedContext from './useSelectedContext'; import usePopover from './usePopover'; import useSelectorActions from './useSelectorActions'; import useFormManagement from './useFormManagement'; @@ -25,10 +23,8 @@ export { useFetchWrapper, useGridItems, useLoading, - useLogger, useManageCookies, usePagination, - useSelectedContext, usePopover, useSelectorActions, useFormManagement, diff --git a/src/context/hooks/useBreakPoint.jsx b/src/context/hooks/useBreakPoint.jsx index 2eb1f01..fc8438c 100644 --- a/src/context/hooks/useBreakPoint.jsx +++ b/src/context/hooks/useBreakPoint.jsx @@ -1,4 +1,3 @@ -import { useTheme } from '@mui/material/styles'; import useMediaQuery from '@mui/material/useMediaQuery'; import { useMode } from 'context'; diff --git a/src/context/hooks/useDialogState.jsx b/src/context/hooks/useDialogState.jsx index 6cf6a18..60fd074 100644 --- a/src/context/hooks/useDialogState.jsx +++ b/src/context/hooks/useDialogState.jsx @@ -1,4 +1,3 @@ -// hooks/useDialogState.js import { useState, useCallback } from 'react'; const useDialogState = () => { diff --git a/src/context/hooks/useFetchWrapper.jsx b/src/context/hooks/useFetchWrapper.jsx index 02bd686..ec4e2b8 100644 --- a/src/context/hooks/useFetchWrapper.jsx +++ b/src/context/hooks/useFetchWrapper.jsx @@ -1,5 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; -import useLogger from './useLogger'; +import { useState, useCallback } from 'react'; import useLocalStorage from './useLocalStorage'; import useLoading from './useLoading'; import { useSnackbar } from 'notistack'; diff --git a/src/context/hooks/useFormManagement.jsx b/src/context/hooks/useFormManagement.jsx index d996e3a..ff4e2cf 100644 --- a/src/context/hooks/useFormManagement.jsx +++ b/src/context/hooks/useFormManagement.jsx @@ -1,4 +1,5 @@ import { useState, useCallback } from 'react'; + const useFormManagement = (initialFormKey, formSchemas) => { const [activeForm, setActiveForm] = useState(initialFormKey); diff --git a/src/context/hooks/useFormSubmission.jsx b/src/context/hooks/useFormSubmission.jsx index 62261f2..8997f58 100644 --- a/src/context/hooks/useFormSubmission.jsx +++ b/src/context/hooks/useFormSubmission.jsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react'; -import { handleValidation, zodSchemas } from 'data'; +import { handleSchemaValidation, zodSchemas } from 'data'; const useFormSubmission = (formHandlers, formKey) => { const [isSubmitting, setIsSubmitting] = useState(false); @@ -9,7 +9,7 @@ const useFormSubmission = (formHandlers, formKey) => { setIsSubmitting(true); console.log('[ACTIVE FORM]', formKey); console.log('[FORM DATA]', formData); - const validationResult = handleValidation(activeSchema, formData); + const validationResult = handleSchemaValidation(activeSchema, formData); if (!validationResult.success) { console.error('[INVALID RESULT]', validationResult); setIsSubmitting(false); diff --git a/src/context/hooks/useGridItems.jsx b/src/context/hooks/useGridItems.jsx index a196675..d6d9784 100644 --- a/src/context/hooks/useGridItems.jsx +++ b/src/context/hooks/useGridItems.jsx @@ -1,9 +1,12 @@ import React, { useMemo } from 'react'; import { Grid, Grow, IconButton, Tooltip } from '@mui/material'; -import GenericCard from 'layout/cards/GenericCard'; import HighlightOffRoundedIcon from '@mui/icons-material/HighlightOffRounded'; -import { useCardStore, useManager, useMode } from 'context'; + import { MDBox, SkeletonCard } from 'layout/REUSABLE_COMPONENTS'; +import GenericCard from 'layout/cards/GenericCard'; + +import { useCardStore, useManager, useMode } from 'context'; + const useGridItems = ({ itemsPerPage, cards, diff --git a/src/context/hooks/useLocalStorage.jsx b/src/context/hooks/useLocalStorage.jsx index 865582d..8fc4654 100644 --- a/src/context/hooks/useLocalStorage.jsx +++ b/src/context/hooks/useLocalStorage.jsx @@ -72,10 +72,6 @@ function useLocalStorage(key, initialValue) { url: window.location.href, }); window.dispatchEvent(storageEvent); - // Optionally, for cross-tab communication, you might want to trigger a storage event manually - // window.dispatchEvent( - // new Event('storage', { key, newValue: JSON.stringify(valueToStore) }) - // ); } catch (error) { console.error(`Error setting localStorage key "${key}":`, error); } diff --git a/src/context/hooks/useLogger.jsx b/src/context/hooks/useLogger.jsx deleted file mode 100644 index cf09cb1..0000000 --- a/src/context/hooks/useLogger.jsx +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; - -const useLogger = (componentName, options = {}) => { - const mountedRef = useRef(false); - const [state, setState] = useState({}); - const specialEvents = ['apiResponse', 'stateChange']; - - // Function to log custom events - const logEvent = (eventName, additionalData = {}) => { - // Check if the event is special - if (specialEvents.includes(eventName)) { - // Handle special event - if (eventName === 'apiResponse') { - const { message, data, source } = additionalData; - console.log( - `[${componentName}] API Response:`, - `Message: ${message}`, - `Data: ${data}`, - `Source: ${source || 'Unknown'}` - ); - } - // Add additional special event handling here if needed - } else { - // Log general events - console.log( - `[${componentName}] Event: ${eventName}`, - // field ? `[${field}]` : '', - additionalData - ); - } - }; - - // Function to log errors - const logError = (error) => { - console.error(`[${componentName}] Error:`, error); - }; - - useEffect(() => { - // console.log(`[${componentName}] Mounted`, options); - mountedRef.current = true; - - return () => { - // console.log(`[${componentName}] Unmounted`, options); - mountedRef.current = false; - }; - }, []); - - useEffect(() => { - if (mountedRef.current) { - // console.log(`[${componentName}] Updated`, state); - } - }, [state]); - - const setStateAndLog = (newState) => { - setState(newState); - logEvent('State Change', newState); - }; - - return { logEvent, setStateAndLog, logError }; -}; - -export default useLogger; diff --git a/src/context/hooks/useManageCookies.jsx b/src/context/hooks/useManageCookies.jsx index bea34f7..a954a38 100644 --- a/src/context/hooks/useManageCookies.jsx +++ b/src/context/hooks/useManageCookies.jsx @@ -35,15 +35,10 @@ function useManageCookies() { return acc; }, {}); } else { - // Handling a single cookie name - // return { - // [nameOrNames]: parseCookieValue(cookies[nameOrNames]), - // }; return parseCookieValue(cookies[nameOrNames]); } }; - // Function to remove a cookie const deleteCookie = (name) => { removeCookie(name); }; diff --git a/src/context/hooks/useRCFormHook.jsx b/src/context/hooks/useRCFormHook.jsx index dda478f..efed4ef 100644 --- a/src/context/hooks/useRCFormHook.jsx +++ b/src/context/hooks/useRCFormHook.jsx @@ -1,9 +1,8 @@ import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; -import { useEffect } from 'react'; -import { formFields, zodSchemas } from 'data'; +import { zodSchemas } from 'data'; -const useRCFormHook = (schemaKey, initialData) => { +const useRCFormHook = (schemaKey) => { const schema = zodSchemas[schemaKey]; const defaultValues = Object.keys(schema.shape).reduce((acc, key) => { const fieldDefinition = schema.shape[key]; @@ -19,17 +18,9 @@ const useRCFormHook = (schemaKey, initialData) => { const methods = useForm({ resolver: zodResolver(schema), - defaultValues: !initialData ? defaultValues : null, + defaultValues, }); - // useEffect(() => { - // console.log( - // // `[1] ${schemaKey} SCHEMA REGISTERED: ${schema}`, - // // `[2] FORMSTATE REGISTERED: ${JSON.stringify(methods.formState)}`, - // `[3] VALUES: ${JSON.stringify(methods.getValues())}` - // ); - // }, [methods.formState]); - return methods; }; diff --git a/src/context/hooks/useSelectedContext.jsx b/src/context/hooks/useSelectedContext.jsx deleted file mode 100644 index 45f9c9f..0000000 --- a/src/context/hooks/useSelectedContext.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; - -const useSelectedContext = () => { - const [selectedContext, setSelectedContext] = useState(null); - const [isContextSelected, setIsContextSelected] = useState(false); - - 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/useSelectorActions.jsx b/src/context/hooks/useSelectorActions.jsx index ea9b32c..a630df3 100644 --- a/src/context/hooks/useSelectorActions.jsx +++ b/src/context/hooks/useSelectorActions.jsx @@ -1,8 +1,8 @@ /* eslint-disable no-case-declarations */ import { useState } from 'react'; -import useLocalStorage from './useLocalStorage'; -import useManager from '../state/useManager'; import { nanoid } from 'nanoid'; +import useLocalStorage from './useLocalStorage'; +import { useManager } from 'context'; function useSelectorActions() { const { @@ -11,6 +11,7 @@ function useSelectorActions() { handleSelectCollection, selectedDeck, handleSelectDeck, + fetchDeckById, } = useManager(); const [time, setTime] = useState('24hr'); const [stat, setStat] = useState('highpoint'); @@ -25,21 +26,18 @@ function useSelectorActions() { light: 'light', dark: 'dark', }; - const handleSelectChange = (e, selectorName, context) => { - console.log( - 'SELECTOR VALUES CHANGED', - e.target.value, - selectorName, - context - ); + const handleSelectChange = async (e, selectorName, context) => { + console.log('SELECTOR VALUES CHANGED', e, selectorName, context); const selectedCollectionId = localStorage.getItem('selectedCollectionId'); const selectedDeckId = localStorage.getItem('selectedDeckId'); const selected = localStorage.getItem('selected' + context); const selectedCollection = JSON.parse(selected); + const selectedDeck = JSON.parse(selected); + // setTags(selectedDeck.tags); switch (selectorName) { case 'timeRange': setTime(e.target.value); - updateEntityField( + await updateEntityField( 'collections', selectedCollectionId, ['selectedChartDataKey', 'selectedChartData'], @@ -57,7 +55,7 @@ function useSelectorActions() { break; case 'statRange': setStat(e.target.value); - updateEntityField( + await updateEntityField( 'collections', selectedCollectionId, ['selectedStatDataKey', 'selectedStatData'], @@ -78,7 +76,7 @@ function useSelectorActions() { break; case 'themeRange': setTheme(e.target.value); - updateEntityField( + await updateEntityField( 'collections', selectedCollectionId, ['selectedThemeDataKey', 'selectedThemeData'], @@ -94,22 +92,30 @@ function useSelectorActions() { handleSelectCollection(selectedCollection); break; case 'tags': - const newTag = { id: nanoid(), label: e.target.value }; - const updatedTags = [...tags, newTag]; - console.log('NEW TAG', updatedTags); - const { name, description, color } = selected; - // selectedDeck.tags = updatedTags; - setTags(updatedTags); // Update local state - updateEntityField( + let updatedTags = [...selectedDeck.tags]; // Copy current tags to manipulate + const { name, description, color } = selectedDeck; + + if (e.type === 'add') { + const newTag = { id: nanoid(), label: e.target.value }; + if (!updatedTags.some((tag) => tag.label === newTag.label)) { + updatedTags.push(newTag); // Add new tag if not present + } + } else if (e.type === 'delete') { + updatedTags = updatedTags.filter( + (tag) => tag.id !== e.target.value.id + ); + } + setTags(updatedTags); + await updateEntityField( 'decks', selectedDeckId, ['tags', 'name', 'description', 'color'], [updatedTags, name, description, color] - ); // Persist tags update - - handleSelectDeck(selectedDeck); + ); - e.target.value = ''; // Clear the input after adding a tag + const updatedDeck = await fetchDeckById(selectedDeckId); + console.log('UPDATED DECK', updatedDeck); + handleSelectDeck(updatedDeck); break; case 'deck': default: diff --git a/src/context/index.js b/src/context/index.js index 9c9b628..8962e9f 100644 --- a/src/context/index.js +++ b/src/context/index.js @@ -14,10 +14,8 @@ import { useFetchWrapper, useGridItems, useLoading, - useLogger, useManageCookies, usePagination, - useSelectedContext, usePopover, useSelectorActions, useFormManagement, @@ -41,10 +39,8 @@ export { useFetchWrapper, useGridItems, useLoading, - useLogger, useManageCookies, usePagination, - useSelectedContext, usePopover, useSelectorActions, useFormManagement, diff --git a/src/context/state/useAuthManager.jsx b/src/context/state/useAuthManager.jsx index 90d3afc..6d9ef67 100644 --- a/src/context/state/useAuthManager.jsx +++ b/src/context/state/useAuthManager.jsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import jwt_decode from 'jwt-decode'; +import { jwtDecode as jwt_decode } from 'jwt-decode'; import { useFetchWrapper, useManageCookies, useUserData } from 'context'; function useAuthManager() { @@ -12,7 +12,7 @@ function useAuthManager() { 'accessToken', 'refreshToken', ]); - const { handleSetUser, handleRemoveUser, user } = useUserData(); + const { handleSetUser, handleRemoveUser, hasFetchedUser } = useUserData(); const { fetchWrapper } = useFetchWrapper(); const [loggingOut, setLoggingOut] = useState(false); const setAuthCookies = useCallback( @@ -24,12 +24,11 @@ function useAuthManager() { [accessToken, refreshToken, true, authData, authData.userId], { path: '/' } ); - handleSetUser(authData); // Adjust according to your implementation + // handleSetUser(authData); navigate('/home'); }, - [handleSetUser, navigate] + [addCookies, handleSetUser, navigate] ); - const clearAuthCookies = useCallback(() => { deleteCookies([ 'accessToken', @@ -38,21 +37,15 @@ function useAuthManager() { 'authUser', 'isLoggedIn', ]); - localStorage.clear(); // Clear all local storage data - navigate('/login'); - }, [navigate, deleteCookies]); - - const decodeAndSetUser = useCallback( - (accessToken) => { - const decoded = jwt_decode(accessToken); - handleSetUser(decoded); // Adjust according to your implementation - }, - [handleSetUser] - ); + localStorage.clear(); + }, [deleteCookies, navigate]); const executeAuthAction = useCallback( async (endpoint, requestData) => { try { + console.log( + `SERVER: ${process.env.REACT_APP_SERVER}/api/users/${endpoint}` + ); const responseData = await fetchWrapper( `${process.env.REACT_APP_SERVER}/api/users/${endpoint}`, 'POST', @@ -65,8 +58,8 @@ function useAuthManager() { } else { console.log(responseData?.data); setAuthCookies(responseData.data); + handleSetUser(jwt_decode(responseData.data.accessToken)); } - setAuthCookies(responseData.data); } catch (error) { console.error('Auth action error:', error); } @@ -93,7 +86,6 @@ function useAuthManager() { }, [executeAuthAction] ); - const logout = useCallback(async () => { await executeAuthAction('signout', { userId: authUser.userId, @@ -101,36 +93,40 @@ function useAuthManager() { refreshToken: refreshToken, }); clearAuthCookies(); - }, []); + setLoggingOut(true); + handleRemoveUser(); + navigate('/login'); + }, [executeAuthAction, clearAuthCookies, handleRemoveUser, navigate]); + + const checkTokenValidity = useCallback(async () => { + if (!accessToken) return; - useEffect(() => { - if (!isLoggedIn || !accessToken) return; - if (accessToken) { - decodeAndSetUser(accessToken); - } - }, []); - const checkTokenValidity = useCallback(() => { console.log('Checking token validity...', { accessToken }); - if (!accessToken) { - navigate('/login'); - return; - } try { const { exp } = jwt_decode(accessToken); const isTokenExpired = Date.now() >= exp * 1000; if (isTokenExpired) { + console.log('Token is expired'); logout(); - setLoggingOut(true); - handleRemoveUser(); + return; } } catch (error) { console.error('Token validation error:', error); } - }, [user.accessToken]); + }, [accessToken, logout]); + useEffect(() => { if (!isLoggedIn) return; - checkTokenValidity(); - }, [checkTokenValidity]); + + // if (accessToken) { + // handleSetUser(jwt_decode(accessToken)); + // } + const intervalId = setInterval(() => { + checkTokenValidity(); + }, 300000); // 300000 ms = 5 minutes + + return () => clearInterval(intervalId); // Clean up the interval on component unmount + }, []); return { login, logout, signup }; } diff --git a/src/context/state/useCardStore.jsx b/src/context/state/useCardStore.jsx index c95c804..28c9141 100644 --- a/src/context/state/useCardStore.jsx +++ b/src/context/state/useCardStore.jsx @@ -1,9 +1,10 @@ import { useState, useEffect, useCallback } from 'react'; -import axios from 'axios'; -import useLogger from '../hooks/useLogger'; -import useLocalStorage from '../hooks/useLocalStorage'; // Ensure this is the correct path to your hook -import useLoading from '../hooks/useLoading'; -import useManageCookies from '../hooks/useManageCookies'; +import { + useFetchWrapper, + useLoading, + useLocalStorage, + useManageCookies, +} from 'context'; function debounce(func, wait) { let timeout; @@ -19,8 +20,8 @@ function debounce(func, wait) { const useCardStore = () => { const { getCookie } = useManageCookies(); + const { fetchWrapper, status } = useFetchWrapper(); const { userId } = getCookie(['userId']); - const logger = useLogger('CardProvider'); const [previousSearchData, setPreviousSearchData] = useLocalStorage( 'previousSearchData', [] @@ -44,34 +45,37 @@ const useCardStore = () => { const clearSearchData = () => { setSearchData([]); setIsDataValid(false); - logger.logEvent('Search Data Cleared'); + console.log('Search Data Cleared'); }; const handleRequest = useCallback( debounce(async (searchParams) => { startLoading('isSearchLoading'); try { - logger.logEvent('handleRequest start', { searchParams, userId }); + console.log('handleRequest start', { searchParams, userId }); const requestBody = { searchParams, user: userId, searchTerm: searchParams, }; - const response = await axios.post( + const response = await fetchWrapper( `${process.env.REACT_APP_SERVER}/api/cards/ygopro`, - requestBody + 'POST', + requestBody, + 'FETCH_SEARCH_DATA' ); - if (response?.data?.data?.length > 0) { - logger.logEvent('Data Fetched Successfully', { - dataLength: response?.data.data.length, + console.log('handleRequest response', response); + if (response?.data?.length > 0) { + console.log('Data Fetched Successfully', { + dataLength: response?.data.length, }); - const limitedData = response?.data.data.slice(0, 30); // Limit to 30 cards + const limitedData = response?.data.slice(0, 30); // Limit to 30 cards setIsDataValid(true); setSearchData(limitedData); // Directly set the new searchData } else { clearSearchData(); } } catch (err) { - logger.logEvent('Error fetching card data', err); + console.log('Error fetching card data', err); clearSearchData(); } finally { stopLoading('isSearchLoading'); // Set loading to false once the request is complete @@ -79,22 +83,28 @@ const useCardStore = () => { }, 100), [] ); - async function fetchRandomCardsAndSet() { + const fetchRandomCardsAndSet = useCallback(async () => { startLoading('fetchRandomCardsAndSet'); try { - const response = await fetch( - `${process.env.REACT_APP_SERVER}/api/cards/randomCardData` + // const response = await fetch( + // `${process.env.REACT_APP_SERVER}/api/cards/randomCardData` + // ); + const response = await fetchWrapper( + `${process.env.REACT_APP_SERVER}/api/cards/randomCardData`, + 'GET', + null, + 'FETCH_RANDOM_CARDS' ); - if (!response.ok) { - throw new Error('Network response was not ok'); - } - const cards = await response.json(); - setRandomCards(cards); + // if (!response.ok) { + // throw new Error('Network response was not ok'); + // } + // const cards = await response.json(); + setRandomCards(response); } catch (error) { console.error('Failed to fetch random cards:', error); startLoading('fetchRandomCardsAndSet'); } - } + }, []); return { searchData, searchSettings, diff --git a/src/context/state/useManager.jsx b/src/context/state/useManager.jsx index 357367d..bf5bb2c 100644 --- a/src/context/state/useManager.jsx +++ b/src/context/state/useManager.jsx @@ -1,53 +1,46 @@ /* eslint-disable no-case-declarations */ import { useCallback, useMemo, useState } from 'react'; -import { - useFetchWrapper, - useLocalStorage, - useLogger, - useManageCookies, - useSelectedContext, -} from '../hooks'; - +import { useFetchWrapper, useLocalStorage, useManageCookies } from 'context'; +// const useCleanId = (id) => encodeURIComponent(id.replace(/"/g, '')); const useManager = () => { const { fetchWrapper, status } = useFetchWrapper(); const { getCookie } = useManageCookies(); const { isLoggedIn, userId } = getCookie(['isLoggedIn', 'userId']); - const logger = useLogger('CollectionManager'); const [customError, setCustomError] = useState(''); const baseUrl = `${process.env.REACT_APP_SERVER}/api/users/${userId}`; - const { selectedContext } = useSelectedContext(); const createApiUrl = (entity, action) => `${baseUrl}/${entity}/${action}`; + const cleanId = (id) => encodeURIComponent(id.replace(/"/g, '')); - // DECKS + // State management for all entities using useState const [decks, setDecks] = useState([]); - const [selectedDeck, setSelectedDeck] = useLocalStorage('selectedDeck', {}); - const [selectedDeckId, setSelectedDeckId] = useLocalStorage( - 'selectedDeckId', - null - ); - const [hasFetchedDecks, setHasFetchedDecks] = useState(false); - const [hasUpdatedDecks, setHasUpdatedDecks] = useState(false); - const [hasUpdatedSelectedDeck, setHasUpdatedSelectedDeck] = useState(false); - - // COLLECTIONS const [collections, setCollections] = useState([]); + const [cart, setCart] = useState([]); + + // State management for selected entities using useLocalStorage + const [selectedDeck, setSelectedDeck] = useLocalStorage('selectedDeck', {}); const [selectedCollection, setSelectedCollection] = useLocalStorage( 'selectedCollection', {} ); + const [selectedDeckId, setSelectedDeckId] = useLocalStorage( + 'selectedDeckId', + null + ); const [selectedCollectionId, setSelectedCollectionId] = useLocalStorage( 'selectedCollectionId', null ); + // State management for operational flags + const [hasFetchedDecks, setHasFetchedDecks] = useState(false); const [hasFetchedCollections, setHasFetchedCollections] = useState(false); - - // CART - const [cart, setCart] = useLocalStorage('cart', null); const [hasFetchedCart, setHasFetchedCart] = useState(false); - - // CARDS const [hasFetchedCards, setHasFetchedCards] = useState(false); + + const [hasUpdatedDecks, setHasUpdatedDecks] = useState(false); + const [hasUpdatedCollections, setHasUpdatedCollections] = useState(false); + const [hasUpdatedSelectedDeck, setHasUpdatedSelectedDeck] = useState(false); const [hasUpdatedCards, setHasUpdatedCards] = useState(false); + const [hasFetched, setHasFetched] = useLocalStorage('hasFetched', { initFetch: false, cards: false, @@ -65,10 +58,6 @@ const useManager = () => { 'collectionMetaData', [] ); - const [cardsWithQuantities, setCardsWithQuantities] = useLocalStorage( - 'cardsWithQuantities', - {} - ); const compileCollectionMetaData = useCallback( (allCollections) => { if (!allCollections || allCollections.length === 0) return; @@ -115,66 +104,6 @@ const useManager = () => { }, [collections, setCollectionMetaData] ); - const compileCardsWithQuantities = useCallback( - (col, dec, car) => { - if (!col && !dec && !car) return []; - const cards = []; - if (!dec !== null) { - dec?.forEach((d) => { - if (!d?.cards) return; - cards.push(...(d?.cards || [])); - }); - } - if (col !== null) { - col?.forEach((c) => { - if (!c?.cards) return; - cards.push(...(c?.cards || [])); - }); - } - if (car !== null) { - if (!car?.items) return; - cards.push(...(car?.items || [])); - } - // MAP CARDS BY ID AND SET VALUE AS STRING --> '[card.variant.modelName][card.name]: card quantity' - const cardsWithQuantities = cards?.reduce((acc, card) => { - if (!card.id) return acc; - if (!acc[card.id]) { - acc[card.id] = { - CardInCart: '', - CardInCollection: '', - CardInDeck: '', - }; - } - const modelKey = card?.variant?.cardModel; - if (modelKey && !acc[card.id][modelKey]) { - acc[card.id][modelKey] = `${card.name}: ${card.quantity}`; - } else { - // Handle updating quantities if card already exists in the accumulator - const currentQty = parseInt( - acc[card.id][modelKey].split(': ')[1], - 10 - ); - acc[card.id][modelKey] = - `${card.name}: ${currentQty + card.quantity}`; - } - return acc; - }, {}); - setCardsWithQuantities((state) => ({ ...state, ...cardsWithQuantities })); - return cards; - }, - [setCardsWithQuantities] - ); - const isCardInContext = useCallback( - (card) => { - const cardsList = { - Collection: selectedCollection?.cards, - Deck: selectedDeck?.cards, - Cart: cart?.items, - }; - return !!cardsList[selectedContext]?.find((c) => c?.id === card?.id); - }, - [selectedContext, selectedCollection, selectedDeck, cart] - ); // FETCHING ENTITIES const handleSelectEntity = useCallback( (entityName, entityData) => { @@ -200,34 +129,30 @@ const useManager = () => { const fetchEntities = useCallback( async (entity) => { const loadingID = `FETCH_ALL_${entity.toUpperCase()}`; - console.log('LOADING ID:', loadingID); + const url = createApiUrl(entity, 'all'); try { - const response = await fetchWrapper( - createApiUrl(entity, 'all'), - 'GET', - null, - loadingID - ); - if (entity === 'cart') { - setCart(response.data); - setHasFetchedCart(true); - setHasFetched((state) => ({ ...state, cart: true })); - compileCardsWithQuantities(null, null, response.data); - return response.data; - } else if (entity === 'collections') { - setCollections(response.data); - compileCollectionMetaData(response.data); - setHasFetchedCollections(true); - setHasFetched((state) => ({ ...state, collections: true })); - compileCardsWithQuantities(response.data, null, null); - return response.data; - } else { - setDecks(response.data); - setHasFetchedDecks(true); - setHasFetched((state) => ({ ...state, decks: true })); - compileCardsWithQuantities(null, response.data, null); - return response.data; + const response = await fetchWrapper(url, 'GET', null, loadingID); + switch (entity) { + case 'decks': + setDecks(response.data); + setHasFetched((state) => ({ ...state, decks: true })); + setHasFetchedDecks(true); + break; + case 'collections': + setCollections(response.data); + compileCollectionMetaData(response.data); + setHasFetched((state) => ({ ...state, collections: true })); + setHasFetchedCollections(true); + break; + case 'cart': + setCart(response.data); + setHasFetched((state) => ({ ...state, cart: true })); + setHasFetchedCart(true); + break; + default: + console.error(`Unhandled entity type: ${entity}`); } + return response.data; } catch (error) { console.error('ERROR FETCHING ENTITIES', error); setCustomError('Failed to fetch data'); @@ -235,7 +160,6 @@ const useManager = () => { }, [ fetchWrapper, - logger, setCollections, setDecks, setHasFetchedCollections, @@ -244,43 +168,30 @@ const useManager = () => { setHasFetched, setCart, compileCollectionMetaData, - compileCardsWithQuantities, ] ); const fetchSingleEntity = useCallback( async (entity, id) => { - try { - const response = await fetchWrapper( - createApiUrl(entity, `get/${id}`), - 'GET', - null, - `fetch${entity}`.toLocaleUpperCase() - ); - handleSelectEntity(entity, response.data); - return response.data; - } catch (error) { - logger.logError('Fetch Error:', error); - setCustomError('Failed to fetch data'); - } + const response = await fetchWrapper( + createApiUrl(entity, `get/${cleanId(id)}`), + 'GET', + null, + `fetch${entity}`.toLocaleUpperCase() + ); + handleSelectEntity(entity, response.data); + return response.data; }, - [fetchWrapper, logger] + [fetchWrapper, handleSelectEntity] ); // UPDATING ENTITIES const handleEntityOperation = useCallback( - async (entity, endpoint, action, data) => { + async (entity, endpoint, action, data, method, idValue) => { if (!isLoggedIn) { setCustomError('User is not logged in'); return; } const loadingID = `${action}${entity}`.toUpperCase(); - console.log(loadingID); const url = createApiUrl(entity, endpoint); - const method = endpoint.includes('delete') - ? 'DELETE' - : endpoint.includes('create') || endpoint.includes('add') - ? 'POST' - : 'PUT'; - try { const response = await fetchWrapper(url, method, data, loadingID); if (response && response.data) { @@ -293,14 +204,19 @@ const useManager = () => { ); break; case 'decks': - const prevDecks = [...decks]; - const updatedDecks = prevDecks.map((deck) => - deck._id === response?.data?.data?._id - ? response?.data?.data - : deck + // const prevDecks = [...decks]; + // const updatedDecks = prevDecks.map((deck) => + // deck._id === response?.data?.data?._id + // ? response?.data?.data + // : deck + // ); + setDecks((prev) => + prev.map((deck) => + deck._id === idValue ? { ...deck, ...response.data } : deck + ) ); - setDecks(updatedDecks); - handleSelectEntity('deck', response.data.data); + // setDecks(updatedDecks); + handleSelectEntity('deck', response.data); setHasUpdatedCards(true); setHasUpdatedDecks(true); break; @@ -313,91 +229,49 @@ const useManager = () => { } fetchEntities(entity); } catch (error) { - logger.logError(`Error performing ${endpoint} on ${entity}:`, error); + console.log(`Error performing ${endpoint} on ${entity}:`, error); setCustomError(`Failed to ${endpoint} ${entity}`); } }, [ isLoggedIn, fetchWrapper, - logger, setCart, setCollections, setDecks, setHasUpdatedCards, ] ); - /** - * Updates the fields of an entity with the specified values. - * - * @param {string} entity - The type of entity to update. - * @param {string} id - The ID of the entity to update. - * @param {string|string[]} fields - The field(s) to update. - * @param {any|any[]} values - The value(s) to set for the field(s). - * @returns {Promise} - A promise that resolves when the update is complete. - * @example updateEntityFields('collections', '6158888888888', ['name', 'description'], ['New Collection Name', 'New Collection Description']); - */ const updateEntityField = useCallback( async (entity, id, fields, values) => { if (!isLoggedIn) { setCustomError('User is not logged in'); return; } - const cleanedId = encodeURIComponent(id.replace(/"/g, '')); - const url = createApiUrl(entity, `update/${cleanedId}`); - let data; - // Handle updating multiple fields - if (Array.isArray(fields)) { - data = fields.reduce((acc, field, index) => { - acc[field] = values[index]; - return acc; - }, {}); + // Prepare data object from fields and values + let data = {}; + if (Array.isArray(fields) && Array.isArray(values)) { + fields.forEach((field, index) => { + data[field] = values[index]; + }); } else { - // If fields is not an array, treat it as a single field with a single value - data = { [fields]: values }; + data[fields] = values; } - try { - const response = await fetchWrapper( - url, - 'PUT', - data, - `update${entity}` - ); - if (response && response.data) { - // Assuming the server returns the updated entity - switch (entity) { - case 'collections': - const selectedId = localStorage.getItem('selectedCollectionId'); - setCollections(response.data); - break; - case 'decks': - setDecks((prev) => - prev.map((deck) => - deck._id === id ? { ...deck, ...response.data } : deck - ) - ); - break; - case 'cart': - if (id === cart._id) { - setCart({ ...cart, ...response.data }); - } - break; - default: - throw new Error(`Unhandled entity type: ${entity}`); - } - } - // return response.data; - } catch (error) { - console.error(`Error updating ${entity} fields:`, error); - setCustomError(`Failed to update ${entity} fields`); - } + // Construct endpoint and action + const endpoint = `update/${cleanId(id)}`; + const action = `update_${entity}`; + + // Use handleEntityOperation to perform the update + handleEntityOperation(entity, endpoint, action, data, 'PUT', id); }, - [isLoggedIn, fetchWrapper, logger, setCart, setCollections, setDecks, cart] + [handleEntityOperation, isLoggedIn, setCustomError] ); const addEntity = useCallback( - (entity, data) => handleEntityOperation(entity, 'create', 'add_', data), - [handleEntityOperation] + (entity, data) => + handleEntityOperation(entity, 'create', 'add_', data, 'POST')[ + handleEntityOperation + ] ); const updateEntity = useCallback( async (entity, data) => { @@ -405,15 +279,27 @@ const useManager = () => { const id = localStorage.getItem('selected' + localEntity + 'Id'); const cleanedId = encodeURIComponent(id.replace(/"/g, '')); console.log('UPDAtING', entity, data); - handleEntityOperation(entity, `update/${cleanedId}`, 'update_', data); + handleEntityOperation( + entity, + `update/${cleanedId}`, + 'update_', + data, + 'PUT' + ); }, [handleEntityOperation] ); const deleteEntity = useCallback( - (entity, id) => - handleEntityOperation(entity, `delete/${id}`, 'delete_', { - id: id, - }), + async (entity, id) => + handleEntityOperation( + entity, + `delete/${id}`, + 'delete_', + { + id: id, + }, + 'DELETE' + ), [handleEntityOperation] ); const addCardToEntity = useCallback( @@ -426,10 +312,16 @@ const useManager = () => { } else { type = 'addNew'; } - return handleEntityOperation(entity, 'cards/add', 'add_cards_to_', { - cards: [card], - type: type, - }); + return handleEntityOperation( + entity, + 'cards/add', + 'add_cards_to_', + { + cards: [card], + type: type, + }, + 'POST' + ); } const selectEntVal = entity === 'collections' ? selectedCollection?._id : selectedDeck?._id; @@ -449,7 +341,8 @@ const useManager = () => { { cards: [card], type: type, - } + }, + 'POST' ); }, [handleEntityOperation] @@ -476,7 +369,8 @@ const useManager = () => { { cards: [item.id], type: type, - } + }, + 'DELETE' ); } const selectEntVal = @@ -505,13 +399,13 @@ const useManager = () => { { cards: [existingCard?.id], type: type, - } + }, + 'DELETE' ); }, [handleEntityOperation] ); - // Memoized return values to prevent unnecessary re-renders return useMemo( () => ({ customError, @@ -542,7 +436,6 @@ const useManager = () => { removeCardFromEntity('collections', item), addDeck: (data) => addEntity('decks', data), - updateDeck: (data) => updateEntity('decks', data), deleteDeck: (id) => deleteEntity('decks', id), addItemToDeck: (item) => addCardToEntity('decks', item), removeItemFromDeck: (item) => removeCardFromEntity('decks', item), @@ -558,8 +451,6 @@ const useManager = () => { setHasFetchedCollections, setHasFetchedDecks, setHasFetchedCart, - checkForCardInContext: isCardInContext, - compileCardsWithQuantities, fetchEntities, updateEntityField, setHasUpdatedCards, @@ -598,8 +489,6 @@ const useManager = () => { addCardToEntity, removeCardFromEntity, handleSelectEntity, - isCardInContext, - compileCardsWithQuantities, ] ); }; diff --git a/src/context/state/useMode.jsx b/src/context/state/useMode.jsx index 7749471..18faba0 100644 --- a/src/context/state/useMode.jsx +++ b/src/context/state/useMode.jsx @@ -1,5 +1,5 @@ +import { ColorModeContext } from 'context/ColorModeProvider'; import { useContext } from 'react'; -import { ColorModeContext } from '../ColorModeProvider'; const useMode = () => { const { mode, colorMode, theme, toggleColorMode } = diff --git a/src/context/state/useUserData.jsx b/src/context/state/useUserData.jsx index 71d808e..e8ed77e 100644 --- a/src/context/state/useUserData.jsx +++ b/src/context/state/useUserData.jsx @@ -1,10 +1,5 @@ -// hooks/useUserData.js +import { useFetchWrapper, useLocalStorage, useManageCookies } from 'context'; import { useCallback, useState, useEffect } from 'react'; -import useLocalStorage from '../hooks/useLocalStorage'; -import useFetchWrapper from '../hooks/useFetchWrapper'; -import useLogger from '../hooks/useLogger'; -import { useCookies } from 'react-cookie'; -import useManageCookies from '../hooks/useManageCookies'; function useUserData() { const { getCookie } = useManageCookies(); @@ -30,7 +25,6 @@ function useUserData() { totalPrice: 0, }, }); - const logger = useLogger('useUserData'); const { fetchWrapper } = useFetchWrapper(); const [hasFetchedUser, setHasFetchedUser] = useState(false); const [error, setError] = useState(null); @@ -67,7 +61,7 @@ function useUserData() { }, }); setHasFetchedUser(false); - }, [setUser, setHasFetchedUser, logger]); + }, [setUser, setHasFetchedUser]); const fetchUserData = useCallback(async () => { if (!userId || !isLoggedIn) return; @@ -82,9 +76,9 @@ function useUserData() { } catch (error) { console.error('Error fetching user data:', error); setError(error.message || 'Failed to fetch user data'); - logger.logEvent('Failed to fetch user data', error.message); + console.log('Failed to fetch user data', error.message); } - }, [userId, isLoggedIn, user, fetchWrapper, setUser, logger]); + }, [userId, isLoggedIn, user, fetchWrapper, setUser]); const updateUser = useCallback( async (updatedUserData) => { @@ -106,10 +100,10 @@ function useUserData() { } catch (error) { console.error('Error updating user data:', error); setError('Failed to update user data'); - logger.logError('Error updating user data:', error); + console.log('Error updating user data:', error); } }, - [userId, isLoggedIn, fetchWrapper, fetchUserData, logger] + [userId, isLoggedIn, fetchWrapper, fetchUserData] ); useEffect(() => { diff --git a/src/data/baseMenuItems.jsx b/src/data/baseMenuItems.jsx deleted file mode 100644 index 3c87f78..0000000 --- a/src/data/baseMenuItems.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import { - Store as StoreIcon, - ShoppingCart as CartIcon, - Assessment as CollectionIcon, -} from '@mui/icons-material'; -import DeckBuilderIcon from './DeckBuilderIcon'; -import { Badge } from '@mui/material'; - -const baseMenuItems = ({ cartCardQuantity }) => [ - { - name: 'Deck', - icon: , - to: '/deckbuilder', - requiresLogin: false, - }, - { - name: 'Collection', - icon: , - to: '/collection', - requiresLogin: true, - }, - { - name: 'Store', - icon: , - to: '/store', - requiresLogin: true, - }, - { - name: 'Cart', - icon: ( - - - - ), - to: '/cart', - requiresLogin: true, - }, -]; - -export default baseMenuItems; diff --git a/src/data/formsConfig.jsx b/src/data/formsConfig.jsx index 004bce4..7997a17 100644 --- a/src/data/formsConfig.jsx +++ b/src/data/formsConfig.jsx @@ -25,7 +25,7 @@ import { useAuthManager, useCardStore, useManager } from 'context'; // ---------------------------- FORM FIELD HANDLERS ---------------------------- // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- -const getFormFieldHandlers = () => { +const useGetFormFieldHandlers = () => { const { signup, login } = useAuthManager(); const { handleRequest } = useCardStore(); const { @@ -33,8 +33,7 @@ const getFormFieldHandlers = () => { updateCollection, deleteDeck, addDeck, - updateDeck: updateDeckDetails, - // selectedCollection, + updateEntityField, } = useManager(); const formHandlers = { @@ -59,7 +58,7 @@ const getFormFieldHandlers = () => { }, updateDeckForm: (formData) => { console.log('Update Deck Form Data:', formData); - updateDeckDetails(formData); + // updateEntityField('', formData); }, addDeckForm: (formData, additionalData) => { console.log('Add Deck Form Data:', formData, additionalData); @@ -112,7 +111,7 @@ const loginFormFields = { label: 'Username', name: 'username', type: 'text', - placeHolder: 'Username', + placeholder: 'Username', helperText: 'Enter your username', defaultValue: '', rules: { @@ -128,7 +127,7 @@ const loginFormFields = { name: 'password', type: 'password', helperText: 'Enter your password', - placeHolder: 'Password', + placeholder: 'Password', defaultValue: '', rules: { required: true, @@ -145,7 +144,7 @@ const signupFormFields = { label: 'First Name', name: 'firstName', type: 'text', - placeHolder: 'first name', + placeholder: 'first name', defaultValue: '', rules: { required: true, @@ -157,7 +156,7 @@ const signupFormFields = { label: 'Last Name', name: 'lastName', type: 'text', - placeHolder: 'last name', + placeholder: 'last name', defaultValue: '', rules: { required: true, @@ -169,7 +168,7 @@ const signupFormFields = { label: 'Email', name: 'email', type: 'email', - placeHolder: 'email', + placeholder: 'email', defaultValue: '', rules: { required: true, @@ -186,7 +185,7 @@ const addDeckFormFields = { name: 'name', label: 'Name', type: 'text', - placeHolder: 'Enter deck name', + placeholder: 'Enter deck name', defaultValue: '', rules: { required: true, @@ -200,7 +199,7 @@ const addDeckFormFields = { name: 'description', label: 'Description', type: 'multiline', - placeHolder: 'Enter deck description', + placeholder: 'Enter deck description', defaultValue: '', rules: { required: true, @@ -237,6 +236,7 @@ const updateDeckFormFields = { label: 'Color', name: 'color', type: 'select', + placeholder: 'Select a color', defaultValue: 'blue', rules: { required: false, @@ -263,7 +263,7 @@ const collectionFormFields = { name: 'name', label: 'Name', type: 'text', - placeHolder: 'Enter collection name', + placeholder: 'Enter collection name', defaultValue: '', rules: { required: true, @@ -276,7 +276,7 @@ const collectionFormFields = { name: 'description', label: 'Description', type: 'multiline', - placeHolder: 'Enter collection description', + placeholder: 'Enter collection description', defaultValue: '', rules: { required: true, @@ -394,7 +394,7 @@ const authSwitchFormFields = { authSwitch: { label: 'Auth Switch', type: 'switch', - placeHolder: 'Auth Switch', + placeholder: 'Auth Switch', name: 'authSwitch', defaultValue: false, rules: { @@ -540,15 +540,45 @@ const zodSchemas = { // -------------------------- ZOD VALIDATION FUNCTIONS ------------------------- // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- -const handleValidation = (schema, formData) => { +const handleSchemaValidation = (schema, formData) => { const result = schema.safeParse(formData); if (result.success) { console.log('Validation successful', formData); return { success: true, data: formData }; - } else { - console.error('Validation errors', result.error.errors); - return { success: false, errors: result.error.errors }; } + if (result.error) { + console.log('Validation failed', result.error.errors); + return { + success: false, + errors: result.error.errors, + message: 'Validation failed', + }; + } +}; +const handleFieldValidation = (fieldRules, value) => { + const rules = fieldRules; + const [error, setError] = []; + if (rules.required && !value) { + setError('This field is required'); + return 'This field is required'; + } + if (rules.minLength && value.length < rules.minLength) { + setError(`This field must be at least ${rules.minLength} characters`); + return `This field must be at least ${rules.minLength} characters`; + } + if (rules.maxLength && value.length > rules.maxLength) { + setError(`This field must not be more than ${rules.maxLength} characters`); + return `This field must be less than ${rules.maxLength} characters`; + } + if (rules.password) { + // const passwordRegex = new RegExp( + // '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])' + // ); + // if (!passwordRegex.test(value)) { + // return 'Password must contain at least one uppercase, lowercase, number, and special character'; + // } + } + // return console.log('Validation passed'); }; // ----------------------------------------------------------------------------- // ----------------------------------------------------------------------------- @@ -609,6 +639,7 @@ export { formFields, zodSchemas, configOptions, - handleValidation, - getFormFieldHandlers, + handleSchemaValidation, + handleFieldValidation, + useGetFormFieldHandlers, }; diff --git a/src/data/index.jsx b/src/data/index.jsx index 5d9a3ec..d16b335 100644 --- a/src/data/index.jsx +++ b/src/data/index.jsx @@ -1,30 +1,28 @@ import heroData from './heroData'; import collectionPortfolioHeaderItems from './collectionPortfolioHeaderItems'; -import baseMenuItems from './baseMenuItems'; import prepareTableData from './prepareTableData'; import HelmetMetaData from './HelmetMetaData'; import DeckBuilderIcon from './DeckBuilderIcon'; -import { ROUTES } from './route-config.jsx'; import { formFields, zodSchemas, configOptions, - handleValidation, - getFormFieldHandlers, + handleSchemaValidation, + useGetFormFieldHandlers, } from './formsConfig'; +import getRoutes from './route-config.jsx'; export { heroData, - ROUTES, + getRoutes, // ROUTE_CONFIG, HelmetMetaData, collectionPortfolioHeaderItems, - baseMenuItems, prepareTableData, // FORMS formFields, zodSchemas, configOptions, DeckBuilderIcon, - handleValidation, - getFormFieldHandlers, + handleSchemaValidation, + useGetFormFieldHandlers, }; diff --git a/src/data/route-config.jsx b/src/data/route-config.jsx index db7910c..83617d7 100644 --- a/src/data/route-config.jsx +++ b/src/data/route-config.jsx @@ -1,58 +1,119 @@ -// const ROUTES = [ -// { path: '/', componentName: 'HomePage', isPrivate: false }, -// { path: '/home', componentName: 'HomePage', isPrivate: false }, -// { path: '/deckbuilder', componentName: 'DeckBuilderPage', isPrivate: false }, -// { path: '/store', componentName: 'StorePage', isPrivate: false }, -// { path: '/cart', componentName: 'CartPage', isPrivate: true }, -// { path: '/collection', componentName: 'CollectionPage', isPrivate: true }, -// { path: '/profile', componentName: 'ProfilePage', isPrivate: true }, -// // { path: '/login', componentName: 'LoginDialog', isPrivate: false }, -// { path: '/login', componentName: 'LoginPage', isPrivate: false }, -// // { path: '/loginPage', componentName: 'LoginPage', isPrivate: false }, -// { path: '/signup', componentName: 'SignupPage', isPrivate: false }, -// // { path: '/about', componentName: 'AboutPage', isPrivate: false }, -// // { path: '/contact', componentName: 'ContactPage', isPrivate: false }, -// // { path: '/terms', componentName: 'TermsPage', isPrivate: false }, -// // { path: '/privacy', componentName: 'PrivacyPage', isPrivate: false }, -// { path: '*', componentName: 'NotFoundPage', isPrivate: false }, -// ]; - -import { NotFoundPage } from 'layout/REUSABLE_COMPONENTS'; import { CartPage, CollectionPage, DeckBuilderPage, HomePage, LoginPage, + NotFoundPage, ProfilePage, StorePage, } from 'pages'; +import { + Store as StoreIcon, + ShoppingCart as CartIcon, + Assessment as CollectionIcon, +} from '@mui/icons-material'; +import { Badge } from '@mui/material'; +import DeckBuilderIcon from './DeckBuilderIcon'; + +function getRoutes() { + const cartItemQuantity = JSON.parse(localStorage.getItem('cart'))?.items + ?.length; + const ROUTES = [ + { + routerPath: '/', + directoryPath: 'HomePage', + component: HomePage, + isPrivate: false, + + // navItem: true, + }, + { + routerPath: '/home', + directoryPath: 'HomePage', + component: HomePage, + isPrivate: false, + // navItem: true, + }, + { + routerPath: '/deckbuilder', + directoryPath: 'DeckBuilderPage', + component: DeckBuilderPage, + isPrivate: false, + name: 'Deck', + + icon: , + navItem: true, + }, + { + routerPath: '/store', + directoryPath: 'StorePage', + component: StorePage, + isPrivate: false, + name: 'Store', -// const ROUTE_CONFIG = { -// defaultPath: '/', -// routes: ROUTES.map(({ path, componentName, isPrivate }) => ({ -// path, -// componentName, -// isPrivate, -// })), -// }; + icon: , + navItem: true, + }, + { + routerPath: '/cart', + directoryPath: 'CartPage', + component: CartPage, + isPrivate: true, + name: 'Cart', -// export { ROUTES, ROUTE_CONFIG }; -// First, import all the necessary components + icon: ( + + + + ), + navItem: true, + }, + { + routerPath: '/collection', + directoryPath: 'CollectionPage', + component: CollectionPage, + isPrivate: true, + name: 'Collection', -// Then, refactor the ROUTES to use component references directly -const ROUTES = [ - { path: '/', component: HomePage, isPrivate: false }, - { path: '/home', component: HomePage, isPrivate: false }, - { path: '/deckbuilder', component: DeckBuilderPage, isPrivate: false }, - { path: '/store', component: StorePage, isPrivate: false }, - { path: '/cart', component: CartPage, isPrivate: true }, - { path: '/collection', component: CollectionPage, isPrivate: true }, - { path: '/profile', component: ProfilePage, isPrivate: true }, - { path: '/login', component: LoginPage, isPrivate: false }, - // { path: '/signup', component: SignupPage, isPrivate: false }, - { path: '*', component: NotFoundPage, isPrivate: false }, -]; + icon: , + navItem: true, + }, + { + routerPath: '/profile', + directoryPath: 'ProfilePage', + component: ProfilePage, + isPrivate: true, + // navItem: true, + }, + { + routerPath: '/login', + directoryPath: 'LoginPage', + component: LoginPage, + isPrivate: false, + }, + { + routerPath: '*', + directoryPath: 'NotFoundPage', + component: NotFoundPage, + isPrivate: false, + }, + ]; + return ROUTES; +} -// Export ROUTES and the refactored ROUTE_CONFIG -export { ROUTES }; +export default getRoutes; diff --git a/src/layout/REUSABLE_COMPONENTS/Configurator/index.jsx b/src/layout/REUSABLE_COMPONENTS/Configurator/index.jsx index 6b9d6d0..84528fc 100644 --- a/src/layout/REUSABLE_COMPONENTS/Configurator/index.jsx +++ b/src/layout/REUSABLE_COMPONENTS/Configurator/index.jsx @@ -13,7 +13,7 @@ import { } from '@mui/material'; import ConfiguratorRoot from './ConfiguratorRoot'; import searchData from 'data/json-data/search.json'; -import { StyledFormControl } from 'layout/REUSABLE_STYLED_COMPONENTS/ReusableStyledComponents'; +import { StyledFormControl } from 'layout/REUSABLE_STYLED_COMPONENTS'; import { Controller, useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMode, useConfigurator } from 'context'; diff --git a/src/layout/REUSABLE_COMPONENTS/MDBOX/index.jsx b/src/layout/REUSABLE_COMPONENTS/MDBOX/index.jsx index edbbb6a..faaaf3e 100644 --- a/src/layout/REUSABLE_COMPONENTS/MDBOX/index.jsx +++ b/src/layout/REUSABLE_COMPONENTS/MDBOX/index.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import MDBoxRoot from './MDBoxRoot'; +import MDBoxRoot from './MDBoxRoot.jsx'; import React from 'react'; const MDBox = React.forwardRef( diff --git a/src/layout/REUSABLE_COMPONENTS/RCBUTTON/examples/ButtonGradient.jsx b/src/layout/REUSABLE_COMPONENTS/RCBUTTON/examples/ButtonGradient.jsx index ced5682..79bc6f5 100644 --- a/src/layout/REUSABLE_COMPONENTS/RCBUTTON/examples/ButtonGradient.jsx +++ b/src/layout/REUSABLE_COMPONENTS/RCBUTTON/examples/ButtonGradient.jsx @@ -1,8 +1,7 @@ import Container from '@mui/material/Container'; import Grid from '@mui/material/Grid'; import Stack from '@mui/material/Stack'; -import { MDBox } from 'MDBOX/index'; -import RCButton from '..'; +import { MDBox, RCButton } from 'layout/REUSABLE_COMPONENTS'; function ButtonsGradient() { return ( diff --git a/src/layout/REUSABLE_COMPONENTS/RCLOADINGBUTTON/index.jsx b/src/layout/REUSABLE_COMPONENTS/RCLOADINGBUTTON/index.jsx index 6161dd7..0dd21f6 100644 --- a/src/layout/REUSABLE_COMPONENTS/RCLOADINGBUTTON/index.jsx +++ b/src/layout/REUSABLE_COMPONENTS/RCLOADINGBUTTON/index.jsx @@ -20,7 +20,7 @@ const ButtonContainer = ({ children, withContainer }) => ( const labelContent = (loading, icon, label) => loading ? ( - + {label} ) : ( diff --git a/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/RCTypographyRoot.jsx b/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/RCTypographyRoot.jsx index 0562614..2e560fa 100644 --- a/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/RCTypographyRoot.jsx +++ b/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/RCTypographyRoot.jsx @@ -2,7 +2,7 @@ import Typography from '@mui/material/Typography'; import { useMode } from 'context'; import styled from 'styled-components'; -export default styled(Typography)(({ ownerstate }) => { +export default styled(Typography)(({ ownerState }) => { // const { palette, typography, functions } = theme; const { theme } = useMode(); const { @@ -13,7 +13,7 @@ export default styled(Typography)(({ ownerstate }) => { opacity, textGradient, darkMode, - } = ownerstate; + } = ownerState; const { palette } = theme; const { gradients, transparent, white } = palette; const { diff --git a/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/index.jsx b/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/index.jsx index a8e590d..7ce6237 100644 --- a/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/index.jsx +++ b/src/layout/REUSABLE_COMPONENTS/RCTYPOGRAPHY/index.jsx @@ -2,6 +2,21 @@ import PropTypes from 'prop-types'; import RCTypographyRoot from './RCTypographyRoot'; import React from 'react'; +/** + * A reusable typography component. + * + * @component + * @param {string} color - Options: primary, secondary, tertiary, error, warning, info, success, light, dark, text, white, inherit. + * @param {boolean} fontWeight - Options: false, regular, medium, bold. + * @param {string} textTransform - Options: none, capitalize, uppercase, lowercase. + * @param {string} verticalAlign - Options: unset, baseline, sub, super, text-top, text-bottom, middle, top, bottom. + * @param {boolean} textGradient - Whether to apply a text gradient to the typography. + * @param {number} opacity - Options: 0 - 1. + * @param {ReactNode} children - The content of the typography. + * @param {object} rest - Additional props to be spread on the root element. + * @param {React.Ref} ref - The ref to be forwarded to the root element. + * @returns {JSX.Element} The rendered typography component. + */ const RCTypography = React.forwardRef( ( { @@ -20,7 +35,7 @@ const RCTypography = React.forwardRef( { +export default styled(Box)(({ ownerstate }) => { const { theme } = useMode(); - // const { color, size } = ownerstate; - + const { palette, functions, borders, boxShadows } = theme; + const { size, bgColor } = ownerstate; + const { white, text, transparent, gradients, dark, grey, success, error } = + palette; + const { boxShadow, linearGradient, pxToRem, rgba } = functions; + const backgroundValue = palette[bgColor] ? palette[bgColor].main : white.main; + let sizeValue = pxToRem(38); + if (size === 'small') { + sizeValue = pxToRem(25.4); + } else if (size === 'large') { + sizeValue = pxToRem(52); + } + let paddingValue = `${pxToRem(11)} ${pxToRem(11)} ${pxToRem(10)}`; + if (size === 'small') { + paddingValue = pxToRem(4.5); + } else if (size === 'large') { + paddingValue = pxToRem(16); + } + let colorValue = white.main; + if ( + bgColor === 'default' || + bgColor === 'primary' || + bgColor === 'white' || + bgColor === 'light' || + !palette[bgColor] + ) { + colorValue = text.colorPrimary; + } else if ( + bgColor === 'error' || + bgColor === 'dark' || + bgColor === 'success' || + bgColor === 'secondary' || + bgColor === 'info' || + bgColor === 'warning' + ) { + colorValue = white.main; + } return { + // !PREVIOUS STYLES borderRadius: '50%', - width: 40, - height: 40, - minHeight: '4rem', display: 'flex', alignItems: 'center', justifyContent: 'center', - color: color, - background: - color === 'success' ? theme.palette.success.main_light : 'black', - // [theme.breakpoints.down('md')]: { - // width: 30, - // height: 30, - // }, - [theme.breakpoints.down('sm')]: { - fontSize: '1.2rem', + // NEW STYLES + width: sizeValue, + minWidth: sizeValue, + height: sizeValue, + minHeight: sizeValue, + padding: paddingValue, + background: `${backgroundValue} !important`, // Use !important to override any later conflicting styles + color: `${colorValue} !important`, // Use !important to override any later conflicting styles + + '& .material-icons': { + marginTop: 0, }, - [theme.breakpoints.down('xs')]: { - fontSize: '1rem', + + '&:hover, &:focus, &:active': { + transform: 'none', }, }; }); +// let sizeValue = pxToRem(38); +// if (size === 'small') { +// sizeValue = pxToRem(25.4); +// } else if (size === 'large') { +// sizeValue = pxToRem(52); +// } +// let paddingValue = `${pxToRem(11)} ${pxToRem(11)} ${pxToRem(10)}`; +// if (size === 'small') { +// paddingValue = pxToRem(4.5); +// } else if (size === 'large') { +// paddingValue = pxToRem(16); +// } +// let colorValue = white.main; +// if ( +// bgColor === 'default' || +// bgColor === 'primary' || +// bgColor === 'white' || +// bgColor === 'light' || +// !palette[bgColor] +// ) { +// colorValue = text.colorPrimary; +// } else if ( +// bgColor === 'error' || +// bgColor === 'dark' || +// bgColor === 'success' || +// bgColor === 'secondary' || +// bgColor === 'info' || +// bgColor === 'warning' +// ) { +// colorValue = white.main; +// } +// const iconContainerStyles = () => { +// let sizeValue = pxToRem(38); +// if (size === 'small') { +// sizeValue = pxToRem(25.4); +// } else if (size === 'large') { +// sizeValue = pxToRem(52); +// } +// let paddingValue = `${pxToRem(11)} ${pxToRem(11)} ${pxToRem(10)}`; +// if (size === 'small') { +// paddingValue = pxToRem(4.5); +// } else if (size === 'large') { +// paddingValue = pxToRem(16); +// } +// return { +// width: sizeValue, +// minWidth: sizeValue, +// height: sizeValue, +// minHeight: sizeValue, +// padding: paddingValue, + +// '& .material-icons': { +// marginTop: 0, +// }, + +// '&:hover, &:focus, &:active': { +// transform: 'none', +// }, +// }; +// }; +// const iconStyles = () => { +// // let sizeValue = pxToRem(38); +// // if (size === 'small') { +// // sizeValue = pxToRem(25.4); +// // } else if (size === 'large') { +// // sizeValue = pxToRem(52); +// // } +// // let paddingValue = `${pxToRem(11)} ${pxToRem(11)} ${pxToRem(10)}`; +// // if (size === 'small') { +// // paddingValue = pxToRem(4.5); +// // } else if (size === 'large') { +// // paddingValue = pxToRem(16); +// // } +// // let colorValue = white.main; +// // if ( +// // bgColor === 'default' || +// // bgColor === 'primary' || +// // bgColor === 'white' || +// // bgColor === 'light' || +// // !palette[bgColor] +// // ) { +// // colorValue = text.colorPrimary; +// // } else if ( +// // bgColor === 'error' || +// // bgColor === 'dark' || +// // bgColor === 'success' || +// // bgColor === 'secondary' || +// // bgColor === 'info' || +// // bgColor === 'warning' +// // ) { +// // colorValue = white.main; +// // } +// // return { +// // // !PREVIOUS STYLES +// // borderRadius: '50%', +// // display: 'flex', +// // alignItems: 'center', +// // justifyContent: 'center', +// // // NEW STYLES +// // width: sizeValue, +// // minWidth: sizeValue, +// // height: sizeValue, +// // minHeight: sizeValue, +// // padding: paddingValue, +// // background: `${backgroundValue} !important`, // Use !important to override any later conflicting styles +// // color: `${colorValue} !important`, // Use !important to override any later conflicting styles + +// // '& .material-icons': { +// // marginTop: 0, +// // }, + +// // '&:hover, &:focus, &:active': { +// // transform: 'none', +// // }, +// // }; +// }; + +// const iconStyles = () => { +// let sizeValue = pxToRem(38); +// if (size === 'small') { +// sizeValue = pxToRem(25.4); +// } else if (size === 'large') { +// sizeValue = pxToRem(52); +// } +// let paddingValue = `${pxToRem(11)} ${pxToRem(11)} ${pxToRem(10)}`; + +// if (size === 'small') { +// paddingValue = pxToRem(4.5); +// } else if (size === 'large') { +// paddingValue = pxToRem(16); +// } + +// return { +// width: sizeValue, +// minWidth: sizeValue, +// height: sizeValue, +// minHeight: sizeValue, +// padding: paddingValue, + +// '& .material-icons': { +// marginTop: 0, +// }, + +// '&:hover, &:focus, &:active': { +// transform: 'none', +// }, +// }; +// }; +// return { +// width: sizeValue, +// minWidth: sizeValue, +// height: sizeValue, +// minHeight: sizeValue, +// padding: paddingValue, +// background: `${backgroundValue} !important`, // Use !important to override any later conflicting styles +// color: `${colorValue} !important`, // Use !important to override any later conflicting styles +// !PREV +// borderRadius: '50%', +// display: 'flex', +// alignItems: 'center', +// justifyContent: 'center', +// width: 40, +// height: 40, +// minHeight: '4rem', +// color: color, +// background: +// color === 'success' ? theme.palette.success.main_light : 'black', +// [theme.breakpoints.down('md')]: { +// width: 30, +// height: 30, +// }, +// [theme.breakpoints.down('sm')]: { +// fontSize: '1.2rem', +// }, +// [theme.breakpoints.down('xs')]: { +// fontSize: '1rem', +// }, +// }; +// }); diff --git a/src/layout/REUSABLE_COMPONENTS/RCWRAPPEDICON/index.jsx b/src/layout/REUSABLE_COMPONENTS/RCWRAPPEDICON/index.jsx index efdfab3..0386800 100644 --- a/src/layout/REUSABLE_COMPONENTS/RCWRAPPEDICON/index.jsx +++ b/src/layout/REUSABLE_COMPONENTS/RCWRAPPEDICON/index.jsx @@ -6,20 +6,41 @@ import PropTypes from 'prop-types'; // ==============================|| WRAPPED ICON ||============================== // +/** + * A reusable component that wraps an icon. + * + * @component + * @param {string} color - Options: + * @param {string} size - Options: + * @param {string} background - Options: + * @param {React.Ref} ref - The ref object for the component. + * @returns {React.Element} The rendered RCWrappedIcon component. + */ const RCWrappedIcon = React.forwardRef( ( - { color = 'black', size = '3rem', children, background = 'white', ...rest }, + { + // iconColor = 'white', + size = 'medium', + bgColor = 'success', + children, + ...rest + }, ref ) => { - const { theme } = useMode(); - return ( - + {children} @@ -30,10 +51,21 @@ const RCWrappedIcon = React.forwardRef( RCWrappedIcon.displayName = 'RCWrappedIcon'; RCWrappedIcon.propTypes = { - color: PropTypes.string, - size: PropTypes.string, + size: PropTypes.oneOf(['small', 'medium', 'large']), children: PropTypes.node.isRequired, - background: PropTypes.string, + bgColor: PropTypes.oneOf([ + 'inherit', + 'primary', + 'secondary', + 'info', + 'success', + 'warning', + 'error', + 'light', + 'dark', + 'text', + 'white', + ]), }; export default RCWrappedIcon; diff --git a/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCDynamicForm.jsx b/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCDynamicForm.jsx index 424ae3c..8c09184 100644 --- a/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCDynamicForm.jsx +++ b/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCDynamicForm.jsx @@ -1,21 +1,19 @@ /* eslint-disable @typescript-eslint/no-empty-function */ import React, { useEffect, useState } from 'react'; -import { Box, InputAdornment, Tooltip } from '@mui/material'; +import { Box, InputAdornment, LinearProgress, Tooltip } from '@mui/material'; import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; import { Controller } from 'react-hook-form'; -import { - FormBox, - FormFieldBox, -} from 'layout/REUSABLE_STYLED_COMPONENTS/ReusableStyledComponents'; +import { FormBox, FormFieldBox } from 'layout/REUSABLE_STYLED_COMPONENTS'; import { useBreakpoint, useFormSubmission, useRCFormHook, useMode, + useManager, } from 'context'; -import { getFormFieldHandlers } from 'data'; +import { useGetFormFieldHandlers } from 'data'; import { RCInput, RCLoadingButton } from '..'; const RCDynamicForm = ({ @@ -27,25 +25,34 @@ const RCDynamicForm = ({ additonalData, }) => { const { theme } = useMode(); + const { status } = useManager(); const { isMobile } = useBreakpoint(); - const methods = useRCFormHook(formKey, initialData); - const { onSubmit } = useFormSubmission(getFormFieldHandlers(), formKey); - // State and Effects + const methods = useRCFormHook(formKey); + const { onSubmit } = useFormSubmission(useGetFormFieldHandlers(), formKey); const [isMounted, setIsMounted] = useState(false); - const [formData, setFormData] = useState(initialData); - useEffect(() => { setIsMounted(true); + initialData && console.log('Mounted with initialData:', initialData); // Logging to debug initialData structure + inputs && console.log('Mounted with inputs:', inputs); // Logging to debug inputs structure + methods && console.log('Methods:', methods); // Logging to debug methods structure + methods.getValues() && console.log('Values:', methods.getValues()); // Logging to debug formValues structure return () => { setIsMounted(false); }; }, []); - useEffect(() => { - setFormData(initialData); - methods.reset(initialData); // Reset form with new initial data - }, [initialData, methods]); - + if (initialData) { + methods.reset(initialData); + } + }, []); + // useEffect(() => { + // // CHECK SUBMITTING STATUS + // console.log('SUBMITTING STATUS:', methods.formState.isSubmitting); + // }, [methods.formState.isSubmitting]); + useEffect(() => { + // CHECK VALIDATION STATUS + console.log('VALIDATION STATUS:', methods.formState.isValid); + }, [methods.formState.isValid]); const optionsForUi = userInterfaceOptions ? userInterfaceOptions : {}; if (!inputs || typeof inputs !== 'object') { @@ -80,15 +87,21 @@ const RCDynamicForm = ({ render={({ field, fieldState: { error } }) => ( { + // handleFieldValidation(field, e.target.value); + // field.onChange(e.target.value); + // }} /> )} /> @@ -124,7 +141,7 @@ const RCDynamicForm = ({ // ? optionsForUi?.updateActions?.handleSubmit // : handleSubmit(onSubmit) // } - loading={isSubmitting} + loading={isSubmitting || status === 'loading'} withContainer={false} fullWidth={true} circular={true} @@ -151,8 +168,8 @@ const RCDynamicForm = ({ )} + {/* {methods.formState.isSubmitting || + (status === 'loading' && ( + + + + ))} */} ); }; diff --git a/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCInput.jsx b/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCInput.jsx index c2c6292..abb031f 100644 --- a/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCInput.jsx +++ b/src/layout/REUSABLE_COMPONENTS/RC_FORMS/RCInput.jsx @@ -19,7 +19,7 @@ import { Chip, } from '@mui/material'; import PropTypes from 'prop-types'; -import { StyledTextField } from 'layout/REUSABLE_STYLED_COMPONENTS/ReusableStyledComponents'; +import { StyledTextField } from 'layout/REUSABLE_STYLED_COMPONENTS'; import { useBreakpoint, useManager, @@ -27,22 +27,28 @@ import { useSelectorActions, } from 'context'; import { RCSwitch } from '..'; +import { zodSchemas } from 'data'; +import { handleFieldValidation } from 'data/formsConfig'; const RCInput = React.forwardRef( ( { type = 'text', options = [], - onChange = () => {}, + onChange, initialValue = '', value = '', + rules = {}, placeholder = '', + loading = false, error = false, helperText = '', label = '', name = '', context = '', - InputProps = {}, + InputProps = { + formKey: '', + }, withContainer = false, // Default value for withContainer prop ...rest }, @@ -53,21 +59,38 @@ const RCInput = React.forwardRef( const { handleSelectChange, setTags, tags } = useSelectorActions(); const { updateEntityField } = useManager(); const [inputValue, setInputValue] = useState(''); + // const activeSchema = zodSchemas[formKey]; + const handleKeyDown = (event) => { if (event.key === 'Enter' && inputValue.trim()) { event.preventDefault(); console.log(`[INPUT]: ${inputValue}`); console.log(`[EVENT]: ${event.target.value}`); console.log(`[EVENT]: ${event}`); - handleSelectChange({ target: { value: inputValue } }, 'tags', 'decks'); + handleSelectChange( + { + type: 'add', // Define event type as 'add' + target: { value: inputValue }, + }, + 'tags', + 'Deck' + ); setInputValue(''); } }; const handleDeleteTag = (tagToDelete) => () => { - const updatedTags = tags.filter((tag) => tag.id !== tagToDelete.id); - setTags(updatedTags); // Update local state - updateEntityField('decks', 'deckId', 'tags', updatedTags); // Persist tags update + console.log(`[TAG TO DELETE]: ${tagToDelete}`); + handleSelectChange( + { + type: 'delete', // Define event type as 'delete' + target: { + value: tagToDelete, + }, + }, + 'tags', + 'Deck' + ); }; const { newPalette, functions } = theme; const { @@ -131,10 +154,17 @@ const RCInput = React.forwardRef( fullWidth placeholder={placeholder} onChange={(e) => onChange(e.target.value)} - value={value || ''} + // onChange={(e) => { + // e.preventDefault(); + // console.log(`[FIELD]: ${rules}`); + // // handleFieldValidation(rules, e.target.value); + // handleInputChange(e); + // }} + value={value} InputLabelProps={{ shrink: !initialValue ? undefined : true, }} + autoComplete="off" fontSize={isMobile ? '1rem' : '1.25rem'} error={!!error} helperText={helperText} @@ -144,15 +174,18 @@ const RCInput = React.forwardRef( case 'select': return ( - {label} + + {label} +