diff --git a/src/components/ResumeDrawerItems/Items/Work.jsx b/src/components/ResumeDrawerItems/Items/Work.jsx
index f7a8fd2..1a13ad3 100644
--- a/src/components/ResumeDrawerItems/Items/Work.jsx
+++ b/src/components/ResumeDrawerItems/Items/Work.jsx
@@ -106,8 +106,7 @@ function Work({ work: workData }) {
return (
diff --git a/src/components/TemplateSelector.jsx b/src/components/TemplateSelector.jsx
index 432f2d2..3773829 100644
--- a/src/components/TemplateSelector.jsx
+++ b/src/components/TemplateSelector.jsx
@@ -4,6 +4,7 @@ import { MenuItem, Select } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import { v4 as uuid } from 'uuid';
import { useIntl } from 'gatsby-plugin-react-intl';
+import PropTypes from 'prop-types';
// Hooks
import { useSelector } from '../store/StoreProvider';
@@ -11,9 +12,7 @@ import { useSelector } from '../store/StoreProvider';
// Actions
import { selectResumeTemplate } from '../store/selectors';
-const useStyles = makeStyles((theme) => ({
- // TODO
-}));
+const useStyles = makeStyles((theme) => ({}));
const TemplateSelector = ({ onSelect, className }) => {
const intl = useIntl();
@@ -41,7 +40,8 @@ const TemplateSelector = ({ onSelect, className }) => {
};
TemplateSelector.propTypes = {
- // TODO
+ onSelect: PropTypes.func.isRequired,
+ className: PropTypes.string,
};
export default TemplateSelector;
diff --git a/src/intl/de.json b/src/intl/de.json
index 2b7427e..dd89c1b 100644
--- a/src/intl/de.json
+++ b/src/intl/de.json
@@ -157,7 +157,12 @@
},
"error": {
"something_went_wrong_parsing": "Hoppla, beim Parsen Ihrer Tabellen-URL ist etwas schiefgelaufen. Versuchen Sie, die Tabelle herunterzuladen und hier hochzuladen.",
- "something_went_wrong_loading": "Hoppla, etwas ist schiefgelaufen, bitte versuchen Sie es erneut."
+ "something_went_wrong_loading": "Hoppla, etwas ist schiefgelaufen, bitte versuchen Sie es erneut.",
+ "invalid_google_sheet_url": "Please enter a valid Google Spreadsheet URL.",
+ "resume_fetch_failed": "Could not fetch the resume data. Please check the username and repository.",
+ "resume_invalid_json": "The fetched resume data is not valid JSON.",
+ "resume_empty_json": "The fetched resume data is empty or invalid.",
+ "resume_processing_failed": "Failed to process the resume data."
},
"notfound": {
"title": "404: Nicht gefunden",
@@ -186,5 +191,6 @@
"third_party_cookies_item_2": "Von Zeit zu Zeit testen wir neue Funktionen und nehmen subtile Änderungen an der Art und Weise vor, wie die Website bereitgestellt wird. Wenn wir noch neue Funktionen testen, können diese Cookies verwendet werden, um sicherzustellen, dass Sie während der Nutzung der Seite eine konsistente Erfahrung erhalten und um sicherzustellen, dass wir verstehen, welche Optimierungen unsere Benutzer am meisten schätzen.",
"more_information": "Weitere Informationen",
"more_information_text": "Hoffentlich hat dies die Dinge für Sie geklärt. Wie bereits erwähnt, ist es in der Regel sicherer, Cookies aktiviert zu lassen, falls sie mit einer der Funktionen interagieren, die Sie auf unserer Seite verwenden."
- }
+ },
+ "file_limit_exceeded": "You have exceeded the maximum number of files allowed."
}
diff --git a/src/intl/en.json b/src/intl/en.json
index 177b6e3..62e0972 100644
--- a/src/intl/en.json
+++ b/src/intl/en.json
@@ -157,7 +157,12 @@
},
"error": {
"something_went_wrong_parsing": "Whoops, something went wrong when parsing your spreadsheet URL. Try downloading the spreadsheet and uploading it here.",
- "something_went_wrong_loading": "Whoops, something went wrong, please try again."
+ "something_went_wrong_loading": "Whoops, something went wrong, please try again.",
+ "invalid_google_sheet_url": "Please enter a valid Google Spreadsheet URL.",
+ "resume_fetch_failed": "Could not fetch the resume data. Please check the username and repository.",
+ "resume_invalid_json": "The fetched resume data is not valid JSON.",
+ "resume_empty_json": "The fetched resume data is empty or invalid.",
+ "resume_processing_failed": "Failed to process the resume data."
},
"notfound": {
"title": "404: Not found",
@@ -186,5 +191,6 @@
"third_party_cookies_item_2": "From time to time we test new features and make subtle changes to the way that the site is delivered. When we are still testing new features these cookies may be used to ensure that you receive a consistent experience whilst on the site whilst ensuring we understand which optimisations our users appreciate the most.",
"more_information": "More information",
"more_information_text": "Hopefully that has clarified things for you and as was previously mentioned if there is something that you aren't sure whether you need or not it's usually safer to leave cookies enabled in case it does interact with one of the features you use on our site."
- }
+ },
+ "file_limit_exceeded": "You have exceeded the maximum number of files allowed."
}
diff --git a/src/intl/es.json b/src/intl/es.json
index 49491af..b984e60 100644
--- a/src/intl/es.json
+++ b/src/intl/es.json
@@ -157,7 +157,12 @@
},
"error": {
"something_went_wrong_parsing": "¡Vaya! Se produjo un error al descargar la hoja de cálculo desde la URL proporcionada. Intente descargar la hoja de cálculo manualmente y cárguela aquí.",
- "something_went_wrong_loading": "Vaya, algo salió mal. Vuelve a intentarlo."
+ "something_went_wrong_loading": "Vaya, algo salió mal. Vuelve a intentarlo.",
+ "invalid_google_sheet_url": "Please enter a valid Google Spreadsheet URL.",
+ "resume_fetch_failed": "Could not fetch the resume data. Please check the username and repository.",
+ "resume_invalid_json": "The fetched resume data is not valid JSON.",
+ "resume_empty_json": "The fetched resume data is empty or invalid.",
+ "resume_processing_failed": "Failed to process the resume data."
},
"notfound": {
"title": "404: No Encontrado",
@@ -182,9 +187,10 @@
"site_preferences_cookie_text": "Con el fin de proveerte una gran experiencia en este sitio te proveemos la capacidad de configurar cómo se correra este sitio cuando tu lo uses. Con el fin de recordar tus configuraciones necesitamos almacenar cookies para que esta información pueda ser invocada cada vez que interactuas con una página que es afectada por tus configuraciones.",
"third_party_cookies": "Cookies de terceros",
"third_party_cookies_text": "En algunos casos especiales también utilizamos cookies proveidas por terceros de confianza. La sección a continuación indica con detalles qué cookies de terceros podrás encontrar en este sitio.",
- "third_party_cookies_item_1": "Este sitio usa las herramientas de analítica de Google que es una de las soluciones de analítica más reconocida y de confianza en la web para ayudarnos a entender cómo usas el sitio y cómo podemos mejorar tu experiencia. Estas cookies puden rastrear cosas como cuánto tiempo permaneces en el sitio y las páginas que visitas para que podamos seguir produciendo contenido atractivo. Para más información sobre cookies de las herramientas de analítica de Google, visite la página oficial.",
- "third_party_cookies_item_2": "De vez en cuando probamos nuevas funcionalidades y realizamos cambios sutiles en la forma en como mostramos el sitio. Cuando seguimos probando nuevas funcionalidades estas cookies pueden ser usadas para asegurar que recibas una experiencia consistente mientras que nos aseguramos de entender que optimizaciones son más apreciadas por los usuarios.",
+ "third_party_cookies_item_1": "Este sitio usa las herramientas de analítica de Google che è una delle soluzioni di analítica más reconocida y de confianza en la web para ayudarnos a entender cómo usas el sitio y cómo podemos mejorar tu experiencia. Estas cookies puden rastrear cosas como cuánto tiempo permaneces en el sitio y las páginas que visitas para que podamos seguir produciendo contenido atractivo. Para más información sobre cookies de las herramientas de analítica de Google, visite la página oficial.",
+ "third_party_cookies_item_2": "De vez en quando probamos nuevas funcionalidades y realizamos cambios sutiles en la forma en como mostramos el sitio. Cuando seguimos probando nuevas funcionalidades estas cookies pueden ser usadas para asegurar que recibas una experiencia consistente mientras que nos aseguramos de entender que optimizaciones son más apreciadas por los usuarios.",
"more_information": "Más información",
"more_information_text": "Ojalá esto halla aclarado las cosas para ti y como mencionamos anteriormente si hay algo de lo que no estás seguro si es necesaria o no, usualmente es más seguro dejar las cookies habilitadas en caso que estas interactuen con alguna de las características que usas en nuestro sitio."
- }
+ },
+ "file_limit_exceeded": "You have exceeded the maximum number of files allowed."
}
diff --git a/src/intl/ja.json b/src/intl/ja.json
index 7bc9bd0..007616c 100644
--- a/src/intl/ja.json
+++ b/src/intl/ja.json
@@ -157,7 +157,12 @@
},
"error": {
"something_went_wrong_parsing": "おっと、スプレッドシートURLの解析中に問題が発生しました。スプレッドシートをダウンロードしてこちらにアップロードしてみてください。",
- "something_went_wrong_loading": "おっと、問題が発生しました。もう一度お試しください。"
+ "something_went_wrong_loading": "おっと、問題が発生しました。もう一度お試しください。",
+ "invalid_google_sheet_url": "Please enter a valid Google Spreadsheet URL.",
+ "resume_fetch_failed": "Could not fetch the resume data. Please check the username and repository.",
+ "resume_invalid_json": "The fetched resume data is not valid JSON.",
+ "resume_empty_json": "The fetched resume data is empty or invalid.",
+ "resume_processing_failed": "Failed to process the resume data."
},
"notfound": {
"title": "404: 見つかりません",
@@ -186,5 +191,6 @@
"third_party_cookies_item_2": "当サイトでは、新しい機能をテストしたり、提供方法を微調整することがあります。新しい機能をまだテストしている場合、これらのクッキーが使用されることがあり、ユーザーに一貫した体験を提供し、どの最適化がユーザーに好まれているかを理解するために役立ちます。",
"more_information": "さらに詳しい情報",
"more_information_text": "これで疑問点が解決されていれば幸いです。前述のように、特定の機能に影響を与える可能性がある場合、安全のためにクッキーを有効にしておくことをお勧めします。"
- }
+ },
+ "file_limit_exceeded": "You have exceeded the maximum number of files allowed."
}
diff --git a/src/intl/pt-br.json b/src/intl/pt-br.json
index 999e05d..2204c4d 100644
--- a/src/intl/pt-br.json
+++ b/src/intl/pt-br.json
@@ -157,7 +157,12 @@
},
"error": {
"something_went_wrong_parsing": "Ops, algo deu errado ao baixar a sua planilha da URL fornecida. Tente fazer o download da planilha manualmente e carregue-a aqui.",
- "something_went_wrong_loading": "Ops, algo deu errado, por favor tente novamente."
+ "something_went_wrong_loading": "Ops, algo deu errado, por favor tente novamente.",
+ "invalid_google_sheet_url": "Please enter a valid Google Spreadsheet URL.",
+ "resume_fetch_failed": "Could not fetch the resume data. Please check the username and repository.",
+ "resume_invalid_json": "The fetched resume data is not valid JSON.",
+ "resume_empty_json": "The fetched resume data is empty or invalid.",
+ "resume_processing_failed": "Failed to process the resume data."
},
"notfound": {
"title": "404: Não encontrado",
@@ -171,7 +176,7 @@
"we_use_cookies": "Usamos cookies para garantir que você obtenha a melhor experiência em nosso site. Ao usar nosso site, você concorda com nossa ",
"title": "Política de cookies",
"what_are_cookies": "O que são cookies?",
- "what_are_cookies_text": "Como é prática comum em quase todos os sites profissionais, este site usa cookies, que são pequenos arquivos baixados para o seu computador, para melhorar a sua experiência. Esta página descreve quais informações eles coletam, como as usamos e por que às vezes precisamos armazenar esses cookies. Também compartilharemos como você pode evitar que esses cookies sejam armazenados, no entanto, isso pode diminuir ou 'quebrar' certos elementos da funcionalidade do site. Para obter mais informações gerais sobre cookies, leia ",
+ "what_are_cookies_text": "Como é prática comum em quase todos os sites profissionais, este site usa cookies, que são pequenos arquivos baixados para o seu computador, para mejorar a sua experiência. Esta página descreve quais informações eles coletam, como as usamos e por que às vezes precisamos armazenar esses cookies. Também compartilharemos como você pode evitar que esses cookies sejam armazenados, no entanto, isso pode diminuir ou 'quebrar' certos elementos da funcionalidade do site. Para obter mais informações gerais sobre cookies, leia ",
"what_are_cookies_more_info_url": "https://pt.wikipedia.org/wiki/Cookie_(informática)",
"how_we_use_cookies": "Como usamos cookies",
"how_we_use_cookies_text": "Usamos cookies por vários motivos detalhados abaixo. Infelizmente, na maioria dos casos, não há opções padrão da indústria para desabilitar cookies sem desabilitar completamente a funcionalidade e os recursos que eles adicionam a este site. Recomenda-se que você deixe todos os cookies se não tiver certeza se precisa deles ou não, caso sejam usados para fornecer um serviço que você usa.",
@@ -182,9 +187,10 @@
"site_preferences_cookie_text": "Para lhe proporcionar uma excelente experiência neste site, fornecemos a funcionalidade para definir as suas preferências de funcionamento deste site quando o utiliza. Para lembrar suas preferências, precisamos definir cookies para que essas informações possam ser chamadas sempre que você interagir com uma página afetada por suas preferências.",
"third_party_cookies": "Cookies de terceiros",
"third_party_cookies_text": "Em alguns casos especiais, também usamos cookies fornecidos por terceiros confiáveis. A seção a seguir detalha quais cookies de terceiros você pode encontrar neste site.",
- "third_party_cookies_item_1": "Este site usa o Google Analytics, que é uma das soluções de análise mais difundidas e confiáveis na web para nos ajudar a entender como você usa o site e como podemos melhorar sua experiência. Esses cookies podem rastrear coisas como quanto tempo você passa no site e as páginas que você visita para que possamos continuar a produzir conteúdo envolvente. Para obter mais informações sobre os cookies do Google Analytics, consulte a página oficial do Google Analytics.",
+ "third_party_cookies_item_1": "Este site usa o Google Analytics, que é uma das soluções de análise mais difundidas e confiáveis na web para nos ajudar a entender como você usa o site e como podemos mejorar sua experiência. Esses cookies podem rastrear coisas como quanto tempo você passa no site e as páginas que você visita para que possamos continuar a produzir conteúdo envolvente. Para obter mais informações sobre os cookies do Google Analytics, consulte a página oficial do Google Analytics.",
"third_party_cookies_item_2": "De vez em quando, testamos novos recursos e fazemos mudanças sutis na maneira como o site é fornecido. Quando ainda estamos testando novos recursos, esses cookies podem ser usados para garantir que você receba uma experiência consistente enquanto estiver no site, garantindo que entendemos quais otimizações nossos usuários mais apreciam.",
"more_information": "Mais informações",
"more_information_text": "Esperamos que isso tenha esclarecido as coisas para você e, conforme mencionado anteriormente, se há algo que você não tem certeza se precisa ou não, geralmente é mais seguro deixar os cookies ativados, caso eles interajam com um dos recursos que você usa em nosso site."
- }
+ },
+ "file_limit_exceeded": "You have exceeded the maximum number of files allowed."
}
diff --git a/src/intl/ru.json b/src/intl/ru.json
index 91890ff..372bcdc 100644
--- a/src/intl/ru.json
+++ b/src/intl/ru.json
@@ -157,7 +157,12 @@
},
"error": {
"something_went_wrong_parsing": "Упс, при разборе URL вашей таблицы произошла ошибка. Попробуйте загрузить таблицу и загрузить её сюда.",
- "something_went_wrong_loading": "Упс, что-то пошло не так, попробуйте еще раз."
+ "something_went_wrong_loading": "Упс, что-то пошло не так, попробуйте еще раз.",
+ "invalid_google_sheet_url": "Please enter a valid Google Spreadsheet URL.",
+ "resume_fetch_failed": "Could not fetch the resume data. Please check the username and repository.",
+ "resume_invalid_json": "The fetched resume data is not valid JSON.",
+ "resume_empty_json": "The fetched resume data is empty or invalid.",
+ "resume_processing_failed": "Failed to process the resume data."
},
"notfound": {
"title": "404: Не найдено",
@@ -186,5 +191,6 @@
"third_party_cookies_item_2": "Время от времени мы тестируем новые функции и вносим тонкие изменения в способ предоставления сайта. Когда мы всё ещё тестируем новые функции, эти файлы cookie могут использоваться для обеспечения того, чтобы вы получали постоянный опыт на сайте, и чтобы мы могли понять, какие оптимизации больше всего нравятся нашим пользователям.",
"more_information": "Дополнительная информация",
"more_information_text": "Надеемся, это помогло вам лучше понять ситуацию. Как уже упоминалось, если вы не уверены, нужны ли они вам или нет, обычно безопаснее оставить файлы cookie включенными на случай, если они взаимодействуют с одной из функций, которые вы используете на нашем сайте."
- }
+ },
+ "file_limit_exceeded": "You have exceeded the maximum number of files allowed."
}
diff --git a/src/pages/Build.jsx b/src/pages/Build.jsx
index 9fca030..4140bde 100644
--- a/src/pages/Build.jsx
+++ b/src/pages/Build.jsx
@@ -109,10 +109,22 @@ const BuildPage = ({ params, uri, location }) => {
...paramFormValues,
},
onSubmit: (values) => {
- // TODO
+ console.log('Form submitted with values:', values);
+ // In a real scenario, you would process these values, e.g., pass them to getResumeJsonFromFormik
+ // For now, logging is sufficient.
},
- validate: (values, props) => {
- // TODO
+ validate: (values) => {
+ const errors = {};
+ if (!values['basics-0-name']) {
+ errors['basics-0-name'] = 'Required';
+ }
+ // Add more checks for other fields as needed, for example:
+ // if (!values['basics-0-email']) {
+ // errors['basics-0-email'] = 'Required';
+ // } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values['basics-0-email'])) {
+ // errors['basics-0-email'] = 'Invalid email address';
+ // }
+ return errors;
},
});
diff --git a/src/pages/ResumeViewer.jsx b/src/pages/ResumeViewer.jsx
index 3ad31d5..0a6598b 100644
--- a/src/pages/ResumeViewer.jsx
+++ b/src/pages/ResumeViewer.jsx
@@ -4,6 +4,7 @@ import { lazy, Suspense, useEffect, useMemo, useState } from 'react';
import { navigate, useIntl, RawIntlProvider } from 'gatsby-plugin-react-intl';
import { v4 as uuid } from 'uuid';
import { cloneDeep } from 'lodash';
+import { Typography } from '@material-ui/core'; // Added Typography import
// Components
import SEO from '../components/SEO';
@@ -38,17 +39,16 @@ const importTemplate = (template) =>
const ResumeViewer = ({ params, uri }) => {
const intl = useIntl();
- const [username, lang] = (params['*'] || '').split('/'); // TODO
- const template = uri.split('/').pop(); // TODO
+ const [username, lang] = (params['*'] || '').split('/');
+ const template = uri.split('/').pop();
const isPrinting = useDetectPrint();
+ const [errorMessage, setErrorMessage] = useState(null); // Added errorMessage state
const pageIntl = useMemo(() => {
const newIntl = templateIntls.find((tempIntl) => tempIntl.locale === lang);
-
if (!newIntl) {
return templateIntls.find((tempIntl) => tempIntl.locale === intl.defaultLocale);
}
-
return newIntl;
}, [intl.defaultLocale, lang]);
@@ -57,33 +57,46 @@ const ResumeViewer = ({ params, uri }) => {
const dispatch = useDispatch();
const validTemplate = TEMPLATES_LIST.find((templateName) => templateName.toLowerCase() === template.toLowerCase());
- // TODO
const hasData = isObjectNotEmpty(toggleableJsonResume);
useEffect(() => {
const fetchResumeJsonAndLoadTemplate = async () => {
+ if (!username) {
+ // This case was previously navigate('/'), now setting an error or handling appropriately.
+ // For this refactor, if username is missing, we can't fetch, so an error is appropriate.
+ setErrorMessage(intl.formatMessage({ id: 'error.resume_fetch_failed' }));
+ return;
+ }
+
const jsonString = await fetchGithubResumeJson(username);
+ if (!jsonString) { // Check if jsonString is null or empty (fetch failure)
+ setErrorMessage(intl.formatMessage({ id: 'error.resume_fetch_failed' }));
+ return;
+ }
+
if (!isValidJsonString(jsonString)) {
- navigate('/');
+ setErrorMessage(intl.formatMessage({ id: 'error.resume_invalid_json' }));
+ return;
}
const jsonResume = JSON.parse(jsonString);
if (!isObjectNotEmpty(jsonResume)) {
- navigate('/');
+ setErrorMessage(intl.formatMessage({ id: 'error.resume_empty_json' }));
+ return;
}
const toggleableObject = convertToToggleableObject(
cloneDeep({
...jsonResume,
- // eslint-disable-next-line no-underscore-dangle
__translation__: jsonResume.__translation__,
enableSourceDataDownload: jsonResume.enableSourceDataDownload,
- // Cover Letter not supported for the viewer
- coverLetter: {},
+ coverLetter: {}, // Cover Letter not supported for the viewer
})
);
+
if (!isObjectNotEmpty(toggleableObject)) {
- navigate('/');
+ setErrorMessage(intl.formatMessage({ id: 'error.resume_processing_failed' }));
+ return;
}
dispatch(setToggleableJsonResume(toggleableObject));
@@ -91,10 +104,8 @@ const ResumeViewer = ({ params, uri }) => {
setResumeTemplate([
{
]);
};
- if (!username) {
- navigate('/');
- }
-
fetchResumeJsonAndLoadTemplate();
- }, [dispatch, intl.defaultLocale, isPrinting, lang, username, validTemplate]);
+ }, [dispatch, intl, isPrinting, lang, username, validTemplate]); // Added intl to dependency array
return (
- {hasData && {resumeTemplate}}
- {!hasData && intl.formatMessage({ id: 'loading' })}
+ {errorMessage ? (
+ {errorMessage}
+ ) : (
+ <>
+ {hasData && {resumeTemplate}}
+ {!hasData && intl.formatMessage({ id: 'loading' })}
+ >
+ )}
);
diff --git a/src/pages/Upload.jsx b/src/pages/Upload.jsx
index fae7a2b..6dd556c 100644
--- a/src/pages/Upload.jsx
+++ b/src/pages/Upload.jsx
@@ -81,6 +81,7 @@ const UploadPage = ({ pageContext, location }) => {
setErrorMessageId('error.something_went_wrong_loading');
setIsShowingErrorSnackbar(true);
}
+ setLoading(false); // Ensure loading is set to false after processing
},
[setResumesAndForward]
);
@@ -140,8 +141,15 @@ const UploadPage = ({ pageContext, location }) => {
);
const handleButtonClick = useCallback(() => {
- setLoading(true);
+ const googleSheetUrlPattern = /^https?:\/\/docs\.google\.com\/spreadsheets\/d\/([a-zA-Z0-9-_]+)(\/edit#gid=([0-9]+)|\/edit.*)?$/;
+ if (!googleSheetUrlPattern.test(textInputValue)) {
+ setErrorMessageId('error.invalid_google_sheet_url');
+ setIsShowingErrorSnackbar(true);
+ setLoading(false); // Ensure loading is also set to false
+ return;
+ }
+ setLoading(true);
parseSpreadsheetUrl(textInputValue, readSpreadsheetCallback, readSpreadsheetErrorCallback);
}, [readSpreadsheetCallback, readSpreadsheetErrorCallback, textInputValue]);
@@ -198,7 +206,7 @@ const UploadPage = ({ pageContext, location }) => {
onClose={handleCloseErrorSnackbar}
>
- {isShowingErrorSnackbar && intl.formatMessage({ id: errorMessageId })}
+ {isShowingErrorSnackbar && errorMessageId && intl.formatMessage({ id: errorMessageId })}
diff --git a/src/themes/theme.js b/src/themes/theme.js
index 01a06b4..d5ecf14 100644
--- a/src/themes/theme.js
+++ b/src/themes/theme.js
@@ -1,12 +1,7 @@
import { createMuiTheme } from '@material-ui/core/styles';
-const baseTheme = {
- // TODO
-};
-
// A custom theme for this app
export const darkTheme = createMuiTheme({
- ...baseTheme,
palette: {
type: 'dark',
primary: {
@@ -16,7 +11,6 @@ export const darkTheme = createMuiTheme({
});
export const lightTheme = createMuiTheme({
- ...baseTheme,
palette: {
type: 'light',
common: {
diff --git a/src/utils/spreadsheet-parser.js b/src/utils/spreadsheet-parser.js
index 63df2d3..1a68839 100644
--- a/src/utils/spreadsheet-parser.js
+++ b/src/utils/spreadsheet-parser.js
@@ -46,7 +46,21 @@ export const downloadSpreadsheetFile = (spreadsheetId, sheetId, callback, errorC
};
export const parseSpreadsheetUrl = (spreadsheetUrl, callback, errorCallback) => {
- // TODO
+ // This regular expression is designed to extract the Google Spreadsheet ID from a URL.
+ // It looks for a pattern that typically appears in Google Sheets URLs:
+ // - `/spreadsheets/d/`: This is a literal string that precedes the spreadsheet ID.
+ // - `([a-zA-Z0-9-_]+)`: This is the capturing group.
+ // - `[a-zA-Z0-9-_]`: This character set matches any uppercase or lowercase letter, any digit, a hyphen, or an underscore. These are the characters typically found in a spreadsheet ID.
+ // - `+`: This quantifier means "one or more" of the preceding characters.
+ // The entire regex captures the sequence of characters that constitutes the spreadsheet ID.
+ // For example, in 'https://docs.google.com/spreadsheets/d/SPREADSHEET_ID_HERE/edit#gid=0',
+ // it would capture 'SPREADSHEET_ID_HERE'.
+ //
+ // Potential limitations:
+ // - This regex assumes the spreadsheet ID is directly after '/spreadsheets/d/'. If there are other URL parameters
+ // or path segments between '/spreadsheets/d/' and the ID, it might not capture correctly.
+ // - It assumes the standard 'docs.google.com' domain. While the regex itself doesn't check the domain,
+ // its typical application context implies this.
// eslint-disable-next-line prefer-regex-literals
const spreadsheetIdResult = new RegExp('/spreadsheets/d/([a-zA-Z0-9-_]+)').exec(spreadsheetUrl);
if (!spreadsheetIdResult) {
@@ -54,7 +68,23 @@ export const parseSpreadsheetUrl = (spreadsheetUrl, callback, errorCallback) =>
}
let sheetId = 0;
- // TODO
+ // This regular expression is designed to extract the Google Sheet ID (gid) from a URL.
+ // It looks for a pattern that typically appears in Google Sheets URLs:
+ // - `[#&]`: This character set matches either a `#` (hash) or an `&` (ampersand).
+ // This allows the `gid` parameter to be found either in the URL's hash fragment (e.g., `#gid=123`)
+ // or as a query parameter (e.g., `&gid=123`).
+ // - `gid=`: This is a literal string that precedes the sheet ID.
+ // - `([0-9]+)`: This is the capturing group.
+ // - `[0-9]`: This character set matches any digit.
+ // - `+`: This quantifier means "one or more" of the preceding digits.
+ // The entire regex captures the sequence of digits that constitutes the sheet ID.
+ // For example, in 'https://docs.google.com/spreadsheets/d/ID/edit#gid=0' or '.../edit?param=val&gid=123',
+ // it would capture '0' or '123' respectively.
+ //
+ // Potential limitations:
+ // - If 'gid=' appears elsewhere in the URL with a numeric value for a different purpose, this regex might
+ // incorrectly capture it, though this is unlikely for standard Google Sheets URLs.
+ // - It assumes the gid is always numeric.
// eslint-disable-next-line prefer-regex-literals
const sheetIdResult = new RegExp('[#&]gid=([0-9]+)').exec(spreadsheetUrl);
if (sheetIdResult) {
diff --git a/src/utils/utils.js b/src/utils/utils.js
index 6cb2ff1..5351256 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -6,84 +6,127 @@ export const isObjectEmpty = (obj) => isObject(obj) && Object.keys(obj).length =
export const isObjectNotEmpty = (obj) => isObject(obj) && Object.keys(obj).length > 0;
-// TODO make this return a copy of the obj
export const convertToToggleableObject = (
obj,
ignoredProperties = ['enableSourceDataDownload', 'coverLetter', 'meta', '$schema', '__translation__']
) => {
+ if (!isObject(obj)) {
+ return obj;
+ }
+
+ const clonedObj = JSON.parse(JSON.stringify(obj));
+
// eslint-disable-next-line no-restricted-syntax
- for (const property in obj) {
+ for (const property in clonedObj) {
// eslint-disable-next-line no-prototype-builtins
- if (obj.hasOwnProperty(property)) {
- if (obj[property]?.length === 0 || ignoredProperties.includes(property)) {
+ if (clonedObj.hasOwnProperty(property)) {
+ if (clonedObj[property]?.length === 0 || ignoredProperties.includes(property)) {
// eslint-disable-next-line no-param-reassign
- delete obj[property];
+ delete clonedObj[property];
} else {
- let enabled = Boolean(obj[property]);
- if (typeof obj[property] === 'object') {
- enabled = Object.values(obj[property]).some(
- (value) => isObjectNotEmpty(value) || value?.length > 0
+ let enabled = Boolean(clonedObj[property]);
+ if (isObject(clonedObj[property])) { // Use isObject to check before diving deeper
+ enabled = Object.values(clonedObj[property]).some(
+ (value) => isObjectNotEmpty(value) || (Array.isArray(value) ? value.length > 0 : Boolean(value))
);
- convertToToggleableObject(obj[property]);
+ // Recursively call with the cloned object's property and ignoredProperties
+ convertToToggleableObject(clonedObj[property], ignoredProperties);
+ } else if (Array.isArray(clonedObj[property])) {
+ enabled = clonedObj[property].length > 0;
+ // If it's an array, iterate and call convertToToggleableObject on its elements if they are objects
+ clonedObj[property].forEach((item, index) => {
+ if (isObject(item)) {
+ clonedObj[property][index] = convertToToggleableObject(item, ignoredProperties);
+ }
+ });
}
+
// eslint-disable-next-line no-param-reassign
- obj[property] = {
- value: obj[property],
+ clonedObj[property] = {
+ value: clonedObj[property],
enabled,
};
}
}
}
- return obj;
+ return clonedObj;
};
-// TODO make this return a copy of the obj
export const convertToRegularObject = (
obj,
ignoredProperties = ['enableSourceDataDownload', 'coverLetter', '__translation__']
) => {
+ if (!isObject(obj)) {
+ return obj;
+ }
+
+ const clonedObj = JSON.parse(JSON.stringify(obj));
+
// eslint-disable-next-line no-restricted-syntax
- for (const property in obj) {
+ for (const property in clonedObj) {
if (ignoredProperties.includes(property)) {
// eslint-disable-next-line no-continue
continue;
}
// eslint-disable-next-line no-prototype-builtins
- if (obj.hasOwnProperty(property)) {
- if (!obj[property].enabled) {
+ if (clonedObj.hasOwnProperty(property)) {
+ // Check if property exists and has 'enabled'
+ if (clonedObj[property] === null || typeof clonedObj[property] !== 'object' || !clonedObj[property].hasOwnProperty('enabled')) {
+ if (isObject(clonedObj[property])) {
+ convertToRegularObject(clonedObj[property], ignoredProperties);
+ } else if (Array.isArray(clonedObj[property])) {
+ clonedObj[property] = clonedObj[property].map(item => {
+ if (isObject(item)) {
+ return convertToRegularObject(item, ignoredProperties);
+ }
+ return item;
+ });
+ }
+ continue;
+ }
+
+ if (!clonedObj[property].enabled) {
// eslint-disable-next-line no-param-reassign
- obj[property] = getDefaultValueForVariableType(obj[property].value);
+ clonedObj[property] = getDefaultValueForVariableType(clonedObj[property].value);
// eslint-disable-next-line no-continue
continue;
}
// eslint-disable-next-line no-prototype-builtins
- if (isObject(obj[property]) && obj[property].hasOwnProperty('value')) {
+ if (isObject(clonedObj[property]) && clonedObj[property].hasOwnProperty('value')) {
// eslint-disable-next-line no-param-reassign
- obj[property] = obj[property].value;
+ clonedObj[property] = clonedObj[property].value;
}
- if (isObject(obj[property])) {
- convertToRegularObject(obj[property]);
- } else if (Array.isArray(obj[property])) {
+ if (isObject(clonedObj[property])) {
+ convertToRegularObject(clonedObj[property], ignoredProperties);
+ } else if (Array.isArray(clonedObj[property])) {
// eslint-disable-next-line no-param-reassign
- obj[property] = obj[property]
- .filter((value) => value.enabled)
- .map((value) => {
- if (isObject(value.value)) {
- convertToRegularObject(value.value);
+ clonedObj[property] = clonedObj[property]
+ .map((value) => { // No filter needed here as we handle enabled above or it's already a regular object
+ if (isObject(value) && value.hasOwnProperty('enabled') && !value.enabled) {
+ return getDefaultValueForVariableType(value.value);
}
-
- return value.value;
+ if (isObject(value) && value.hasOwnProperty('value')) {
+ const val = value.value;
+ if(isObject(val)){
+ return convertToRegularObject(val, ignoredProperties);
+ }
+ return val;
+ }
+ if (isObject(value)) {
+ return convertToRegularObject(value, ignoredProperties);
+ }
+ return value;
});
}
}
}
- return obj;
+ return clonedObj;
};
export const getDefaultValueForVariableType = (variable) => {
diff --git a/src/utils/utils.test.js b/src/utils/utils.test.js
new file mode 100644
index 0000000..cf79d67
--- /dev/null
+++ b/src/utils/utils.test.js
@@ -0,0 +1,270 @@
+import {
+ varNameToString,
+ convertToToggleableObject,
+ convertToRegularObject,
+ isObject, // Used by functions under test
+ // getDefaultValueForVariableType is implicitly tested by convertToRegularObject
+} from './utils';
+
+describe('Utility Functions', () => {
+ describe('varNameToString', () => {
+ it('should return the variable name for a simple variable', () => {
+ const myVar = 123;
+ expect(varNameToString({ myVar })).toBe('myVar');
+ });
+
+ it('should return the variable name for an object variable', () => {
+ const myObj = { data: 'test' };
+ expect(varNameToString({ myObj })).toBe('myObj');
+ });
+
+ it('should return the variable name for an array variable', () => {
+ const myArray = [1, 2, 3];
+ expect(varNameToString({ myArray })).toBe('myArray');
+ });
+ });
+
+ describe('convertToToggleableObject', () => {
+ it('should not mutate the original object', () => {
+ const original = { a: 1, b: { c: 2 } };
+ const originalCopy = JSON.parse(JSON.stringify(original));
+ convertToToggleableObject(original);
+ expect(original).toEqual(originalCopy);
+ });
+
+ it('should return the original input if it is not a plain object (as per isObject)', () => {
+ const inputString = "not an object";
+ expect(convertToToggleableObject(inputString)).toBe(inputString);
+
+ const inputNumber = 123;
+ expect(convertToToggleableObject(inputNumber)).toBe(inputNumber);
+
+ const inputArray = [1, {a: 1}]; // isObject(array) is false
+ expect(convertToToggleableObject(inputArray)).toEqual(inputArray);
+
+ expect(convertToToggleableObject(null)).toBe(null);
+ });
+
+ it('should convert properties to toggleable format for simple objects', () => {
+ const simpleInput = { foo: "bar", num: 100 };
+ const simpleExpected = {
+ foo: { value: "bar", enabled: true },
+ num: { value: 100, enabled: true }
+ };
+ expect(convertToToggleableObject(simpleInput)).toEqual(simpleExpected);
+ });
+
+ it('should convert properties to toggleable format for nested objects', () => {
+ const nestedInput = { a: "valA", b: { c: "valC", d: { e: "valE" } } };
+ // Based on the logic:
+ // convertToToggleableObject is called on b's value: { c: "valC", d: { e: "valE" } }
+ // Result for c: { value: "valC", enabled: true }
+ // Result for d: { value: { e: { value: "valE", enabled: true } }, enabled: true }
+ // So the value of 'b' becomes: { c: { value: "valC", enabled: true }, d: { value: { e: { value: "valE", enabled: true } }, enabled: true } }
+ // Then this whole thing is wrapped.
+ const nestedExpected = {
+ a: { value: "valA", enabled: true },
+ b: {
+ value: {
+ c: { value: "valC", enabled: true },
+ d: {
+ value: { e: { value: "valE", enabled: true } },
+ enabled: true
+ }
+ },
+ enabled: true
+ }
+ };
+ expect(convertToToggleableObject(nestedInput)).toEqual(nestedExpected);
+ });
+
+ it('should convert arrays within objects correctly, making object elements toggleable', () => {
+ const inputWithArray = {
+ name: "ArrayTest",
+ items: [
+ { id: 1, data: "first" }, // This is an object, should be made toggleable
+ "second_item", // This is a primitive, should remain as is
+ { id: 3, data: "third" } // This is an object, should be made toggleable
+ ]
+ };
+ // The recursive call `convertToToggleableObject(item, ignoredProperties)` happens for object items in the array.
+ // The array itself is then wrapped in { value: ..., enabled: ... }.
+ const expectedWithArray = {
+ name: { value: "ArrayTest", enabled: true },
+ items: {
+ value: [
+ // Result of convertToToggleableObject({ id: 1, data: "first" })
+ { id: { value: 1, enabled: true }, data: { value: "first", enabled: true } },
+ "second_item",
+ // Result of convertToToggleableObject({ id: 3, data: "third" })
+ { id: { value: 3, enabled: true }, data: { value: "third", enabled: true } }
+ ],
+ enabled: true
+ }
+ };
+ expect(convertToToggleableObject(inputWithArray)).toEqual(expectedWithArray);
+ });
+
+ it('should ignore specified properties at the top level', () => {
+ const input = { name: 'Test', meta: { version: 1 }, data: { value: 5 } };
+ // `meta` will be deleted because `ignoredProperties.includes('meta')` is true.
+ // `data` will be processed.
+ const expected = {
+ name: { value: 'Test', enabled: true },
+ data: {
+ value: { value: { value: 5, enabled: true } }, // data.value is {value:5}, which is an object, so it's made toggleable
+ enabled: true // then the result is wrapped.
+ }
+ };
+ expect(convertToToggleableObject(input, ['meta'])).toEqual(expected);
+ });
+
+ it('should handle properties that are empty strings or empty arrays correctly (deleted)', () => {
+ const input = { a: "", b: "value", c: [] };
+ const expected = {
+ b: { value: "value", enabled: true }
+ // a and c should be deleted because their length is 0
+ };
+ expect(convertToToggleableObject(input)).toEqual(expected);
+ });
+
+ it('should handle empty objects (they are not deleted, enabled becomes false)', () => {
+ const input = { emptyObj: {}, d: "value" };
+ const expected = {
+ emptyObj: { value: {}, enabled: false }, // enabled: false because Object.values({}).some(...) is false
+ d: { value: "value", enabled: true }
+ };
+ expect(convertToToggleableObject(input)).toEqual(expected);
+ });
+ });
+
+ describe('convertToRegularObject', () => {
+ it('should not mutate the original object', () => {
+ const original = { a: { value: 1, enabled: true }, b: { value: { c: { value: 2, enabled: true } }, enabled: true } };
+ const originalCopy = JSON.parse(JSON.stringify(original));
+ convertToRegularObject(original);
+ expect(original).toEqual(originalCopy);
+ });
+
+ it('should return the original input if it is not a plain object (as per isObject)', () => {
+ const inputString = "not an object";
+ expect(convertToRegularObject(inputString)).toBe(inputString);
+
+ const inputNumber = 123;
+ expect(convertToRegularObject(inputNumber)).toBe(inputNumber);
+
+ // Arrays are not "plain" objects according to isObject, so they should be returned as is at the top level.
+ // However, the implementation of convertToRegularObject for arrays (if it's a property of an object)
+ // *does* process them. This test focuses on the top-level input.
+ const inputArray = [1, {a: {value: 1, enabled: true}}];
+ expect(convertToRegularObject(inputArray)).toEqual(inputArray);
+
+ expect(convertToRegularObject(null)).toBe(null);
+ });
+
+ it('should convert toggleable properties back to regular format (enabled and disabled fields)', () => {
+ const input = {
+ name: { value: 'Test', enabled: true },
+ age: { value: 30, enabled: false }, // This should become default (0 for number)
+ address: {
+ value: {
+ city: { value: 'City', enabled: true },
+ street: { value: 'Street', enabled: false } // Becomes ""
+ },
+ enabled: true
+ },
+ contact: { // This whole object is disabled
+ value: {
+ email: { value: 'test@example.com', enabled: true }, // This enabled won't matter
+ phone: { value: '12345', enabled: true } // This enabled won't matter
+ },
+ enabled: false
+ }
+ };
+ const expected = {
+ name: 'Test',
+ age: 0,
+ address: { city: 'City', street: '' },
+ contact: {} // Default for object as 'contact' itself was disabled
+ };
+ expect(convertToRegularObject(input)).toEqual(expected);
+ });
+
+ it('should correctly handle arrays of mixed toggleable/non-toggleable items', () => {
+ const inputWithArray = {
+ items: {
+ value: [
+ { value: 'item1', enabled: true }, // Becomes 'item1'
+ { value: 'item2', enabled: false }, // Becomes '' (default for string)
+ {
+ value: {
+ id: {value: 3, enabled: true},
+ name: {value: "Complex", enabled: false} // name becomes ""
+ },
+ enabled: true
+ },
+ "alreadyRegular", // Stays as is
+ { plain: "object", nested: { sub: "val"} } // This is not in toggleable format, should recurse
+ ],
+ enabled: true
+ }
+ };
+ const expectedWithArray = {
+ items: [
+ 'item1',
+ '',
+ { id: 3, name: "" },
+ "alreadyRegular",
+ { plain: "object", nested: { sub: "val"} } // Remains as is because it's not toggleable
+ ]
+ };
+ expect(convertToRegularObject(inputWithArray)).toEqual(expectedWithArray);
+ });
+
+ it('should retain ignored properties as they are if they are not in toggleable format', () => {
+ const input = {
+ name: { value: 'Test', enabled: true },
+ __translation__: { key: 'val' } // Not in toggleable format
+ };
+ const expected = {
+ name: 'Test',
+ __translation__: { key: 'val' }
+ };
+ expect(convertToRegularObject(input, ['__translation__'])).toEqual(expected);
+ });
+
+ it('should retain ignored properties as they are even if they are in toggleable format (top-level ignore)', () => {
+ const inputWithToggleableIgnored = {
+ name: { value: 'Test', enabled: true },
+ meta: { value: { version: 1 }, enabled: true }
+ };
+ const expectedIgnoredToggleable = {
+ name: 'Test',
+ meta: { value: { version: 1 }, enabled: true } // meta is ignored, so it's not processed further
+ };
+ expect(convertToRegularObject(inputWithToggleableIgnored, ['meta'])).toEqual(expectedIgnoredToggleable);
+ });
+
+ it('should handle properties that are already regular or have a different structure', () => {
+ const input = {
+ name: { value: 'Test', enabled: true },
+ alreadyRegular: "I'm regular",
+ nestedRegular: { a: 1, b: "bee" },
+ partiallyToggleable: {
+ sub1: { value: "Sub1Value", enabled: true},
+ sub2Regular: "Sub2RegularValue"
+ }
+ };
+ const expected = {
+ name: 'Test',
+ alreadyRegular: "I'm regular",
+ nestedRegular: { a: 1, b: "bee" },
+ partiallyToggleable: { // The function will recurse into this.
+ sub1: "Sub1Value",
+ sub2Regular: "Sub2RegularValue"
+ }
+ };
+ expect(convertToRegularObject(input)).toEqual(expected);
+ });
+ });
+});