diff --git a/client/package-lock.json b/client/package-lock.json index d3d3fea..86ea40d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -22,7 +22,7 @@ "next": "13.2.1", "react": "18.2.0", "react-dom": "18.2.0", - "use-sync-v": "^2.0.19" + "use-sync-v": "^2.2.0" }, "engines": { "node": "18.14.2", @@ -4962,13 +4962,14 @@ } }, "node_modules/use-sync-v": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/use-sync-v/-/use-sync-v-2.0.19.tgz", - "integrity": "sha512-Ah6ztsfgYrtzdzaDBNozvwqp1sQw8TX1emozd8Dn4OmyGOA/k6ovo7ujmXEVPYlVXDNViqVIFgURVZM7zqVEQA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/use-sync-v/-/use-sync-v-2.2.0.tgz", + "integrity": "sha512-jjRfYr/zQhgWlpQbVu6KlRDSej60Pk+3osmvjpo6u4v6iN06VKW6cTew2dw+QG1ju0L2NKSsOBXWV7jQ4XTXkg==", "dependencies": { "diff": "^5.1.0", "lodash-es": "^4.17.21", - "react": "^18.2.0" + "react": "^18.2.0", + "use-sync-v": "^2.1.4" } }, "node_modules/util-deprecate": { diff --git a/client/package.json b/client/package.json index d2ac73e..d256ab5 100644 --- a/client/package.json +++ b/client/package.json @@ -29,6 +29,6 @@ "next": "13.2.1", "react": "18.2.0", "react-dom": "18.2.0", - "use-sync-v": "^2.0.19" + "use-sync-v": "^2.2.0" } } diff --git a/client/src/common/layout/page/index.js b/client/src/common/layout/page/index.js index 851fc1d..50e2cbf 100644 --- a/client/src/common/layout/page/index.js +++ b/client/src/common/layout/page/index.js @@ -1,11 +1,12 @@ import Box from '@mui/material/Box' import Header from '@/common/layout/header' import Section from '@/common/layout/section' -import { useSyncLocalStorage } from '@/lib/hooks/useSync' +import { setSyncLocalStorage, useSyncLocalStorage } from '@/lib/hooks/useSync' import { Sidebar } from '../sidebar' import { updateSyncV, useSyncV } from 'use-sync-v' import { Divider, useMediaQuery } from '@mui/material' import { useEffect } from 'react' +import { LoadingLinear } from '@/common/ui/loadingLinear' const Background = () => { const activeTheme = useSyncLocalStorage('activeTheme') @@ -20,8 +21,7 @@ const Background = () => { top: 0, width: '100%', height: '100%', - animation: `animate 90s linear infinite, ${ - activeTheme === 'dark' ? 'darkColorSwitcher' : 'lightColorSwitcher' + animation: `animate 90s linear infinite, ${activeTheme === 'dark' ? 'darkColorSwitcher' : 'lightColorSwitcher' } 42s linear infinite`, }, '&:after': { @@ -72,6 +72,9 @@ function Page({ children }) { const animate = useSyncLocalStorage('animate') const showSidebar = useSyncV('show.sidebar') const isMobile = useMediaQuery('(max-width:900px)') + const loading = useSyncV('show.loading') + + useEffect(() => { if (isMobile) { updateSyncV('show.sidebar', false) @@ -79,6 +82,13 @@ function Page({ children }) { updateSyncV('show.sidebar', true) } }, [isMobile]) + + useEffect(() => { + if (typeof animate === 'undefined') { + setSyncLocalStorage('animate', true) + } + }, [animate]) + return ( <> {animate && } @@ -94,14 +104,15 @@ function Page({ children }) { }} >
+ - {showSidebar && auth.authStatus === 'signedIn' && } + {(!loading && showSidebar && auth.authStatus === 'signedIn' && !auth.authLoading) && }
{children}
diff --git a/client/src/common/ui/loadingLinear/index.js b/client/src/common/ui/loadingLinear/index.js index 9c3b71f..df1d904 100644 --- a/client/src/common/ui/loadingLinear/index.js +++ b/client/src/common/ui/loadingLinear/index.js @@ -1,12 +1,39 @@ -import { LinearProgress } from '@mui/material' +import { Box, LinearProgress } from '@mui/material' +import { useRouter } from 'next/router' +import { useEffect } from 'react' +import { updateSyncV, useSyncV } from 'use-sync-v' export const LoadingLinear = () => { + const authLoading = useSyncV('auth.authLoading') + const loadingContacts = useSyncV('contacts.loading') + const loading = useSyncV('show.loading') + + const router = useRouter() + useEffect(() => { + const changeStartHandler = () => { + updateSyncV('show.loading', true) + } + const changeCompleteHandler = () => { + updateSyncV('show.loading', false) + } + router.events.on('routeChangeStart', changeStartHandler) + router.events.on('routeChangeComplete', changeCompleteHandler) + // If the component is unmounted, unsubscribe + // from the event with the `off` method: + return () => { + router.events.off('routeChangeStart', changeStartHandler) + router.events.off('routeChangeComplete', changeCompleteHandler) + } + }, [router]) return ( - + + {(loadingContacts || authLoading || loading) && } + + ) } \ No newline at end of file diff --git a/client/src/components/contacts/index.js b/client/src/components/contacts/index.js index b93183f..a3458c0 100644 --- a/client/src/components/contacts/index.js +++ b/client/src/components/contacts/index.js @@ -1,15 +1,10 @@ import Page from '@/common/layout/page' -import { LoadingLinear } from '@/common/ui/loadingLinear' import { Box } from '@mui/material' -import { useAsyncV } from 'use-sync-v' import { ContactListDisplay } from './display' function ContactsComponent() { - const { loading } = useAsyncV('contacts') - return ( - {loading && } { const subscribe = useCallback( @@ -34,7 +32,11 @@ export const useSyncLocalStorage = (saveDirectory = 'global') => { const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) - return state ? JSON.parse(state) : undefined + try { + return JSON.parse(state) + } catch (e) { + return state + } } export const setSyncLocalStorage = (saveDirectory = 'global', updatedValue) => { @@ -43,94 +45,12 @@ export const setSyncLocalStorage = (saveDirectory = 'global', updatedValue) => { subscriber() } } - localStorage[saveDirectory] = JSON.stringify(updatedValue) - emitChange() -} - -// this is an attempt to solve the bug, and detach getter from setter, so calling setter won't neccesarily subscribe the component to the store -/** - * Returns the state of the given save directory in the store, by subscribing to the save directory - * and using an external state manager to manage state updates. - * - * @param {string} saveDirectory - The save directory to be subscribed to. - * @returns {*} The state of the given save directory in the store. - */ -export const useSyncStore = (saveDirectory = 'global') => { - /** - * Subscribes to the given save directory and returns a function to unsubscribe from the - * save directory. - * - * @param {Function} callback - The callback function to be called when the save directory changes. - * @returns {Function} A function to unsubscribe from the save directory. - */ - const subscribe = useCallback( - (callback) => { - if (!useSyncStoreSubscribers[saveDirectory]) { - useSyncStoreSubscribers[saveDirectory] = [] - } - - // Adds the callback function to the list of subscribers for the save directory - useSyncStoreSubscribers[saveDirectory] = [ - ...useSyncStoreSubscribers[saveDirectory], - callback, - ] - - // Returns a function to remove the callback function from the list of subscribers - return () => { - useSyncStoreSubscribers[saveDirectory] = useSyncStoreSubscribers[ - saveDirectory - ].filter((el) => el !== callback) - } - }, - [saveDirectory] - ) - - /** - * Returns a snapshot of the state of the given save directory in the store. - * - * @returns {*} A snapshot of the state of the given save directory in the store. - */ - const getSnapshot = () => { - return JSON.stringify(store[saveDirectory]) - } - - /** - * Returns a snapshot of the state of the given save directory in the server. - * - * @returns {*} A snapshot of the state of the given save directory in the server. - */ - const getServerSnapshot = () => { - return JSON.stringify(store[saveDirectory]) - } - - - // Uses an external state manager to manage state updates and returns the state of the given save directory - const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) - - // Returns the parsed state of the given save directory - return state ? JSON.parse(state) : undefined -} - -/** - * Sets the updated value to the given save directory in the store, and notifies all subscribers - * subscribed to the save directory. - * - * @param {string} saveDirectory - The save directory in which to set the updated value. - * @param {*} updatedValue - The updated value to be set in the save directory. - */ -export const setSyncStore = (saveDirectory = 'global', updatedValue) => { - /** - * Emits change event to all subscribers subscribed to the save directory. - */ - const emitChange = () => { - for (let subscriber of useSyncStoreSubscribers[saveDirectory]) { - subscriber() - } + try { + const data = JSON.stringify(updatedValue) + localStorage[saveDirectory] = data + } catch(e) { + localStorage[saveDirectory] = updatedValue } - // Sets the updated value to the given save directory in the store - store[saveDirectory] = updatedValue - - // Notifies all subscribers subscribed to the save directory emitChange() } \ No newline at end of file diff --git a/client/src/pages/_app.js b/client/src/pages/_app.js index 336058e..8bee474 100644 --- a/client/src/pages/_app.js +++ b/client/src/pages/_app.js @@ -1,16 +1,18 @@ -import PropTypes from 'prop-types' -import Head from 'next/head' -import { ThemeProvider } from '@mui/material/styles' -import CssBaseline from '@mui/material/CssBaseline' -import { CacheProvider } from '@emotion/react' import '@/styles/globals.css' +import { CacheProvider } from '@emotion/react' +import CssBaseline from '@mui/material/CssBaseline' +import { ThemeProvider } from '@mui/material/styles' +import Head from 'next/head' +import PropTypes from 'prop-types' // MUI +import useInitAuth from '@/hooks/useInitAuth' +import { setSyncLocalStorage, useSyncLocalStorage } from '@/lib/hooks/useSync' import createEmotionCache from '@/lib/mui/createEmotionCache' -import { lightTheme, darkTheme } from '@/lib/mui/theme' -import { useSyncLocalStorage } from '@/lib/hooks/useSync' +import { darkTheme, lightTheme } from '@/lib/mui/theme' import { init } from '@/lib/store' -import useInitAuth from '@/hooks/useInitAuth' +import { useEffect } from 'react' + init() // Source: https://github.com/mui/material-ui/tree/master/examples/material-next @@ -21,7 +23,13 @@ const clientSideEmotionCache = createEmotionCache() export default function MyApp(props) { const activeTheme = useSyncLocalStorage('activeTheme') const { Component, emotionCache = clientSideEmotionCache, pageProps } = props + useInitAuth() + useEffect(()=>{ + if (typeof activeTheme === 'undefined') { + setSyncLocalStorage('activeTheme', 'light') + } + }, [activeTheme]) return ( diff --git a/client/src/pages/contacts/edit/[doc_id].js b/client/src/pages/contacts/edit/[doc_id].js index 5a0467c..9dfe98e 100644 --- a/client/src/pages/contacts/edit/[doc_id].js +++ b/client/src/pages/contacts/edit/[doc_id].js @@ -1,6 +1,6 @@ import { useRouter } from 'next/router' import { useEffect, useMemo, useState } from 'react' -import { deleteSyncV, updateSyncV, useAsyncV, useSyncV } from 'use-sync-v' +import { deleteSyncV, useAsyncV, useSyncV } from 'use-sync-v' import { FirebaseFirestore } from '@/lib/utils/firebase/firestore' import { deleteFileFromStorage, uploadFileToStorage } from '@/lib/utils/firebase/storageutils' @@ -87,7 +87,7 @@ const EditContact = () => { setIsFormChanged(true) } } - updateSyncV('contacts.loading', true) + const saveHandler = async () => { const createdContact = { ...form, diff --git a/client/src/pages/contacts/index.js b/client/src/pages/contacts/index.js index b2ba124..0dc8262 100644 --- a/client/src/pages/contacts/index.js +++ b/client/src/pages/contacts/index.js @@ -7,12 +7,12 @@ import { useEffect } from 'react' import { updateAsyncV, useSyncV } from 'use-sync-v' function Contacts() { - const user = useSyncV('auth') const showExportPopup = useSyncV('show.exportPopup') + const auth = useSyncV('auth') useEffect(() => { FirebaseFirestore.subscribeCol( - `users/${user.authUser.uid}/contacts`, + `users/${auth.authUser.uid}/contacts`, (querySnapshot) => { const data = [] querySnapshot.forEach((doc) => { @@ -24,8 +24,7 @@ function Contacts() { }, orderBy('sorting') ) - }, [user.authUser.uid]) - + }, [auth.authUser.uid]) return ( <> {showExportPopup && } diff --git a/client/src/pages/index.js b/client/src/pages/index.js index b6113c0..b0d1dff 100644 --- a/client/src/pages/index.js +++ b/client/src/pages/index.js @@ -1,7 +1,9 @@ import HomeComponent from '@/components/home' import { useSyncLocalStorage } from '@/lib/hooks/useSync' import { getRandomJoke } from '@/lib/services/random' +import { useRouter } from 'next/router' import { useEffect, useState } from 'react' +import { useSyncV } from 'use-sync-v' const defaultState = { joke:undefined, @@ -11,7 +13,17 @@ const defaultState = { function Index() { const [state, setState] = useState(defaultState) const activeTheme = useSyncLocalStorage('activeTheme') + const auth = useSyncV('auth') + const router = useRouter() + useEffect(()=>{ + if (auth.authLoading) return + if (auth.authStatus === 'signedIn') { + router.push('/contacts') + } else { + router.push('/login') + } + },[auth.authLoading, auth.authStatus, router]) useEffect(()=>{ (async()=>{ const randomJoke = await getRandomJoke() diff --git a/client/src/pages/login/index.js b/client/src/pages/login/index.js index 4dbb4ef..74bd01b 100644 --- a/client/src/pages/login/index.js +++ b/client/src/pages/login/index.js @@ -27,7 +27,7 @@ function Login () { const [state, setState] = useState(defaultState) const { username, password } = state const router = useRouter() - const { authUser, authLoading } = useSyncV('auth') + const auth = useSyncV('auth') class eventsHandler { static usernameHandler = (e) => { @@ -84,10 +84,11 @@ function Login () { },[]) useEffect(() => { - if (!authLoading && authUser) { + if (auth.authLoading) return + if (auth.authStatus === 'signedIn') { router.push('/contacts') } - }, [router, authUser, authLoading]) + }, [auth.authStatus, auth.authLoading, router]) const resetError = () => { updateSyncV('auth', (p) => ({ ...p, authError: '' }))