diff --git a/client-side/package.json b/client-side/package.json
index b258e36ae..93f0681d6 100644
--- a/client-side/package.json
+++ b/client-side/package.json
@@ -13,13 +13,13 @@
"@mui/x-charts": "^7.10.0",
"@mui/x-data-grid": "^7.10.0",
"@mui/x-date-pickers": "^7.11.0",
+ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@popperjs/core": "^2.11.8",
"@reduxjs/toolkit": "^2.2.7",
- "ajv": "^8.17.1",
- "ajv-keywords": "^5.1.0",
- "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
"@testing-library/user-event": "^13.5.0",
"@types/redux": "^3.6.31",
+ "ajv": "^8.17.1",
+ "ajv-keywords": "^5.1.0",
"axios": "^1.7.2",
"dayjs": "^1.11.12",
"dotenv": "^16.4.5",
@@ -39,8 +39,8 @@
"react-google-recaptcha": "^3.1.0",
"react-i18next": "^15.0.1",
"react-redux": "^9.1.2",
- "react-router": "^6.24.1",
"react-refresh": "^0.14.2",
+ "react-router": "^6.24.1",
"react-router-dom": "^6.26.0",
"redux": "^5.0.1",
"styled-components": "^6.1.11",
@@ -49,10 +49,13 @@
},
"devDependencies": {
"@babel/core": "^7.25.2",
+ "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/plugin-transform-private-property-in-object": "^7.24.7",
"@babel/preset-env": "^7.25.3",
"@babel/preset-react": "^7.24.7",
+ "@chromatic-com/storybook": "^1.6.0",
"@eslint/compat": "^1.1.1",
+ "@eslint/js": "^9.6.0",
"@storybook/addon-actions": "^8.1.11",
"@storybook/addon-essentials": "^8.1.11",
"@storybook/addon-interactions": "^8.1.11",
@@ -73,20 +76,17 @@
"eslint-plugin-react": "^7.35.0",
"eslint-plugin-storybook": "^0.8.0",
"globals": "^15.9.0",
+ "install": "^0.13.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"notistack": "^3.0.1",
+ "npm": "^10.8.2",
"prop-types": "^15.8.1",
+ "react-scripts": "^5.0.1",
"redux-mock-store": "^1.5.4",
"sass": "^1.77.6",
- "webpack": "^5.92.1",
- "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
- "@chromatic-com/storybook": "^1.6.0",
- "@eslint/js": "^9.6.0",
- "install": "^0.13.0",
- "npm": "^10.8.2",
- "react-scripts": "^5.0.1",
- "storybook": "^8.1.11"
+ "storybook": "^8.1.11",
+ "webpack": "^5.92.1"
},
"scripts": {
"start": "react-scripts start",
diff --git a/client-side/src/components/settings/Notifications.jsx b/client-side/src/components/settings/Notifications.jsx
index 3eebf13c3..170c78be2 100644
--- a/client-side/src/components/settings/Notifications.jsx
+++ b/client-side/src/components/settings/Notifications.jsx
@@ -16,11 +16,11 @@ const Notifications = ({ onUpdate, data }) => {
const { t: translate } = useTranslation();
const { user } = useSelector(selectAuth);
- const notificationTime = (data?.sendNotificationTime || user.preference?.sendNotificationTime || 10);
- const initialSoundVoice = (data?.soundVoice || user.preference?.soundVoice || "alertSound.mp3");
- const showIncomeMessages = (data?.displayIncomeMessages || user.preference?.displayIncomeMessages || false);
- const showBrowsingTimeLimit = (data?.displayBrowsingTimeLimit || user.preference?.displayBrowsingTimeLimit || false);
- const initialEmailFrequency = (data?.emailFrequency || user.preference?.emailFrequency || EMAIL_FREQUENCY_ENUM.NEVER);
+ const notificationTime = (data?.sendNotificationTime || user?.preference?.sendNotificationTime || 10);
+ const initialSoundVoice = (data?.soundVoice || user?.preference?.soundVoice || "alertSound.mp3");
+ const showIncomeMessages = (data?.displayIncomeMessages || user?.preference?.displayIncomeMessages || false);
+ const showBrowsingTimeLimit = (data?.displayBrowsingTimeLimit || user?.preference?.displayBrowsingTimeLimit || false);
+ const initialEmailFrequency = (data?.emailFrequency || user?.preference?.emailFrequency || EMAIL_FREQUENCY_ENUM.NEVER);
const [emailFrequency, setEmailFrequency] = useState(initialEmailFrequency);
const [ringtoneFile, setRingtoneFile] = useState({ name: initialSoundVoice });
@@ -30,15 +30,13 @@ const Notifications = ({ onUpdate, data }) => {
const [displayBrowsingTimeLimit, setDisplayBrowsingTimeLimit] = useState(showBrowsingTimeLimit);
const [prevValues] = useState({
- emailFrequency: user.preference.emailFrequency,
- ringtoneFile: { name: user.preference.soundVoice },
- sendNotificationTime: user.preference.sendNotificationTime,
- displayIncomeMessages: user.preference.displayIncomeMessages,
- displayBrowsingTimeLimit: user.preference.displayBrowsingTimeLimit
+ emailFrequency: user?.preference.emailFrequency,
+ ringtoneFile: { name: user?.preference.soundVoice },
+ sendNotificationTime: user?.preference.sendNotificationTime,
+ displayIncomeMessages: user?.preference.displayIncomeMessages,
+ displayBrowsingTimeLimit: user?.preference.displayBrowsingTimeLimit
});
-
-
const emailFrequencyOptions = Object.keys(EMAIL_FREQUENCY_ENUM).map(key => ({
text: translate(key.toLowerCase()),
value: EMAIL_FREQUENCY_ENUM[key]
@@ -47,7 +45,7 @@ const Notifications = ({ onUpdate, data }) => {
useEffect(() => {
if (prevValues.emailFrequency !== emailFrequency) {
onUpdate({ emailFrequency });
- }else {
+ } else if (data && 'emailFrequency' in data) {
onUpdate({ emailFrequency: undefined });
}
}, [emailFrequency]);
@@ -55,7 +53,7 @@ const Notifications = ({ onUpdate, data }) => {
useEffect(() => {
if (prevValues.sendNotificationTime != sendNotificationTime) {
onUpdate({ sendNotificationTime });
- }else {
+ } else if (data && 'sendNotificationTime' in data) {
onUpdate({ sendNotificationTime: undefined });
}
}, [sendNotificationTime]);
@@ -64,7 +62,7 @@ const Notifications = ({ onUpdate, data }) => {
if (prevValues.displayIncomeMessages !== displayIncomeMessages) {
onUpdate({ displayIncomeMessages });
}
- else {
+ else if (data && 'displayIncomeMessages' in data) {
onUpdate({ displayIncomeMessages: undefined });
}
}, [displayIncomeMessages]);
@@ -72,7 +70,7 @@ const Notifications = ({ onUpdate, data }) => {
useEffect(() => {
if (prevValues.displayBrowsingTimeLimit !== displayBrowsingTimeLimit) {
onUpdate({ displayBrowsingTimeLimit });
- }else {
+ } else if (data && 'displayBrowsingTimeLimit' in data) {
onUpdate({ displayBrowsingTimeLimit: undefined });
}
}, [displayBrowsingTimeLimit]);
@@ -87,7 +85,7 @@ const Notifications = ({ onUpdate, data }) => {
audioElement.load();
}
return () => URL.revokeObjectURL(newSoundVoice);
- }else {
+ } else if (data && 'soundVoice' in data) {
onUpdate({ soundVoice: undefined });
}
}, [ringtoneFile]);
@@ -136,6 +134,7 @@ const Notifications = ({ onUpdate, data }) => {
value={emailFrequency}
size='large'
widthOfSelect='11rem'
+ data-testid="select-email-frequency"
/>
@@ -160,9 +159,11 @@ const Notifications = ({ onUpdate, data }) => {
accept='audio/mp3'
className='generic-input-file'
/>
-
+ {soundVoice &&
+ (
+ )}
);
diff --git a/client-side/src/components/settings/Preferences.jsx b/client-side/src/components/settings/Preferences.jsx
index 3570d48bc..aac40355b 100644
--- a/client-side/src/components/settings/Preferences.jsx
+++ b/client-side/src/components/settings/Preferences.jsx
@@ -11,10 +11,12 @@ import CONSTANTS from './constantSetting.js';
import './Preferences.scss';
const createTimeZones = () => {
- return moment.tz.names().map(timezone => ({
- value: timezone,
- text: timezone,
- }));
+ return moment.tz.names()
+ .filter(timezone => timezone.startsWith('Etc'))
+ .map(timezone => ({
+ value: timezone.replace('Etc/', ''),
+ text: timezone.replace('Etc/', ''),
+ }));
};
const Preferences = ({ onUpdate, data }) => {
@@ -27,11 +29,11 @@ const Preferences = ({ onUpdate, data }) => {
text: LANGUAGE[key]['text'],
iconSrc: LANGUAGE[key]['icon']
}));
- const initialLanguage = (data?.language || user.preference?.language || languageOptions[0].value);
+ const initialLanguage = (data?.language || user?.preference?.language || languageOptions[1].value);
const timeZoneOptions = createTimeZones();
const defaultTimeZone = timeZoneOptions.find(option => option.value === "UTC")?.value || timeZoneOptions[0].value;
- const initialTimeZone = (data?.timeZone || user.preference?.timeZone || defaultTimeZone);
- const initialDateFormat = (data?.dateFormat || user.preference?.dateFormat || DATE_FORMATS[0].value);
+ const initialTimeZone = (data?.timeZone || user?.preference?.timeZone || defaultTimeZone);
+ const initialDateFormat = (data?.dateFormat || user?.preference?.dateFormat || DATE_FORMATS[0].value);
const [language, setLanguage] = useState(initialLanguage);
const [timeZone, setTimeZone] = useState(initialTimeZone);
const [dateFormat, setDateFormat] = useState(initialDateFormat);
@@ -41,18 +43,17 @@ const Preferences = ({ onUpdate, data }) => {
value: value
}));
- const [prevValues, setPrevValues] = useState({
- language,
- timeZone,
- dateFormat
+ const [prevValues] = useState({
+ language:user?.preference.language,
+ timeZone:user?.preference.timeZone,
+ dateFormat:user?.preference.dateFormat
});
useEffect(() => {
if (prevValues.language !== language) {
onUpdate({ language });
- setPrevValues(prev => ({ ...prev, language }));
}
- else {
+ else if(data && 'language' in data){
onUpdate({ language: undefined });
}
}, [language]);
@@ -60,8 +61,7 @@ const Preferences = ({ onUpdate, data }) => {
useEffect(() => {
if (prevValues.timeZone !== timeZone) {
onUpdate({ timeZone });
- setPrevValues(prev => ({ ...prev, timeZone }));
- }else {
+ }else if(data && 'timeZone' in data){
onUpdate({ timeZone: undefined });
}
}, [timeZone]);
@@ -69,9 +69,9 @@ const Preferences = ({ onUpdate, data }) => {
useEffect(() => {
if (prevValues.dateFormat !== dateFormat) {
onUpdate({ dateFormat });
- setPrevValues(prev => ({ ...prev, dateFormat }));
- }else {
+ }else if(data && 'dateFormat' in data){
onUpdate({ dateFormat: undefined });
+
}
}, [dateFormat]);
@@ -95,6 +95,7 @@ const Preferences = ({ onUpdate, data }) => {
widthOfSelect='11rem'
value={language}
onChange={handleLanguageChange}
+ data-testid="select-language"
/>
@@ -106,6 +107,7 @@ const Preferences = ({ onUpdate, data }) => {
value={timeZone}
size='large'
widthOfSelect='11rem'
+ data-testid="select-time-zone"
/>
@@ -117,6 +119,7 @@ const Preferences = ({ onUpdate, data }) => {
title={translate(LABELS.SELECT_DATE_FORMAT)}
size='large'
widthOfSelect='11rem'
+ data-testid="select-date-format"
/>
diff --git a/client-side/src/components/settings/Settings.jsx b/client-side/src/components/settings/Settings.jsx
index 9677cc71a..d4842cec1 100644
--- a/client-side/src/components/settings/Settings.jsx
+++ b/client-side/src/components/settings/Settings.jsx
@@ -6,11 +6,9 @@ import { useSelector, useDispatch } from 'react-redux';
import VerticalTabs from '../../stories/verticalTabs/verticalTabss';
import GenericButton from '../../stories/Button/GenericButton.jsx';
import ToastMessage from '../../stories/Toast/ToastMessage.jsx';
-import { updatePreference as updatePreferenceService } from '../../services/preferenceService.js';
-import { updatePreference } from '../../redux/preference/preference.slice.js';
+import { updatePreference } from '../../services/preferenceService.js';
import { updateCurrentUser } from '../../redux/auth/auth.slice.js';
import { selectAuth } from '../../redux/auth/auth.selector.js';
-import { selectPreference } from '../../redux/preference/preference.selector.js';
import Preferences from './Preferences.jsx';
import AccountTab from './AccountTab.jsx';
@@ -24,13 +22,12 @@ const Settings = () => {
const { t: translate } = useTranslation();
const { enqueueSnackbar } = useSnackbar();
const { user } = useSelector(selectAuth);
- const updatedPreferences = useSelector(selectPreference);
const dispatch = useDispatch();
const [notificationsData, setNotificationsData] = useState({});
const [preferencesData, setPreferencesData] = useState({});
- const preferenceId = user.preference._id;
+ const preferenceId = user?.preference._id;
const handleUpdatePreferences = (updatedPreferences) => {
setPreferencesData(prev => {
@@ -63,10 +60,9 @@ const Settings = () => {
if (value !== undefined) formData.append(key, value);
});
try {
- const response = await updatePreferenceService(preferenceId, formData);
+ const response = await updatePreference(preferenceId, formData);
if (response) {
enqueueSnackbar();
- dispatch(updatePreference(response));
dispatch(updateCurrentUser({
...user,
preference: response
diff --git a/client-side/src/components/settings/UserLocalization.jsx b/client-side/src/components/settings/UserLocalization.jsx
new file mode 100644
index 000000000..184bf05a1
--- /dev/null
+++ b/client-side/src/components/settings/UserLocalization.jsx
@@ -0,0 +1,82 @@
+import React, { useState, useEffect, useRef } from 'react';
+import axios from 'axios';
+
+const Localization = ({ preferenceId }) => {
+ const [timeZone, setTimeZone] = useState('UTC');
+ const [language, setLanguage] = useState('en');
+
+ const baseUrl = process.env.REACT_APP_BASE_URL;
+ const permissionRef = useRef(false);
+
+ useEffect(() => {
+
+ const askForPermission = async () => {
+ console.log('askForPermission executed');
+ const permission = window.confirm('Allow us to get your location and preferred language for a better experience?');
+ permissionRef.current = true;
+
+ if (permission) {
+ // Get time zone
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ (position) => {
+ const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
+ setTimeZone(userTimeZone);
+
+ // Get language
+ const userLanguage = navigator.languages && navigator.languages.length ? navigator.languages[0] : navigator.language || 'en';
+ setLanguage(userLanguage);
+
+ // Update preferences with user's data
+ updateUserPreferences(userTimeZone, userLanguage);
+ },
+ (error) => {
+ console.error('Error getting location:', error);
+ // Since geolocation failed, update with defaults
+ updateUserPreferences(timeZone, language);
+ }
+ );
+ } else {
+ console.error('Geolocation is not supported by this browser.');
+ // Update with defaults if geolocation not supported
+ updateUserPreferences(timeZone, language);
+ }
+ } else {
+ // Permission denied, update with defaults
+ updateUserPreferences(timeZone, language);
+ }
+ };
+
+ // Only ask for permission if it hasn't been granted before
+ if (permissionRef.current == false) {
+ askForPermission();
+ }
+ }, []);
+
+ const updateUserPreferences = async (updatedTimeZone, updatedLanguage) => {
+ try {
+ const formData = new FormData();
+ formData.append('timeZone', updatedTimeZone);
+ formData.append('language', updatedLanguage);
+
+ const response = await axios.put(`${baseUrl}/preferences/${preferenceId}`, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ }
+ });
+ console.log(response.data);
+ } catch (error) {
+ console.error('Error updating preferences:', error);
+ }
+ };
+
+ return (
+
+
Localization Settings
+
Time Zone: {timeZone}
+
Preferred Language: {language}
+
+ );
+};
+
+export default Localization;
diff --git a/client-side/src/components/signUp.jsx b/client-side/src/components/signUp.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/client-side/src/i18n.js b/client-side/src/i18n.js
index 29ccbc407..8c3c41882 100644
--- a/client-side/src/i18n.js
+++ b/client-side/src/i18n.js
@@ -8,7 +8,8 @@ i18n
.use(languageDetector)
.use(Backend)
.init({
- debug: true,
+ debug: true,
+ lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
diff --git a/client-side/src/stories/Select/Select.jsx b/client-side/src/stories/Select/Select.jsx
index dfd0eee54..a0caafc66 100644
--- a/client-side/src/stories/Select/Select.jsx
+++ b/client-side/src/stories/Select/Select.jsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {Box,InputLabel,MenuItem,FormControl}from '@mui/material';
+import { Box, InputLabel, MenuItem, FormControl } from '@mui/material';
import SelectMui from '@mui/material/Select';
import PropTypes from 'prop-types';
@@ -9,25 +9,29 @@ import './select.scss';
const Select = ({
className,
options = OPTION_SELSCT,
- onChange = () => {},
+ onChange = () => { },
title,
size = 'large',
- widthOfSelect,
- value
+ widthOfSelect,
+ value,
+ 'data-testid': dataTestId
}) => {
return (
{title}
- onChange(event.target.value)}
value={value}
+ inputProps={{ 'data-testid': dataTestId }}
>
{options.map((option, index) => (
-