diff --git a/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js b/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js index 38540a6c..6fe68997 100644 --- a/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js +++ b/renderer/src/components/EditorPage/TextEditor/BibleNavigationX/index.js @@ -11,7 +11,7 @@ import SelectChapter from './SelectChapter'; export default function BibleNavigationX(props) { const { - chapterNumber, setChapterNumber, setBook, loading, bookAvailable, booksInProject, + chapterNumber, setChapterNumber, setBook, loading, bookAvailable, booksInProject, parseError } = props; const { @@ -60,6 +60,13 @@ export default function BibleNavigationX(props) { setReference(); }, [bookId, chapter]); + useEffect(() => { + if (parseError) { + setOpenBook(false); + setOpenChapter(false); + } + }, [parseError]); + useEffect(() => { if (openBook === false && openChapter === false) { setCloseNavigation(true); diff --git a/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx b/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx index 6bc2ea42..7047589d 100644 --- a/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx +++ b/renderer/src/components/EditorPage/TextEditor/EditorMenuBar.jsx @@ -22,6 +22,7 @@ export default function EditorMenuBar(props) { loading, bookAvailable, booksInProject, + parseError } = props; const { t } = useTranslation(); @@ -53,6 +54,7 @@ export default function EditorMenuBar(props) { loading={loading} bookAvailable={bookAvailable} booksInProject={booksInProject} + parseError={parseError} />
+
+
+
+
+ + + + + + + + + +
+

+ Some USFM tags appear to be in unexpected locations. Kindly check the + + USFM documentation + + +

+ {/*

Kindly check for errors in the USFM file

*/} +
+
+
+
+ ); +} diff --git a/renderer/src/components/EditorPage/TextEditor/cacheUtils.js b/renderer/src/components/EditorPage/TextEditor/cacheUtils.js index 4a8dd433..afb1776f 100644 --- a/renderer/src/components/EditorPage/TextEditor/cacheUtils.js +++ b/renderer/src/components/EditorPage/TextEditor/cacheUtils.js @@ -84,32 +84,40 @@ export async function handleCache(filePath, usfmContent, projectCachePath, fileC const oldHash = fileCacheMap[filePath]; async function processAndCacheUSJ() { - const { usj, error } = await convertUsfmToUsj(usfmContent); - if (error) { - // eslint-disable-next-line no-console - console.error('Error parsing USFM', error); - return { error }; + try { + const { usj, error } = await convertUsfmToUsj(usfmContent); + if (error) { + console.error('Error parsing USFM:', error); + return { error, usj: null }; // Return consistent error object + } + writeCache(newHash, usj, projectCachePath); + updateCacheMapToFile(fileCacheMapPath, filePath, newHash); + return { usj, error: null }; // Always include error field + } catch (err) { + console.error('Error in processAndCacheUSJ:', err); + return { error: err.message, usj: null }; } - writeCache(newHash, usj, projectCachePath); - updateCacheMapToFile(fileCacheMapPath, filePath, newHash); - return { usj }; } - if (!oldHash) { - // eslint-disable-next-line no-console - console.log('No existing hash found. Creating new cache entry.'); - return processAndCacheUSJ(); - } + try { + if (!oldHash) { + console.log('No existing hash found. Creating new cache entry.'); + return processAndCacheUSJ(); + } - if (isCacheValid(oldHash, projectCachePath) && oldHash === newHash) { - // eslint-disable-next-line no-console - console.log('Cache hit'); - return { usj: await readCache(oldHash, projectCachePath) }; + if (isCacheValid(oldHash, projectCachePath) && oldHash === newHash) { + console.log('Cache hit'); + const cachedUsj = await readCache(oldHash, projectCachePath); + return { usj: cachedUsj, error: null }; + } + + console.log('Cache miss or content changed'); + deleteOldCacheFile(oldHash, projectCachePath); + return processAndCacheUSJ(); + } catch (err) { + console.error('Error in handleCache:', err); + return { error: err.message, usj: null }; } - // eslint-disable-next-line no-console - console.log('Cache miss or content changed'); - deleteOldCacheFile(oldHash, projectCachePath); - return processAndCacheUSJ(); } export async function updateCache(filePath, usj, usfm, fileCacheMapPath, projectCachePath) { diff --git a/renderer/src/components/EditorPage/TextEditor/index.jsx b/renderer/src/components/EditorPage/TextEditor/index.jsx index eb9e75f9..24a148ca 100644 --- a/renderer/src/components/EditorPage/TextEditor/index.jsx +++ b/renderer/src/components/EditorPage/TextEditor/index.jsx @@ -10,6 +10,9 @@ import EditorMenuBar from './EditorMenuBar'; import LexicalEditor from './LexicalEditor'; import { updateCacheNSaveFile } from './updateAndSave'; import EmptyScreen from './EmptyScreen'; +import ErrorScreen from './ErrorScreen'; +import { useAutoSnackbar } from '@/components/SnackBar'; +import { useTranslation } from 'react-i18next'; const defaultScrRef = { bookCode: 'PSA', @@ -24,16 +27,17 @@ export default function TextEditor() { const [usjInput, setUsjInput] = useState(); const [scrRef, setScrRef] = useState(defaultScrRef); const [navRef, setNavRef] = useState(); + const [parseError, setParseError] = useState(false); const { state: { bookId: defaultBookId, selectedFont, editorFontSize, projectScriptureDir, - // chapter, - // verse, }, actions: { handleSelectedFont, onChangeChapter, onChangeVerse, handleEditorFontSize, }, } = useContext(ReferenceContext); + const { showSnackbar } = useAutoSnackbar(); + const { t } = useTranslation(); const [book, setBook] = useState(defaultBookId); const { @@ -41,15 +45,28 @@ export default function TextEditor() { } = useReadUsfmFile(book); useEffect(() => { - if (cachedData.error) { - console.error('Error parsing USFM', cachedData.error); + if (loading) { + showSnackbar(`Preparing ${book.toUpperCase()} file`, 'update'); + }; + const { usj, error } = cachedData; + if (!loading) { + if (error) { + console.error('Error parsing USFM:', error); + setParseError(true); + showSnackbar(t('dynamic-msg-load-ref-bible-snack-fail', { refName: book.toUpperCase() }), 'failure'); + setUsjInput(null); + return; + } + } + if (!usj || Object.entries(usj).length === 0) { + setParseError(false); + setUsjInput(null); return; } - const { usj } = cachedData; - if (!usj && usj?.entries(usj).length === 0) { return; } - // console.log(usj); + setParseError(false); setUsjInput(usj); - }, [book, cachedData]); + !loading && showSnackbar(t('dynamic-msg-load-ref-bible-snack', { refName: book.toUpperCase() }), 'success'); + }, [cachedData, loading]); useEffect(() => { setScrRef({ @@ -93,6 +110,7 @@ export default function TextEditor() { handleEditorFontSize, bookAvailable, booksInProject, + parseError, }; const props = { @@ -114,10 +132,12 @@ export default function TextEditor() { ) : ( <> - {!bookAvailable && } - {bookAvailable && usjInput && } + {parseError && } + {!parseError && !bookAvailable && } + {!parseError && bookAvailable && usjInput && } )} + ); } diff --git a/renderer/src/components/SnackBar/AutoSnackBar.js b/renderer/src/components/SnackBar/AutoSnackBar.js new file mode 100644 index 00000000..86cd161d --- /dev/null +++ b/renderer/src/components/SnackBar/AutoSnackBar.js @@ -0,0 +1,157 @@ +import React, { Fragment, useState, useEffect, useRef } from 'react'; +import { XMarkIcon } from '@heroicons/react/24/outline'; +import { Popover, Transition } from '@headlessui/react'; +import PropTypes from 'prop-types'; +import { createRoot } from 'react-dom/client'; + +const colors = { + success: '#82E0AA', + failure: '#F5B7B1', + warning: '#F8C471', + info: '#85C1E9', + update: '#D5D8DC' +}; + +const AutoSnackBar = ({ + snackText, + snackType, + isOpen, + onClose, +}) => { + const handleTransitionEnd = () => { + if (!isOpen) { + onClose(); + } + }; + + return ( + + + +
+ +

+ {snackText} +

+
+
+
+
+ ); +}; + +AutoSnackBar.propTypes = { + snackText: PropTypes.string, + snackType: PropTypes.string, + isOpen: PropTypes.bool, + onClose: PropTypes.func, +}; + +export const useAutoSnackbar = () => { + const [snackbar, setSnackbar] = useState({ + isOpen: false, + snackText: '', + snackType: 'success', + }); + const [timeLeft, setTimeLeft] = useState(null); + const portalRef = useRef(null); + const rootRef = useRef(null); + + useEffect(() => { + if (!portalRef.current) { + portalRef.current = document.createElement('div'); + portalRef.current.id = 'snackbar-portal'; + document.body.appendChild(portalRef.current); + } + + if (!rootRef.current && portalRef.current) { + rootRef.current = createRoot(portalRef.current); + } + + const handleClose = () => { + if (rootRef.current) { + rootRef.current.unmount(); + rootRef.current = null; + } + if (portalRef.current && document.body.contains(portalRef.current)) { + document.body.removeChild(portalRef.current); + portalRef.current = null; + } + }; + + if (snackbar.isOpen && rootRef.current) { + rootRef.current.render( + { + setSnackbar(prev => ({ ...prev, isOpen: false })); + setTimeLeft(null); + handleClose(); + }} + /> + ); + } + + return () => { + if (!snackbar.isOpen) { + handleClose(); + } + }; + }, [snackbar]); + + useEffect(() => { + if (timeLeft === 0) { + setTimeLeft(null); + setSnackbar(prev => ({ ...prev, isOpen: false })); + } + + if (!timeLeft) return; + + const intervalId = setInterval(() => { + setTimeLeft(timeLeft - 1); + }, 1000); + + return () => clearInterval(intervalId); + }, [timeLeft]); + + const showSnackbar = (text, type = 'success') => { + if (rootRef.current) { + rootRef.current.unmount(); + rootRef.current = null; + } + if (portalRef.current && document.body.contains(portalRef.current)) { + document.body.removeChild(portalRef.current); + portalRef.current = null; + } + + setSnackbar({ + snackText: text, + snackType: type, + isOpen: true, + }); + setTimeLeft(type === 'failure' ? 15 : 8); + }; + + return { showSnackbar }; +}; \ No newline at end of file diff --git a/renderer/src/components/SnackBar/index.js b/renderer/src/components/SnackBar/index.js index 04948b4c..3c8ad956 100644 --- a/renderer/src/components/SnackBar/index.js +++ b/renderer/src/components/SnackBar/index.js @@ -1 +1,2 @@ export { default as SnackBar } from './SnackBar'; +export { useAutoSnackbar } from './AutoSnackBar';