diff --git a/.babelrc b/.babelrc index 2ab6b2d3..1861e81e 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,4 @@ { - "presets": ["next/babel"], "env": { "test": { "plugins": [ diff --git a/jest.config.js b/jest.config.js index ef1cf259..5deb30c7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -26,6 +26,7 @@ module.exports = { '^@components/(.*)$': '/src/components/$1', '^@context/(.*)$': '/src/context/$1', '^@hooks/(.*)$': '/src/hooks/$1', + '^@styles/(.*)$': '/src/styles/$1', '^@utils/(.*)$': '/src/utils/$1', }, 'collectCoverageFrom': [ diff --git a/package.json b/package.json index 2f2af011..428e25d6 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "resource-workspace-rcl": "2.1.4", "scripture-resources-rcl": "5.5.9", "scripture-tsv": "0.4.0", - "single-scripture-rcl": "3.4.17", + "single-scripture-rcl": "3.4.19-beta.2", "tailwindcss": "^2.0.4", "tc-ui-toolkit": "5.3.3", "translation-helps-rcl": "3.5.14", diff --git a/pages/_app.js b/pages/_app.js new file mode 100644 index 00000000..b7482162 --- /dev/null +++ b/pages/_app.js @@ -0,0 +1,41 @@ +import { useEffect } from 'react' +import PropTypes from 'prop-types' +import { ThemeProvider } from '@material-ui/core/styles' +import CssBaseline from '@material-ui/core/CssBaseline' +import StoreContextProvider from '@context/StoreContext' +import AuthContextProvider from '@context/AuthContext' +import { APP_NAME } from '@common/constants' +import AppHead from '@components/AppHead' +import theme from '../src/theme' +import '@styles/globals.css' + +export default function Application({ Component, pageProps }) { + useEffect(() => { + // Remove the server-side injected CSS. + const jssStyles = document.querySelector('#jss-server-side') + + if (jssStyles) { + jssStyles.parentElement.removeChild(jssStyles) + } + }, []) + + return ( + <> + + + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + + + + + + + + ) +} + +Application.propTypes = { + Component: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), + pageProps: PropTypes.object, +} diff --git a/src/components/App.jsx b/src/components/App.jsx index 39a984d9..5fc2cb8b 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -5,8 +5,8 @@ import CssBaseline from '@material-ui/core/CssBaseline' import StoreContextProvider from '@context/StoreContext' import AuthContextProvider from '@context/AuthContext' // import { APP_NAME } from '@common/constants' -// import '@styles/globals.css' -// import WorkspaceContainer from '@components/WorkspaceContainer' +import '@styles/globals.css' +import WorkspaceContainer from '@components/WorkspaceContainer' import Layout from '@components/Layout' import theme from '../theme' @@ -27,7 +27,7 @@ export default function Application() { - {/**/} + diff --git a/src/components/Drawer.jsx b/src/components/Drawer.jsx index 71dc5205..dd93f50f 100644 --- a/src/components/Drawer.jsx +++ b/src/components/Drawer.jsx @@ -1,6 +1,5 @@ import React, { useContext } from 'react' import PropTypes from 'prop-types' -import { useRouter } from 'next/router' import SwipeableDrawer from '@material-ui/core/SwipeableDrawer' import ChevronLeftIcon from '@material-ui/icons/ChevronLeft' import ListItemIcon from '@material-ui/core/ListItemIcon' @@ -9,7 +8,7 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction' import ExitToAppIcon from '@material-ui/icons/ExitToApp' import SettingsIcon from '@material-ui/icons/Settings' import BugReportIcon from '@material-ui/icons/BugReport' -import DoneAllIcon from '@material-ui/icons/DoneAll'; +import DoneAllIcon from '@material-ui/icons/DoneAll' import IconButton from '@material-ui/core/IconButton' import ListItem from '@material-ui/core/ListItem' import List from '@material-ui/core/List' @@ -34,13 +33,22 @@ export default function Drawer({ checkUnsavedChanges, showFeedback, }) { - const router = useRouter() + const { + state: { + mergeStatusForCards, + cardsSaving, + cardsLoadingMerge, + }, + actions: { + setPage, + } + } = useContext(StoreContext) async function onSettingsClick() { const okToContinue = await checkUnsavedChanges() if (okToContinue) { - router.push('/settings') + setPage('/settings') onClose() } } @@ -48,16 +56,8 @@ export default function Drawer({ function onFeedbackClick() { onClose() showFeedback && showFeedback() - } - - const { - state: { - mergeStatusForCards, - cardsSaving, - cardsLoadingMerge, - }, - } = useContext(StoreContext) + } const mergeButtonProps = useMergeCardsProps({ mergeStatusForCards, isMerging: cardsLoadingMerge?.length }); const { diff --git a/src/components/ErrorPage.jsx b/src/components/ErrorPage.jsx new file mode 100644 index 00000000..096ec0d9 --- /dev/null +++ b/src/components/ErrorPage.jsx @@ -0,0 +1,62 @@ +import { useContext } from 'react' +import PropTypes from 'prop-types' +import { StoreContext } from '@context/StoreContext' + +export default function ErrorPage({ statusCode }) { + const { + actions: { + setPage + }, + } = useContext(StoreContext) + + if (statusCode) { + return
{`Error ${statusCode}`}
+ } + + localStorage.clear() + + const handleClick = (e) => { + e.preventDefault() + setPage('/') + } + + return ( +
+ + + + + + + + + + +

+ An unexpected error has occurred. The application cache has been cleared. +

+ +
+) +} + +ErrorPage.propTypes = { statusCode: PropTypes.number } + +ErrorPage.getInitialProps = ({ res, err }) => {//eslint-disable-next-line + const statusCode = res ? res.statusCode : err ? err.statusCode : 404 + return { statusCode } +} diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 91270c06..5c98d438 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,6 +1,5 @@ import { useState, useContext } from 'react' import PropTypes from 'prop-types' -import { useRouter } from 'next/router' import { makeStyles } from '@material-ui/core/styles' import Typography from '@material-ui/core/Typography' import IconButton from '@material-ui/core/IconButton' @@ -44,16 +43,16 @@ export default function Header({ mergeStatusForCards, }) { const classes = useStyles() - const router = useRouter() const [drawerOpen, setOpen] = useState(false) const { actions: { logout } } = useContext(AuthContext) const { state: { cardsSaving, - cardsLoadingUpdate + cardsLoadingUpdate, }, actions: { checkUnsavedChanges, + setPage, } } = useContext(StoreContext) @@ -104,7 +103,7 @@ export default function Header({ router.push('/')} + onClick={() => setPage('/')} > {title} diff --git a/src/components/Layout.jsx b/src/components/Layout.jsx index 81f4f7d0..1166122e 100644 --- a/src/components/Layout.jsx +++ b/src/components/Layout.jsx @@ -8,14 +8,14 @@ import { StoreContext } from '@context/StoreContext' import { getBuildId } from '@utils/build' import { APP_NAME, BASE_URL, PROD, QA, QA_BASE_URL } from '@common/constants' import useValidateAccountSettings from '@hooks/useValidateAccountSettings' -import { useRouter } from 'next/router' +import SettingsPage from '@components/SettingsPage' +import { parsePage } from '@utils/pages' export default function Layout({ children, showChildren, title = APP_NAME, }) { - const router = useRouter() const mainScreenRef = useRef(null) const [feedback, setFeedback_] = useState(null) // contains feedback data const { @@ -43,6 +43,7 @@ export default function Layout({ owner, server, mergeStatusForCards, + page, }, actions: { setCurrentLayout, @@ -57,27 +58,58 @@ export default function Layout({ }, [ mainScreenRef?.current ]) useEffect(() => { - const params = router?.query + const parsedUrl = new URL(window.location.href) + const params = parsedUrl.searchParams - if (typeof params?.server === 'string') { // if URL param given - let serverID_ = params.server.toUpperCase() === QA ? QA : PROD + if (params && typeof params.get('server') === 'string') { // if URL param given + let serverID_ = params.get('server').toUpperCase() === QA ? QA : PROD let server_ = (serverID_ === QA) ? QA_BASE_URL : BASE_URL - if (params.server?.length === 0){ + + if (params.get('server')?.length === 0){ server_ = (import.meta.env.NEXT_PUBLIC_BUILD_CONTEXT === 'production') ? BASE_URL : QA_BASE_URL serverID_ = (server_ === QA_BASE_URL) ? QA : PROD } if (server !== server_) { - console.log(`_app.js - On init switching server to: ${serverID_}, url server param '${params.server}', old server ${server}, reloading page`) + console.log( + `_app.js - On init switching server to: ${serverID_}, url server param '${params.get( + 'server', + )}', old server ${server}, reloading page`, + ) setServer(server_) // persist server selection in localstorage - router.push(`/?server=${serverID_}`) // reload page + window.location.assign(`${window.location.host}/?server=${serverID_}`) // reload page } } - }, [router?.query]) // TRICKY query property not loaded on first pass, so watch for change + }, []) const buildId = useMemo(getBuildId, []) useValidateAccountSettings(authentication, showAccountSetup, languageId, owner, setShowAccountSetup) + /** + * determine the page to show based on state + * @returns {*|JSX.Element} + */ + function getDisplayPage() { + const parsed = parsePage(page) + + if (showChildren || (authentication && !showAccountSetup)) { + return children + } + + if ((authentication && showAccountSetup) || (parsed?.page === '/settings')) { + return ( + + ) + } + + return ( + + ) + } + return (
- {showChildren || (authentication && !showAccountSetup) ? ( - children - ) : ( - - )} + {getDisplayPage()}
import('@components/AccountSetup'), { - loading: () => , -}) +import AccountSetup from '@components/AccountSetup' function Onboarding({ authentication, authenticationComponent }) { if (authentication) { diff --git a/src/components/SettingsPage.jsx b/src/components/SettingsPage.jsx new file mode 100644 index 00000000..a8b1745c --- /dev/null +++ b/src/components/SettingsPage.jsx @@ -0,0 +1,38 @@ +import { useContext } from 'react' +import Button from '@material-ui/core/Button' +import SaveIcon from '@material-ui/icons/Save' +import { AuthenticationContext } from 'gitea-react-toolkit' +import TranslationSettings from '@components/TranslationSettings' +import { StoreContext } from '@context/StoreContext' + +const SettingsPage = () => { + const { state: authentication } = useContext(AuthenticationContext) + const { + actions: { + setShowAccountSetup + }, + } = useContext(StoreContext) + + return ( +
+
+

Account Settings

+ +
+ +
+
+
+ ) +} + +export default SettingsPage diff --git a/src/components/TranslationSettings.jsx b/src/components/TranslationSettings.jsx index 7d7faead..99437c63 100644 --- a/src/components/TranslationSettings.jsx +++ b/src/components/TranslationSettings.jsx @@ -22,7 +22,6 @@ import { processNetworkError, reloadApp, } from '@utils/network' -import { useRouter } from 'next/router' import { AuthContext } from '@context/AuthContext' import NetworkErrorPopUp from '@components/NetworkErrorPopUp' import CircularProgress from './CircularProgress' @@ -35,8 +34,10 @@ const useStyles = makeStyles(theme => ({ }, })) -export default function TranslationSettings({ authentication }) { - const router = useRouter() +export default function TranslationSettings({ + authentication, + setPage, +}) { const { actions: { logout } } = useContext(AuthContext) const classes = useStyles() const [organizations, setOrganizations] = useState([]) @@ -67,7 +68,7 @@ export default function TranslationSettings({ authentication }) { error, httpCode, logout, - router, + setPage, setNetworkError, setLastError, setOrgErrorMessage @@ -206,12 +207,13 @@ export default function TranslationSettings({ authentication }) { {organizations.length === 0 ? null : languages.map( - ({ languageId, languageName, localized }, i) => ( - - {`${languageId} - ${languageName} - ${localized}`} - - ) - )} + ({ languageId, languageName, localized }, i) => ( + + {`${languageId} - ${languageName} - ${localized}`} + + ) + ) + }
diff --git a/src/components/WorkspaceContainer.jsx b/src/components/WorkspaceContainer.jsx index 77646b12..7e2c1257 100644 --- a/src/components/WorkspaceContainer.jsx +++ b/src/components/WorkspaceContainer.jsx @@ -19,7 +19,7 @@ import { ORIGINAL_SOURCE, OT_ORIG_LANG, OT_ORIG_LANG_BIBLE, - // ScriptureCard, + ScriptureCard, splitUrl, TARGET_LITERAL, TARGET_SIMPLIFIED, @@ -41,7 +41,6 @@ import { processNetworkError, reloadApp, } from '@utils/network' -import { useRouter } from 'next/router' import { HTTP_CONFIG } from '@common/constants' import NetworkErrorPopUp from '@components/NetworkErrorPopUp' import WordAlignerDialog from '@components/WordAlignerDialog' @@ -71,7 +70,6 @@ const buildId = getBuildId() console.log(`Gateway Edit App Version`, buildId) function WorkspaceContainer() { - const router = useRouter() const classes = useStyles() const [state, _setState] = useState({ currentVerseReference: null, @@ -145,6 +143,7 @@ function WorkspaceContainer() { setSavedChanges, updateMergeState, updateTaDetails, + setPage }, } = useContext(StoreContext) @@ -234,7 +233,7 @@ function WorkspaceContainer() { * @param {number} httpCode - http code returned */ function processError(errorMessage, httpCode=0) { - processNetworkError(errorMessage, httpCode, logout, router, setNetworkError, setLastError ) + processNetworkError(errorMessage, httpCode, logout, setPage, setNetworkError, setLastError ) } function setNetworkError( error ) { @@ -247,8 +246,8 @@ function WorkspaceContainer() { */ function showNetworkError() { if (tokenNetworkError) { // if we had a token network error on startup - if (!tokenNetworkError.router) { // needed for reload of page - setTokenNetworkError({ ...tokenNetworkError, router }) // make sure router is set + if (!tokenNetworkError.setPage) { // needed for reload of page + setTokenNetworkError({ ...tokenNetworkError, setPage }) // make sure setPage is set } return ( cardProps.type === 'scripture_card' ? - { /* */ } + : , ) diff --git a/src/context/StoreContext.jsx b/src/context/StoreContext.jsx index 4988da55..19e1239a 100644 --- a/src/context/StoreContext.jsx +++ b/src/context/StoreContext.jsx @@ -8,6 +8,7 @@ import useLocalStorage from '@hooks/useLocalStorage' import useULS from '@hooks/useUserLocalStorage' import { AuthContext } from '@context/AuthContext' import useSaveChangesPrompt from '@hooks/useSaveChangesPrompt' +import { parsePage } from '@utils/pages' export const StoreContext = createContext({}) @@ -21,6 +22,7 @@ export default function StoreContextProvider(props) { button in the hamburger menu. */ const [mergeStatusForCards, setMergeStatusForCards] = useState({}) + function updateMergeState( cardId, title, @@ -92,6 +94,7 @@ export default function StoreContextProvider(props) { const [cardsSaving, setCardsSaving] = useState([]) const [cardsLoadingUpdate, setCardsLoadingUpdate] = useState([]) const [cardsLoadingMerge, setCardsLoadingMerge] = useState([]) + const [page, setPage_] = useState(null) // to navigate pages const { savedChanges, @@ -100,6 +103,20 @@ export default function StoreContextProvider(props) { showSaveChangesPrompt, } = useSaveChangesPrompt() + /** + * change the current page + * @param {string} path + */ + function setPage(path) { + const parsed = parsePage(path) + + if (parsed?.page === '/') { + window.location.assign(path) // load page + } else { + setPage_(path) + } + } + function onReferenceChange(bookId, chapter, verse) { setQuote(null) setBibleReference(prevState => ({ @@ -150,6 +167,7 @@ export default function StoreContextProvider(props) { cardsLoadingUpdate, cardsLoadingMerge, mergeStatusForCards, + page, }, actions: { logout, @@ -176,6 +194,7 @@ export default function StoreContextProvider(props) { checkUnsavedChanges, showSaveChangesPrompt, updateMergeState, + setPage, }, } diff --git a/src/utils/__tests__/pages.spec.js b/src/utils/__tests__/pages.spec.js new file mode 100644 index 00000000..a7474c7a --- /dev/null +++ b/src/utils/__tests__/pages.spec.js @@ -0,0 +1,19 @@ +/// +import { parsePage } from '../pages' + +const tests = [ + { + path: null, + expected: { }, + }, +] + +describe('test parsePage', () => { + tests.forEach(({ path, expected }) => { + it(`path ${path} should return ${expected}`,() => { + const results = parsePage(path) + + expect(results).toEqual(expected) + }) + }) +}) diff --git a/src/utils/network.js b/src/utils/network.js index 02939473..26f34b27 100644 --- a/src/utils/network.js +++ b/src/utils/network.js @@ -124,12 +124,12 @@ export async function getNetworkError(error, httpCode ) { * @param {string|Error} error - initial error message message or object * @param {number} httpCode - http code returned * @param {function} logout - invalidate current login - * @param {object} router - to change to different web page + * @param {function} setPage - to change to different web page * @param {function} setNetworkError - callback to toggle display of error popup * @param {function} setLastError - callback to save error details * @param {function} setErrorMessage - optional callback to apply error message */ -export async function processNetworkError(error, httpCode, logout, router, +export async function processNetworkError(error, httpCode, logout, setPage, setNetworkError, setLastError, setErrorMessage, ) { // TRICKY we need to show an initial message because there may be delays checking for server connection. @@ -138,15 +138,15 @@ export async function processNetworkError(error, httpCode, logout, router, const initialShownError = { errorMessage: initialMessage + CHECKING_SERVER, lastError: error, - router: router, - logout: logout, + setPage, + logout, } setNetworkError && setNetworkError(initialShownError) // clear until processing finished const errorObj = await getNetworkError(error, httpCode) setErrorMessage && setErrorMessage(errorObj.errorMessage) setLastError && setLastError(errorObj.lastError) // error info to attach to sendmail // add params needed for button actions - errorObj.router = router + errorObj.setPage = setPage errorObj.logout = logout setNetworkError && setNetworkError(errorObj) // this triggers network error popup } @@ -156,12 +156,12 @@ export async function processNetworkError(error, httpCode, logout, router, * @param {string|Error} error - initial error message message or object * @param {number} httpCode - http code returned * @param {function} logout - invalidate current login - * @param {object} router - to change to different web page + * @param {function} setPage - to change to different web page * @param {function} setNetworkError - callback to toggle display of error popup * @param {function} setLastError - callback to save error details * @param {function} setErrorMessage - optional callback to apply error message */ -export async function addNetworkDisconnectError(error, httpCode, logout, router, +export async function addNetworkDisconnectError(error, httpCode, logout, setPage, setNetworkError, setLastError, setErrorMessage, ) { const errorObj = await getNetworkError(error, httpCode) @@ -173,7 +173,7 @@ export async function addNetworkDisconnectError(error, httpCode, logout, router, setErrorMessage && setErrorMessage(errorObj.errorMessage) setLastError && setLastError(errorObj.lastError) // error info to attach to sendmail // add params needed for button actions - errorObj.router = router + errorObj.setPage = setPage errorObj.logout = logout setNetworkError && setNetworkError(errorObj) // this triggers network error popup } @@ -193,16 +193,16 @@ export function unAuthenticated(httpCode) { */ export function reloadApp(networkError) { setLocalStorageValue(SERVER_CHECK_SECOND_TRY_KEY, true) // we will do longer wait on retry - networkError?.router?.reload() + goToPage(networkError?.setPage, '/') } /** * go to specific page - * @param {object} router - to change to different web page + * @param {function} setPage - to change to different web page * @param {string} page - URL to redirect to */ -export function goToPage(router, page) { - router?.push(page) +export function goToPage(setPage, page) { + setPage?.push(page) } /** @@ -210,7 +210,7 @@ export function goToPage(router, page) { * @param {object} networkError - contains details about how to display and handle network error - created by processNetworkError */ function gotoFeedback(networkError) { - goToPage(networkError?.router, FEEDBACK_PAGE) + goToPage(networkError?.setPage, FEEDBACK_PAGE) } /** diff --git a/src/utils/pages.js b/src/utils/pages.js new file mode 100644 index 00000000..4cd82749 --- /dev/null +++ b/src/utils/pages.js @@ -0,0 +1,17 @@ +/** + * parse the new page path + * @param {string} path - in format such as `/` , `/settings` or `/?server=QA` + * @returns {{page: string, params: object}} + */ +export function parsePage(path) { + if ((typeof path === 'string') && (path[0] === '/')) { + const parts = path.substr(1).split('?') // remove any parameters + const page = parts[0] + return { + page, + params: parts?.length > 1 ? parts[1] : '', + } + } + return {} +} + diff --git a/yarn.lock b/yarn.lock index 6fdc1074..327a8d17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9612,10 +9612,10 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" -single-scripture-rcl@3.4.17: - version "3.4.17" - resolved "https://registry.yarnpkg.com/single-scripture-rcl/-/single-scripture-rcl-3.4.17.tgz#365bbf6ac349fdfc839ba646171ff3936533047d" - integrity sha512-uJ07dhxxaXVNAsed1SIVT7qLId+59MNvz5RjFUXoYJCXS73XoJnJXrqAgLEjiU7Q1F/+bQAYjFRSkNzKVo9CyQ== +single-scripture-rcl@3.4.19-beta.2: + version "3.4.19-beta.2" + resolved "https://registry.yarnpkg.com/single-scripture-rcl/-/single-scripture-rcl-3.4.19-beta.2.tgz#17309072fd55fe48bb5323a25d7ca011165aa503" + integrity sha512-0J8TSf/xnm4ZlXSIcMWB/69T6TFBVzAVELQjPoRWPyrHrgw52Sc6iQ1EI+ozh1N3A2vCtYItnDNDCDBEcXWN+A== dependencies: "@react-hookz/deep-equal" "^1.0.4" "@types/react" "^17.0.0"