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';