Merge pull request #73 from donetick/feature/internationalization-support
Feature/internationalization support
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { AlertsProvider } from '../service/AlertsProvider'
|
||||
import { NotificationProvider } from '../service/NotificationProvider'
|
||||
import { LocalizationProvider } from './LocalizationContext'
|
||||
import QueryContext from './QueryContext'
|
||||
import RouterContext from './RouterContext'
|
||||
import ThemeContext from './ThemeContext'
|
||||
@@ -8,6 +9,7 @@ const Contexts = ({ children }) => {
|
||||
const contexts = [
|
||||
AlertsProvider,
|
||||
ThemeContext,
|
||||
LocalizationProvider,
|
||||
QueryContext,
|
||||
NotificationProvider,
|
||||
RouterContext,
|
||||
|
||||
127
src/contexts/LocalizationContext.jsx
Normal file
127
src/contexts/LocalizationContext.jsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import useStickyState from '@/hooks/useStickyState'
|
||||
import moment from 'moment'
|
||||
import { createContext, useContext, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const LocalizationContext = createContext()
|
||||
|
||||
export const DATE_FORMATS = {
|
||||
MDY: 'MM/DD/YYYY',
|
||||
DMY: 'DD/MM/YYYY',
|
||||
YMD: 'YYYY-MM-DD',
|
||||
LONG: 'MMMM D, YYYY',
|
||||
SHORT: 'MMM D, YYYY',
|
||||
}
|
||||
|
||||
export const TIME_FORMATS = {
|
||||
HOUR_12: 'h:mm A',
|
||||
HOUR_24: 'HH:mm',
|
||||
}
|
||||
|
||||
export const RTL_LANGUAGES = ['ar', 'he', 'fa', 'ur']
|
||||
|
||||
export const AVAILABLE_LANGUAGES = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'es', name: 'Spanish', nativeName: 'Español' },
|
||||
{ code: 'fr', name: 'French', nativeName: 'Français' },
|
||||
{ code: 'nl', name: 'Dutch', nativeName: 'Nederlands' },
|
||||
]
|
||||
|
||||
export const LocalizationProvider = ({ children }) => {
|
||||
const { i18n } = useTranslation()
|
||||
const [dateFormat, setDateFormat] = useStickyState(
|
||||
DATE_FORMATS.MDY,
|
||||
'dateFormat',
|
||||
)
|
||||
const [timeFormat, setTimeFormat] = useStickyState(
|
||||
TIME_FORMATS.HOUR_12,
|
||||
'timeFormat',
|
||||
)
|
||||
const [firstDayOfWeek, setFirstDayOfWeek] = useStickyState(
|
||||
0,
|
||||
'firstDayOfWeek',
|
||||
) // 0 = Sunday, 1 = Monday
|
||||
const [language, setLanguage] = useStickyState('en', 'language')
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language)
|
||||
moment.locale(language)
|
||||
}, [language, i18n])
|
||||
|
||||
useEffect(() => {
|
||||
const isRTL = RTL_LANGUAGES.includes(language)
|
||||
document.documentElement.dir = isRTL ? 'rtl' : 'ltr'
|
||||
document.documentElement.lang = language
|
||||
}, [language])
|
||||
|
||||
const formatDate = (date, format = dateFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(format)
|
||||
}
|
||||
|
||||
const formatDateTime = (date, format) => {
|
||||
if (!date) return ''
|
||||
const dateTimeFormat = format || `${dateFormat} ${timeFormat}`
|
||||
return moment(date).format(dateTimeFormat)
|
||||
}
|
||||
|
||||
const formatTime = (date, format = timeFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(format)
|
||||
}
|
||||
|
||||
const formatRelative = date => {
|
||||
if (!date) return ''
|
||||
return moment(date).fromNow()
|
||||
}
|
||||
|
||||
const formatCalendar = date => {
|
||||
if (!date) return ''
|
||||
return moment(date).calendar(null, {
|
||||
sameDay: `[Today] ${timeFormat}`,
|
||||
nextDay: `[Tomorrow] ${timeFormat}`,
|
||||
nextWeek: `dddd ${timeFormat}`,
|
||||
lastDay: `[Yesterday] ${timeFormat}`,
|
||||
lastWeek: `[Last] dddd ${timeFormat}`,
|
||||
sameElse: `${dateFormat} ${timeFormat}`,
|
||||
})
|
||||
}
|
||||
|
||||
const isRTL = RTL_LANGUAGES.includes(language)
|
||||
|
||||
const fmt = {
|
||||
date: formatDate,
|
||||
dateTime: formatDateTime,
|
||||
time: formatTime,
|
||||
relative: formatRelative,
|
||||
calendar: formatCalendar,
|
||||
}
|
||||
|
||||
const value = {
|
||||
dateFormat,
|
||||
setDateFormat,
|
||||
timeFormat,
|
||||
setTimeFormat,
|
||||
firstDayOfWeek,
|
||||
setFirstDayOfWeek,
|
||||
language,
|
||||
setLanguage,
|
||||
isRTL,
|
||||
fmt,
|
||||
availableLanguages: AVAILABLE_LANGUAGES,
|
||||
}
|
||||
|
||||
return (
|
||||
<LocalizationContext.Provider value={value}>
|
||||
{children}
|
||||
</LocalizationContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useLocalization = () => {
|
||||
const context = useContext(LocalizationContext)
|
||||
if (!context) {
|
||||
throw new Error('useLocalization must be used within LocalizationProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import PaymentSuccessView from '../views/Payments/PaymentSuccessView'
|
||||
import PrivacyPolicyView from '../views/PrivacyPolicy/PrivacyPolicyView'
|
||||
import ProjectView from '../views/Projects/ProjectView'
|
||||
import APITokenSettings from '../views/Settings/APITokenSettings'
|
||||
import LocalizationSettings from '../views/Settings/LocalizationSettings'
|
||||
import MFASettings from '../views/Settings/MFASettings'
|
||||
import NotificationSetting from '../views/Settings/NotificationSetting'
|
||||
import ProfileSettings from '../views/Settings/ProfileSettings'
|
||||
@@ -115,6 +116,10 @@ const Router = createBrowserRouter([
|
||||
path: 'theme',
|
||||
element: <ThemeSettings />,
|
||||
},
|
||||
{
|
||||
path: 'localization',
|
||||
element: <LocalizationSettings />,
|
||||
},
|
||||
{
|
||||
path: 'advanced',
|
||||
element: <AdvancedSettings />,
|
||||
|
||||
117
src/i18n/README.md
Normal file
117
src/i18n/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Internationalization (i18n) Setup
|
||||
|
||||
This directory contains the internationalization configuration for Donetick.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
src/i18n/
|
||||
├── config.js # i18next configuration
|
||||
└── README.md # This file
|
||||
|
||||
public/locales/
|
||||
├── en/ # English (default)
|
||||
│ ├── common.json
|
||||
│ ├── settings.json
|
||||
│ └── chores.json
|
||||
├── es/ # Spanish
|
||||
├── ar/ # Arabic (RTL)
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Usage in Components
|
||||
|
||||
### Using translations
|
||||
|
||||
```jsx
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
function MyComponent() {
|
||||
const { t } = useTranslation('settings') // or 'common', 'chores'
|
||||
|
||||
return <h1>{t('title')}</h1>
|
||||
}
|
||||
```
|
||||
|
||||
### Using date formatting
|
||||
|
||||
```jsx
|
||||
import { useLocalization } from '@/contexts/LocalizationContext'
|
||||
|
||||
function MyComponent() {
|
||||
const { formatDate, formatDateTime, formatRelative } = useLocalization()
|
||||
|
||||
const date = new Date()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Date: {formatDate(date)}</p>
|
||||
<p>DateTime: {formatDateTime(date)}</p>
|
||||
<p>Relative: {formatRelative(date)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Using language/format settings
|
||||
|
||||
```jsx
|
||||
import { useLocalization } from '@/contexts/LocalizationContext'
|
||||
|
||||
function MyComponent() {
|
||||
const {
|
||||
language,
|
||||
setLanguage,
|
||||
dateFormat,
|
||||
setDateFormat,
|
||||
isRTL
|
||||
} = useLocalization()
|
||||
|
||||
return (
|
||||
<div dir={isRTL ? 'rtl' : 'ltr'}>
|
||||
Current language: {language}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Available Namespaces
|
||||
|
||||
- **common**: General UI elements (buttons, messages, etc.)
|
||||
- **settings**: Settings page translations
|
||||
- **chores**: Chores-related translations
|
||||
|
||||
## Adding New Translations
|
||||
|
||||
1. Add the text to the appropriate JSON file in `public/locales/en/`
|
||||
2. Use the translation in your component with `t('key')`
|
||||
3. Upload to translation platform for community translation
|
||||
|
||||
## RTL Support
|
||||
|
||||
Languages in the `RTL_LANGUAGES` array automatically get:
|
||||
- `dir="rtl"` on the document
|
||||
- RTL-specific CSS styles
|
||||
- Proper text alignment
|
||||
|
||||
Currently supported RTL languages: Arabic (ar), Hebrew (he), Persian (fa), Urdu (ur)
|
||||
|
||||
## Date Format Preferences
|
||||
|
||||
Users can choose from:
|
||||
- MM/DD/YYYY (US)
|
||||
- DD/MM/YYYY (Europe)
|
||||
- YYYY-MM-DD (ISO)
|
||||
- Long format (January 1, 2024)
|
||||
- Short format (Jan 1, 2024)
|
||||
|
||||
## Time Format Preferences
|
||||
|
||||
- 12-hour (with AM/PM)
|
||||
- 24-hour
|
||||
|
||||
## First Day of Week
|
||||
|
||||
Users can choose:
|
||||
- Sunday
|
||||
- Monday
|
||||
36
src/i18n/config.js
Normal file
36
src/i18n/config.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import i18n from 'i18next'
|
||||
import LanguageDetector from 'i18next-browser-languagedetector'
|
||||
import HttpBackend from 'i18next-http-backend'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
i18n
|
||||
.use(HttpBackend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: 'en',
|
||||
debug: import.meta.env.DEV,
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
},
|
||||
|
||||
ns: ['common', 'settings', 'chores'],
|
||||
defaultNS: 'common',
|
||||
|
||||
detection: {
|
||||
order: ['localStorage', 'navigator'],
|
||||
caches: ['localStorage'],
|
||||
lookupLocalStorage: 'i18nextLng',
|
||||
},
|
||||
|
||||
react: {
|
||||
useSuspense: true,
|
||||
},
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -54,3 +54,70 @@ html {
|
||||
.animate-optimized.animation-complete {
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
/* RTL Support */
|
||||
[dir='rtl'] {
|
||||
direction: rtl;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rtl-mirror {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Handle margins and paddings for RTL */
|
||||
[dir='rtl'] .ml-auto {
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
[dir='rtl'] .mr-auto {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Flip icons and arrows in RTL */
|
||||
[dir='rtl'] .rtl-flip {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
/* Ensure proper text alignment in RTL */
|
||||
[dir='rtl'] input,
|
||||
[dir='rtl'] textarea {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Handle border radius for RTL */
|
||||
[dir='rtl'] .rounded-l-none {
|
||||
border-radius: 0 0.375rem 0.375rem 0;
|
||||
}
|
||||
|
||||
[dir='rtl'] .rounded-r-none {
|
||||
border-radius: 0.375rem 0 0 0.375rem;
|
||||
}
|
||||
|
||||
/* Fix flex alignment for RTL */
|
||||
[dir='rtl'] .flex {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
/* Ensure cards and containers align properly in RTL */
|
||||
[dir='rtl'] .MuiCard-root,
|
||||
[dir='rtl'] .MuiBox-root,
|
||||
[dir='rtl'] .MuiStack-root {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Fix list item alignment in RTL */
|
||||
[dir='rtl'] .MuiListItem-root,
|
||||
[dir='rtl'] .MuiListItemButton-root {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
/* Fix gap alignment in RTL */
|
||||
[dir='rtl'] .gap-1,
|
||||
[dir='rtl'] .gap-2,
|
||||
[dir='rtl'] .gap-3,
|
||||
[dir='rtl'] .gap-4 {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import Contexts from './contexts/Contexts.jsx'
|
||||
import './i18n/config'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
|
||||
@@ -19,12 +19,22 @@ const allMonths = [
|
||||
* @param {Object} chore - The chore object (needed for nextDueDate null check)
|
||||
* @returns {string} The formatted due date text
|
||||
*/
|
||||
export const getDueDateChipText = (nextDueDate, chore) => {
|
||||
export const getDueDateChipText = (nextDueDate, chore, timeFormat = 'h:mm A') => {
|
||||
if (chore?.nextDueDate === null || nextDueDate === null) return 'No Due Date'
|
||||
|
||||
const dueDate = moment(nextDueDate)
|
||||
const diff = moment(nextDueDate).diff(moment(), 'hours')
|
||||
|
||||
const calendarFormat = {
|
||||
sameDay: `[Today] ${timeFormat}`,
|
||||
nextDay: `[Tomorrow] ${timeFormat}`,
|
||||
nextWeek: `dddd ${timeFormat}`,
|
||||
lastDay: `[Yesterday] ${timeFormat}`,
|
||||
lastWeek: `[Last] dddd ${timeFormat}`,
|
||||
sameElse: `MMM D ${timeFormat}`,
|
||||
}
|
||||
|
||||
|
||||
// if time is 23:59:59, treat as end-of-day (date only, no specific time)
|
||||
if (dueDate.hours() === 23 && dueDate.minutes() === 59 && dueDate.seconds() === 59) {
|
||||
if (diff < 0) {
|
||||
@@ -33,22 +43,22 @@ export const getDueDateChipText = (nextDueDate, chore) => {
|
||||
if (absDiff <= 48) {
|
||||
return (
|
||||
'Overdue ' +
|
||||
moment(nextDueDate).calendar().split(' at ')[0].toLowerCase()
|
||||
moment(nextDueDate).calendar(null, calendarFormat).split(' ')[0].toLowerCase()
|
||||
)
|
||||
}
|
||||
return 'Overdue ' + dueDate.fromNow()
|
||||
}
|
||||
// if due in next 48 hours, show calendar format without time (e.g., "Tomorrow")
|
||||
if (diff < 48 && diff > 0) {
|
||||
return moment(nextDueDate).calendar().split(' at ')[0]
|
||||
return moment(nextDueDate).calendar(null, calendarFormat).split(' ')[0]
|
||||
}
|
||||
// if due date is after 48 hours, show it in format: Due in 3 days
|
||||
return 'Due ' + dueDate.fromNow()
|
||||
}
|
||||
|
||||
// if due in next 48 hours, we should show it in this format: Tomorrow 11:00 AM
|
||||
// if due in next 48 hours, we should show it in this format: Tomorrow 11:00
|
||||
if (diff < 48 && diff > 0) {
|
||||
return moment(nextDueDate).calendar().replace(' at', '')
|
||||
return moment(nextDueDate).calendar(null, calendarFormat)
|
||||
}
|
||||
return 'Due ' + moment(nextDueDate).fromNow()
|
||||
}
|
||||
|
||||
83
src/utils/DateFormatter.js
Normal file
83
src/utils/DateFormatter.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import moment from 'moment'
|
||||
|
||||
export const createDateFormatter = (
|
||||
dateFormat,
|
||||
timeFormat,
|
||||
firstDayOfWeek,
|
||||
) => {
|
||||
moment.updateLocale('en', {
|
||||
week: {
|
||||
dow: firstDayOfWeek,
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
formatDate: (date, customFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(customFormat || dateFormat)
|
||||
},
|
||||
|
||||
formatDateTime: (date, customFormat) => {
|
||||
if (!date) return ''
|
||||
const format = customFormat || `${dateFormat} ${timeFormat}`
|
||||
return moment(date).format(format)
|
||||
},
|
||||
|
||||
formatTime: (date, customFormat) => {
|
||||
if (!date) return ''
|
||||
return moment(date).format(customFormat || timeFormat)
|
||||
},
|
||||
|
||||
formatRelative: date => {
|
||||
if (!date) return ''
|
||||
return moment(date).fromNow()
|
||||
},
|
||||
|
||||
formatCalendar: (date, opts) => {
|
||||
if (!date) return ''
|
||||
return moment(date).calendar(null, opts)
|
||||
},
|
||||
|
||||
formatShortDate: date => {
|
||||
if (!date) return ''
|
||||
return moment(date).format('MMM D, YYYY')
|
||||
},
|
||||
|
||||
formatLongDate: date => {
|
||||
if (!date) return ''
|
||||
return moment(date).format('MMMM D, YYYY')
|
||||
},
|
||||
|
||||
isBefore: (date, compareDate) => {
|
||||
return moment(date).isBefore(compareDate)
|
||||
},
|
||||
|
||||
isAfter: (date, compareDate) => {
|
||||
return moment(date).isAfter(compareDate)
|
||||
},
|
||||
|
||||
diff: (date1, date2, unit) => {
|
||||
return moment(date1).diff(moment(date2), unit)
|
||||
},
|
||||
|
||||
add: (date, amount, unit) => {
|
||||
return moment(date).add(amount, unit).toDate()
|
||||
},
|
||||
|
||||
subtract: (date, amount, unit) => {
|
||||
return moment(date).subtract(amount, unit).toDate()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const useDateFormatter = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return createDateFormatter('MM/DD/YYYY', 'h:mm A', 0)
|
||||
}
|
||||
|
||||
const dateFormat = localStorage.getItem('dateFormat') || 'MM/DD/YYYY'
|
||||
const timeFormat = localStorage.getItem('timeFormat') || 'h:mm A'
|
||||
const firstDayOfWeek = parseInt(localStorage.getItem('firstDayOfWeek') || '0')
|
||||
|
||||
return createDateFormatter(dateFormat, timeFormat, firstDayOfWeek)
|
||||
}
|
||||
@@ -39,9 +39,11 @@ import { Divider } from '@mui/material'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useChoreDetails } from '../../queries/ChoreQueries.jsx'
|
||||
import {
|
||||
useChoreTimer,
|
||||
@@ -75,6 +77,8 @@ import TimePassedCard from './TimePassedCard.jsx'
|
||||
import TimerSplitButton from './TimerSplitButton.jsx'
|
||||
|
||||
const ChoreView = () => {
|
||||
const { t } = useTranslation('chores')
|
||||
const { fmt } = useLocalization()
|
||||
const [chore, setChore] = useState({})
|
||||
const navigate = useNavigate()
|
||||
const [performers, setPerformers] = useState([])
|
||||
@@ -143,13 +147,13 @@ const ChoreView = () => {
|
||||
{
|
||||
size: 6,
|
||||
icon: <PeopleAlt />,
|
||||
title: 'Assignment',
|
||||
text: `Assigned: ${
|
||||
title: t('choreView.assignment'),
|
||||
text: `${t('choreView.assigned')}: ${
|
||||
performers.find(p => p.userId === chore.assignedTo)?.displayName ||
|
||||
'N/A'
|
||||
t('choreView.na')
|
||||
}`,
|
||||
subtext: ` Last: ${
|
||||
chore.lastCompletedDate && chore.lastCompletedBy
|
||||
subtext: ` ${t('choreView.last')}: ${
|
||||
chore.lastCompletedDate
|
||||
? performers.find(p => p.userId === chore.lastCompletedBy)
|
||||
?.displayName
|
||||
: 'N/A'
|
||||
@@ -158,14 +162,14 @@ const ChoreView = () => {
|
||||
{
|
||||
size: 6,
|
||||
icon: <CalendarMonth />,
|
||||
title: 'Schedule',
|
||||
text: `Due: ${
|
||||
chore.nextDueDate ? moment(chore.nextDueDate).fromNow() : 'N/A'
|
||||
title: t('choreView.schedule'),
|
||||
text: `${t('choreView.due')}: ${
|
||||
chore.nextDueDate ? moment(chore.nextDueDate).fromNow() : t('choreView.na')
|
||||
}`,
|
||||
subtext: `Last: ${
|
||||
subtext: `${t('choreView.last')}: ${
|
||||
chore.lastCompletedDate
|
||||
? moment(chore.lastCompletedDate).fromNow()
|
||||
: 'N/A'
|
||||
: t('choreView.na')
|
||||
}`,
|
||||
|
||||
subtext2:
|
||||
@@ -176,16 +180,16 @@ const ChoreView = () => {
|
||||
{
|
||||
size: 6,
|
||||
icon: <Checklist />,
|
||||
title: 'Statistics',
|
||||
text: `Completed: ${chore.totalCompletedCount || 0} times`,
|
||||
title: t('choreView.statistics'),
|
||||
text: `${t('choreView.completed')}: ${chore.totalCompletedCount || 0} ${t('choreView.times')}`,
|
||||
},
|
||||
{
|
||||
size: 6,
|
||||
icon: <Person />,
|
||||
title: 'Details',
|
||||
subtext: `Created By: ${
|
||||
title: t('choreView.details'),
|
||||
subtext: `${t('choreView.createdBy')}: ${
|
||||
performers.find(p => p.userId === chore.createdBy)?.displayName ||
|
||||
'N/A'
|
||||
t('choreView.na')
|
||||
}`,
|
||||
},
|
||||
]
|
||||
@@ -225,8 +229,8 @@ const ChoreView = () => {
|
||||
.then(() => {
|
||||
// Show undo notification
|
||||
showSuccess({
|
||||
title: 'Task Completed',
|
||||
message: 'Your task has been marked as complete',
|
||||
title: t('choreView.taskCompleted'),
|
||||
message: t('choreView.taskCompletedMessage'),
|
||||
undoAction: async () => {
|
||||
try {
|
||||
const undoResponse = await UndoChoreAction(choreId)
|
||||
@@ -239,16 +243,16 @@ const ChoreView = () => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
}
|
||||
showUndo({
|
||||
title: 'Undo Successful',
|
||||
message: 'Task completion has been undone.',
|
||||
title: t('choreView.undoSuccessful'),
|
||||
message: t('choreView.taskCompletionUndone'),
|
||||
})
|
||||
} else {
|
||||
throw new Error('Failed to undo')
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Undo Failed',
|
||||
message: 'Unable to undo the action. Please try again.',
|
||||
title: t('choreView.undoFailed'),
|
||||
message: t('choreView.undoFailedMessage'),
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -266,7 +270,7 @@ const ChoreView = () => {
|
||||
|
||||
// Show undo notification
|
||||
showSuccess({
|
||||
message: 'Task skipped',
|
||||
message: t('choreView.skipTask'),
|
||||
undoAction: async () => {
|
||||
try {
|
||||
const undoResponse = await UndoChoreAction(choreId)
|
||||
@@ -279,16 +283,16 @@ const ChoreView = () => {
|
||||
queryClient.invalidateQueries(['chores'])
|
||||
}
|
||||
showUndo({
|
||||
title: 'Undo Successful',
|
||||
message: 'Task skip has been undone.',
|
||||
title: t('choreView.undoSuccessful'),
|
||||
message: t('choreView.taskSkipUndone'),
|
||||
})
|
||||
} else {
|
||||
throw new Error('Failed to undo')
|
||||
}
|
||||
} catch (error) {
|
||||
showError({
|
||||
title: 'Undo Failed',
|
||||
message: 'Unable to undo the action. Please try again.',
|
||||
title: t('choreView.undoFailed'),
|
||||
message: t('choreView.undoFailedMessage'),
|
||||
})
|
||||
}
|
||||
},
|
||||
@@ -324,11 +328,10 @@ const ChoreView = () => {
|
||||
const handleResetTimer = () => {
|
||||
setTimerActionConfig({
|
||||
isOpen: true,
|
||||
title: 'Reset Timer',
|
||||
message:
|
||||
'Are you sure you want to reset the timer? This will clear all time records since you started the task.',
|
||||
confirmText: 'Reset Timer',
|
||||
cancelText: 'Cancel',
|
||||
title: t('choreView.resetTimer'),
|
||||
message: t('choreView.resetTimerConfirmation'),
|
||||
confirmText: t('choreView.resetTimer'),
|
||||
cancelText: t('common:cancel'),
|
||||
onClose: confirmed => {
|
||||
if (confirmed) {
|
||||
resetChoreTimer.mutate(choreId, {
|
||||
@@ -349,11 +352,10 @@ const ChoreView = () => {
|
||||
const handleClearAllTime = () => {
|
||||
setTimerActionConfig({
|
||||
isOpen: true,
|
||||
title: 'Clear All Time Records',
|
||||
message:
|
||||
'This will permanently delete all timers for this task and set it back to "not started".',
|
||||
confirmText: 'Clear All Time',
|
||||
cancelText: 'Cancel',
|
||||
title: t('choreView.clearAllTimeRecords'),
|
||||
message: t('choreView.clearAllTimeConfirmation'),
|
||||
confirmText: t('choreView.clearAllTimeRecords'),
|
||||
cancelText: t('common:cancel'),
|
||||
onClose: async confirmed => {
|
||||
if (confirmed) {
|
||||
if (choreTimer?.res?.id) {
|
||||
@@ -473,13 +475,13 @@ const ChoreView = () => {
|
||||
color='warning'
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Archived
|
||||
{t('choreView.archive')}
|
||||
</Chip>
|
||||
)}
|
||||
<Chip startDecorator={<CalendarMonth />} size='md' sx={{ mb: 1 }}>
|
||||
{chore.nextDueDate
|
||||
? `Due at ${moment(chore.nextDueDate).format('MM/DD/YYYY hh:mm A')}`
|
||||
: 'N/A'}
|
||||
? `${t('choreView.due')} ${fmt.dateTime(chore.nextDueDate)}`
|
||||
: t('choreView.na')}
|
||||
</Chip>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -628,7 +630,7 @@ const ChoreView = () => {
|
||||
variant='plain'
|
||||
>
|
||||
{chorePriority ? chorePriority.icon : <LowPriority />}
|
||||
{chorePriority ? chorePriority.name : 'No Priority'}
|
||||
{chorePriority ? chorePriority.name : t('choreView.noPriority')}
|
||||
</MenuButton>
|
||||
<Menu>
|
||||
{Priorities.map((priority, index) => (
|
||||
@@ -655,13 +657,13 @@ const ChoreView = () => {
|
||||
}}
|
||||
onClick={() => {
|
||||
handleUpdatePriority({
|
||||
name: 'No Priority',
|
||||
name: t('choreView.noPriority'),
|
||||
value: 0,
|
||||
})
|
||||
setChorePriority(null)
|
||||
}}
|
||||
>
|
||||
No Priority
|
||||
{t('choreView.noPriority')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
@@ -682,7 +684,7 @@ const ChoreView = () => {
|
||||
}}
|
||||
>
|
||||
<History />
|
||||
History
|
||||
{t('choreView.history')}
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
@@ -708,7 +710,7 @@ const ChoreView = () => {
|
||||
{chore.description && (
|
||||
<>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
Description :
|
||||
{t('choreView.description')}
|
||||
</Typography>
|
||||
|
||||
<Sheet
|
||||
@@ -722,7 +724,7 @@ const ChoreView = () => {
|
||||
onClick={() => {
|
||||
setNoteViewerConfig({
|
||||
isOpen: true,
|
||||
title: 'Description',
|
||||
title: t('choreView.descriptionTitle'),
|
||||
content: chore.description,
|
||||
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||
})
|
||||
@@ -754,7 +756,7 @@ const ChoreView = () => {
|
||||
{chore.notes && (
|
||||
<>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
Previous note:
|
||||
{t('choreView.previousNoteLabel')}
|
||||
</Typography>
|
||||
<Sheet
|
||||
variant='plain'
|
||||
@@ -767,7 +769,7 @@ const ChoreView = () => {
|
||||
onClick={() => {
|
||||
setNoteViewerConfig({
|
||||
isOpen: true,
|
||||
title: 'Previous Note',
|
||||
title: t('choreView.previousNote'),
|
||||
content: chore.notes,
|
||||
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||
})
|
||||
@@ -798,7 +800,7 @@ const ChoreView = () => {
|
||||
{chore.subTasks && chore.subTasks.length > 0 && (
|
||||
<Box sx={{ p: 0, m: 0, mb: 2 }}>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
Subtasks :
|
||||
{t('choreView.subtasksLabel')}
|
||||
</Typography>
|
||||
<Sheet
|
||||
variant='plain'
|
||||
@@ -836,7 +838,7 @@ const ChoreView = () => {
|
||||
variant='soft'
|
||||
>
|
||||
<Typography level='body-md' sx={{ mb: 1 }}>
|
||||
Task Actions
|
||||
{t('choreView.taskActions')}
|
||||
</Typography>
|
||||
|
||||
<FormControl size='sm'>
|
||||
@@ -860,7 +862,7 @@ const ChoreView = () => {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Add a note
|
||||
{t('choreView.addNote')}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
@@ -868,13 +870,13 @@ const ChoreView = () => {
|
||||
{note !== null && (
|
||||
<Box sx={{ mb: 1 }}>
|
||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||
Additional Notes:
|
||||
{t('choreView.additionalNotes')}
|
||||
</Typography>
|
||||
<RichTextEditor
|
||||
value={note || ''}
|
||||
onChange={setNote}
|
||||
entityType={'chore_completion_note'}
|
||||
placeholder='Add a note about the completion...'
|
||||
placeholder={t('choreView.notePlaceholder')}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
@@ -908,7 +910,7 @@ const ChoreView = () => {
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Set custom completion time
|
||||
{t('choreView.setCustomCompletionTime')}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
@@ -942,7 +944,7 @@ const ChoreView = () => {
|
||||
color='primary'
|
||||
startDecorator={<Unarchive />}
|
||||
>
|
||||
Unarchive
|
||||
{t('choreView.unarchive')}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
@@ -980,7 +982,7 @@ const ChoreView = () => {
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
Approve
|
||||
{t('choreView.approve')}
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
@@ -992,7 +994,7 @@ const ChoreView = () => {
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box>Reject</Box>
|
||||
<Box>{t('choreView.reject')}</Box>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -1003,7 +1005,7 @@ const ChoreView = () => {
|
||||
color='neutral'
|
||||
startDecorator={<HourglassEmpty />}
|
||||
>
|
||||
<Box>Pending Approval</Box>
|
||||
<Box>{t('choreView.pendingApproval')}</Box>
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
@@ -1022,7 +1024,7 @@ const ChoreView = () => {
|
||||
flex: 4,
|
||||
}}
|
||||
>
|
||||
<Box>Mark as done</Box>
|
||||
<Box>{t('choreView.markAsDone')}</Box>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -1031,12 +1033,10 @@ const ChoreView = () => {
|
||||
onClick={() => {
|
||||
setConfirmModelConfig({
|
||||
isOpen: true,
|
||||
title: 'Skip Task',
|
||||
|
||||
message: 'Are you sure you want to skip this task?',
|
||||
|
||||
confirmText: 'Skip',
|
||||
cancelText: 'Cancel',
|
||||
title: t('choreView.skipTask'),
|
||||
message: t('choreView.skipTaskConfirmation'),
|
||||
confirmText: t('choreView.skip'),
|
||||
cancelText: t('choreView.cancel'),
|
||||
onClose: confirmed => {
|
||||
if (confirmed) {
|
||||
handleSkippingTask()
|
||||
@@ -1053,7 +1053,7 @@ const ChoreView = () => {
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box>Skip</Box>
|
||||
<Box>{t('choreView.skip')}</Box>
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -1108,7 +1108,7 @@ const ChoreView = () => {
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
Start
|
||||
{t('choreView.start')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import moment from 'moment'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import ThingTriggerSection from './ThingTriggerSection'
|
||||
@@ -76,7 +77,7 @@ const DAY_OCCURRENCE_OPTIONS = [
|
||||
{ value: -1, label: 'Last occurrence' },
|
||||
]
|
||||
// Helper function to generate schedule preview text
|
||||
const generateSchedulePreview = metadata => {
|
||||
const generateSchedulePreview = (metadata, formatTimeFn) => {
|
||||
if (!metadata?.days?.length) return ''
|
||||
|
||||
const dayNames = metadata.days
|
||||
@@ -84,7 +85,7 @@ const generateSchedulePreview = metadata => {
|
||||
.join(', ')
|
||||
|
||||
const timeStr = metadata.time
|
||||
? moment(metadata.time).format('h:mm A')
|
||||
? formatTimeFn(metadata.time)
|
||||
: '6:00 PM'
|
||||
|
||||
if (metadata.weekPattern === 'every_week' || !metadata.weekPattern) {
|
||||
@@ -114,6 +115,7 @@ const RepeatOnSections = ({
|
||||
frequencyMetadata,
|
||||
onFrequencyMetadataUpdate,
|
||||
}) => {
|
||||
const { fmt } = useLocalization()
|
||||
// if time on frequencyMetadata is not set, try to set it to the nextDueDate if available,
|
||||
// otherwise set it to 18:00 of the current day
|
||||
useEffect(() => {
|
||||
@@ -400,7 +402,7 @@ const RepeatOnSections = ({
|
||||
{frequencyMetadata?.days?.length > 0 && (
|
||||
<Card mt={2} p={2}>
|
||||
<Typography level='body-sm' color='primary'>
|
||||
{generateSchedulePreview(frequencyMetadata)}
|
||||
{generateSchedulePreview(frequencyMetadata, fmt.time)}
|
||||
</Typography>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
import { Box, Card, Chip, Typography } from '@mui/joy'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
|
||||
const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
||||
const navigate = useNavigate()
|
||||
const { fmt } = useLocalization()
|
||||
const [time, setTime] = useState(0)
|
||||
const [shouldAnimate, setShouldAnimate] = useState(false)
|
||||
const [prevStatus, setPrevStatus] = useState(null) // Initialize as null
|
||||
@@ -187,10 +189,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
||||
size='md'
|
||||
startDecorator={<Flag sx={{ fontSize: 14 }} />}
|
||||
>
|
||||
{new Date(chore.startTime).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
{fmt.time(chore.startTime)}
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
@@ -202,10 +201,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
||||
size='md'
|
||||
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
|
||||
>
|
||||
{new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
{fmt.time(chore.timerUpdatedAt)}
|
||||
</Chip>
|
||||
)}
|
||||
</>
|
||||
@@ -219,10 +215,7 @@ const TimePassedCard = ({ chore, handleAction, onShowDetails }) => {
|
||||
size='md'
|
||||
startDecorator={<Schedule sx={{ fontSize: 14 }} />}
|
||||
>
|
||||
{new Date(chore.timerUpdatedAt).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
{fmt.time(chore.timerUpdatedAt)}
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import {
|
||||
getDueDateChipColor,
|
||||
@@ -45,6 +46,7 @@ const ChoreCard = ({
|
||||
onSelectionToggle,
|
||||
}) => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { timeFormat } = useLocalization()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
|
||||
@@ -95,7 +97,7 @@ const ChoreCard = ({
|
||||
}}
|
||||
color={getDueDateChipColor(chore.nextDueDate, chore)}
|
||||
>
|
||||
{getDueDateChipText(chore.nextDueDate, chore)}
|
||||
{getDueDateChipText(chore.nextDueDate, chore, timeFormat)}
|
||||
</Chip>
|
||||
|
||||
<Chip
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { Box, Checkbox, Chip, IconButton, Typography } from '@mui/joy'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext.jsx'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
import {
|
||||
getDueDateChipColor,
|
||||
@@ -41,6 +42,7 @@ const CompactChoreCard = ({
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { timeFormat } = useLocalization()
|
||||
const { data: circleMembersData } = useCircleMembers()
|
||||
|
||||
const { impersonatedUser } = useImpersonateUser()
|
||||
@@ -395,7 +397,7 @@ const CompactChoreCard = ({
|
||||
ml: 1,
|
||||
}}
|
||||
>
|
||||
{getDueDateChipText(chore.nextDueDate, chore)}
|
||||
{getDueDateChipText(chore.nextDueDate, chore, timeFormat)}
|
||||
</Chip>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { Box, Button, Card, Container, Grid, Sheet, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import useConfirmationModal from '../../hooks/useConfirmationModal'
|
||||
import {
|
||||
useChoreHistory,
|
||||
@@ -44,6 +45,7 @@ const ChoreHistory = () => {
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
|
||||
const [editHistory, setEditHistory] = useState(null)
|
||||
const { confirmModalConfig, showConfirmation } = useConfirmationModal()
|
||||
const { fmt } = useLocalization()
|
||||
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
|
||||
const [noteViewerConfig, setNoteViewerConfig] = useState({ isOpen: false })
|
||||
const { showSuccess, showError } = useNotification()
|
||||
@@ -367,7 +369,7 @@ const ChoreHistory = () => {
|
||||
onViewNote={notes => {
|
||||
setNoteViewerConfig({
|
||||
isOpen: true,
|
||||
title: `Updated at ${moment(historyEntry.updatedAt).format('LLLL')}`,
|
||||
title: `Updated at ${fmt.dateTime(historyEntry.updatedAt)}`,
|
||||
content: notes,
|
||||
onClose: () => setNoteViewerConfig({ isOpen: false }),
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '@mui/icons-material'
|
||||
import { Avatar, Box, Chip, Grid, IconButton, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { TASK_COLOR } from '../../utils/Colors.jsx'
|
||||
|
||||
const getCompletedChip = historyEntry => {
|
||||
@@ -96,6 +97,7 @@ const HistoryCard = ({
|
||||
onToggleActions,
|
||||
onViewNote,
|
||||
}) => {
|
||||
const { fmt } = useLocalization()
|
||||
const performer = performers.find(p => p.userId === historyEntry.completedBy)
|
||||
const assignedTo = performers.find(p => p.userId === historyEntry.assignedTo)
|
||||
|
||||
@@ -198,13 +200,12 @@ const HistoryCard = ({
|
||||
? 'Rescheduled'
|
||||
: 'Completed'}
|
||||
</Typography>
|
||||
{historyEntry.performedAt && (
|
||||
<Chip size='sm' startDecorator={<EventNote />}>
|
||||
{moment(
|
||||
historyEntry.performedAt || historyEntry.updatedAt,
|
||||
).format('MMM DD, h:mm A')}
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
<Chip size='sm' startDecorator={<EventNote />}>
|
||||
{fmt.dateTime(
|
||||
historyEntry.performedAt || historyEntry.updatedAt,
|
||||
)}
|
||||
</Chip>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
{getCompletedChip(historyEntry)}
|
||||
@@ -224,7 +225,7 @@ const HistoryCard = ({
|
||||
>
|
||||
{historyEntry.dueDate && (
|
||||
<Chip size='sm' startDecorator={<CalendarMonth />}>
|
||||
{moment(historyEntry.dueDate).format('MMM DD h:mm A')}
|
||||
{fmt.dateTime(historyEntry.dueDate)}
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocalization } from '../../../contexts/LocalizationContext'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
import { useNotification } from '../../../service/NotificationProvider'
|
||||
import {
|
||||
@@ -23,6 +24,7 @@ import ConfirmationModal from './ConfirmationModal'
|
||||
|
||||
const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
const { fmt } = useLocalization()
|
||||
|
||||
const [timerData, setTimerData] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -632,11 +634,9 @@ const TimerEditModal = ({ isOpen, onClose, choreId, onTimerUpdate }) => {
|
||||
const sessionDate = moment(pause.start).format(
|
||||
'MMM DD',
|
||||
)
|
||||
const startTime = moment(pause.start).format(
|
||||
'HH:mm',
|
||||
)
|
||||
const startTime = fmt.time(pause.start)
|
||||
const endTime = pause.end
|
||||
? moment(pause.end).format('HH:mm')
|
||||
? fmt.time(pause.end)
|
||||
: null
|
||||
|
||||
const realTimeDuration = isOngoing
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import moment from 'moment'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import {
|
||||
@@ -27,6 +28,7 @@ import SettingsLayout from './SettingsLayout'
|
||||
const APITokenSettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { showNotification } = useNotification()
|
||||
const { fmt } = useLocalization()
|
||||
const [tokens, setTokens] = useState([])
|
||||
const [isGetTokenNameModalOpen, setIsGetTokenNameModalOpen] = useState(false)
|
||||
const [showTokenId, setShowTokenId] = useState(null)
|
||||
@@ -106,7 +108,7 @@ const APITokenSettings = () => {
|
||||
<Typography level='body-md'>{token.name}</Typography>
|
||||
<Typography level='body-xs'>
|
||||
{moment(token.createdAt).fromNow()}(
|
||||
{moment(token.createdAt).format('lll')})
|
||||
{fmt.dateTime(token.createdAt)})
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import SubscriptionModal from '../../components/SubscriptionModal'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { CancelSubscription, UpdatePassword } from '../../utils/Fetcher'
|
||||
@@ -17,6 +18,7 @@ const AccountSettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const queryClient = useQueryClient()
|
||||
const { showNotification } = useNotification()
|
||||
const { fmt } = useLocalization()
|
||||
|
||||
const [changePasswordModal, setChangePasswordModal] = useState(false)
|
||||
const [subscriptionModal, setSubscriptionModal] = useState(false)
|
||||
@@ -37,13 +39,9 @@ const AccountSettings = () => {
|
||||
|
||||
const getSubscriptionDetails = () => {
|
||||
if (userProfile?.subscription === 'active') {
|
||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
|
||||
userProfile?.expiration,
|
||||
).format('MMM DD, YYYY')}.`
|
||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(userProfile?.expiration)}.`
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
|
||||
userProfile?.expiration,
|
||||
).format('MMM DD, YYYY')}.`
|
||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date(userProfile?.expiration)}.`
|
||||
} else {
|
||||
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
||||
}
|
||||
@@ -54,9 +52,7 @@ const AccountSettings = () => {
|
||||
return `Plus`
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
if (moment().isBefore(userProfile?.expiration)) {
|
||||
return `Plus(until ${moment(userProfile?.expiration).format(
|
||||
'MMM DD, YYYY',
|
||||
)})`
|
||||
return `Plus(until ${fmt.date(userProfile?.expiration)})`
|
||||
}
|
||||
return `Free`
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ const CircleSettings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { showNotification } = useNotification()
|
||||
const navigate = useNavigate()
|
||||
const { fmt } = useLocalization()
|
||||
|
||||
const [userCircles, setUserCircles] = useState([])
|
||||
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
||||
@@ -221,12 +223,12 @@ const CircleSettings = () => {
|
||||
</Typography>
|
||||
{member.isActive ? (
|
||||
<Typography level='body-sm'>
|
||||
Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
|
||||
Joined on {fmt.date(member.createdAt)}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography level='body-sm' color='danger'>
|
||||
Request to join{' '}
|
||||
{moment(member.updatedAt).format('MMM DD, YYYY')}
|
||||
{fmt.date(member.updatedAt)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -365,7 +367,7 @@ const CircleSettings = () => {
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{lastRefresh && (
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
|
||||
Last updated: {fmt.dateTime(lastRefresh)}
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
|
||||
196
src/views/Settings/LocalizationSettings.jsx
Normal file
196
src/views/Settings/LocalizationSettings.jsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import {
|
||||
DATE_FORMATS,
|
||||
TIME_FORMATS,
|
||||
useLocalization,
|
||||
} from '@/contexts/LocalizationContext'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
Option,
|
||||
Select,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SettingsLayout from './SettingsLayout'
|
||||
|
||||
const LocalizationSettings = () => {
|
||||
const { t } = useTranslation('settings')
|
||||
const {
|
||||
language,
|
||||
setLanguage,
|
||||
dateFormat,
|
||||
setDateFormat,
|
||||
timeFormat,
|
||||
setTimeFormat,
|
||||
firstDayOfWeek,
|
||||
setFirstDayOfWeek,
|
||||
availableLanguages,
|
||||
isRTL,
|
||||
} = useLocalization()
|
||||
|
||||
const sampleDate = moment('2024-01-15 14:30:00')
|
||||
|
||||
const dateFormatOptions = [
|
||||
{ value: DATE_FORMATS.MDY, label: t('localization.formats.mdy') },
|
||||
{ value: DATE_FORMATS.DMY, label: t('localization.formats.dmy') },
|
||||
{ value: DATE_FORMATS.YMD, label: t('localization.formats.ymd') },
|
||||
{ value: DATE_FORMATS.LONG, label: t('localization.formats.long') },
|
||||
{ value: DATE_FORMATS.SHORT, label: t('localization.formats.short') },
|
||||
]
|
||||
|
||||
return (
|
||||
<SettingsLayout title='Localization'>
|
||||
<div className='grid gap-4 py-4'>
|
||||
<Typography level='body-md'>{t('localization.description')}</Typography>
|
||||
|
||||
<Typography level='h3'>{t('localization.language')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
{t('localization.languageDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={language}
|
||||
onChange={(_, value) => setLanguage(value)}
|
||||
sx={{ maxWidth: '300px' }}
|
||||
>
|
||||
{availableLanguages.map(lang => (
|
||||
<Option key={lang.code} value={lang.code}>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<Typography>{lang.nativeName}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
({lang.name})
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
{isRTL && (
|
||||
<FormHelperText>
|
||||
{t(
|
||||
'localization.rtlNotice',
|
||||
'This language uses right-to-left (RTL) text direction',
|
||||
)}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<Typography level='h3'>{t('localization.dateFormat')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
{t('localization.dateFormatDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={dateFormat}
|
||||
onChange={(_, value) => setDateFormat(value)}
|
||||
sx={{ maxWidth: '300px' }}
|
||||
>
|
||||
{dateFormatOptions.map(option => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography>{option.label}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
{sampleDate.format(option.value)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Preview: {sampleDate.format(dateFormat)}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<Typography level='h3'>{t('localization.timeFormat')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
{t('localization.timeFormatDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={timeFormat}
|
||||
onChange={(_, value) => setTimeFormat(value)}
|
||||
sx={{ maxWidth: '300px' }}
|
||||
>
|
||||
<Option value={TIME_FORMATS.HOUR_12}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography>{t('localization.12hour')}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
{sampleDate.format(TIME_FORMATS.HOUR_12)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
<Option value={TIME_FORMATS.HOUR_24}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography>{t('localization.24hour')}</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
{sampleDate.format(TIME_FORMATS.HOUR_24)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Option>
|
||||
</Select>
|
||||
<FormHelperText>
|
||||
Preview: {sampleDate.format(timeFormat)}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
|
||||
<Typography level='h3'>{t('localization.firstDayOfWeek')}</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
{t('localization.firstDayOfWeekDescription')}
|
||||
</Typography>
|
||||
<FormControl>
|
||||
<ButtonGroup variant='outlined'>
|
||||
<Button
|
||||
variant={firstDayOfWeek === 0 ? 'solid' : 'outlined'}
|
||||
onClick={() => setFirstDayOfWeek(0)}
|
||||
>
|
||||
{t('localization.sunday')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={firstDayOfWeek === 1 ? 'solid' : 'outlined'}
|
||||
onClick={() => setFirstDayOfWeek(1)}
|
||||
>
|
||||
{t('localization.monday')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={firstDayOfWeek === 6 ? 'solid' : 'outlined'}
|
||||
onClick={() => setFirstDayOfWeek(6)}
|
||||
>
|
||||
{t('localization.saturday')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</FormControl>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default LocalizationSettings
|
||||
@@ -13,6 +13,7 @@ import { useQueryClient } from '@tanstack/react-query'
|
||||
import imageCompression from 'browser-image-compression'
|
||||
import { useRef, useState } from 'react'
|
||||
import Cropper from 'react-easy-crop'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
import { apiClient } from '../../utils/ApiClient'
|
||||
@@ -22,6 +23,7 @@ import { getCroppedImg } from '../../utils/imageCropUtils'
|
||||
import SettingsLayout from './SettingsLayout'
|
||||
|
||||
const ProfileSettings = () => {
|
||||
const { t } = useTranslation('settings')
|
||||
const queryClient = useQueryClient()
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { showSuccess, showError } = useNotification()
|
||||
@@ -92,13 +94,13 @@ const ProfileSettings = () => {
|
||||
|
||||
setPhotoURL(url)
|
||||
showSuccess({
|
||||
title: 'Photo Updated',
|
||||
message: 'Your profile photo has been updated successfully!',
|
||||
title: t('profile.photoUpdated'),
|
||||
message: t('profile.photoUpdatedMessage'),
|
||||
})
|
||||
} catch (err) {
|
||||
showError({
|
||||
title: 'Upload Failed',
|
||||
message: 'Failed to upload your photo. Please try again.',
|
||||
title: t('profile.uploadFailed'),
|
||||
message: t('profile.uploadFailedMessage'),
|
||||
})
|
||||
} finally {
|
||||
setIsUploading(false)
|
||||
@@ -118,8 +120,8 @@ const ProfileSettings = () => {
|
||||
|
||||
if (response.ok) {
|
||||
showSuccess({
|
||||
title: 'Profile Updated',
|
||||
message: 'Your profile information has been saved successfully!',
|
||||
title: t('profile.profileUpdated'),
|
||||
message: t('profile.profileUpdatedMessage'),
|
||||
})
|
||||
} else {
|
||||
throw new Error('Failed to update profile')
|
||||
@@ -128,9 +130,8 @@ const ProfileSettings = () => {
|
||||
console.log(err)
|
||||
|
||||
showError({
|
||||
title: 'Update Failed',
|
||||
message:
|
||||
'Unable to update your profile. Please check your connection and try again.',
|
||||
title: t('profile.updateFailed'),
|
||||
message: t('profile.updateFailedMessage'),
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
@@ -140,10 +141,10 @@ const ProfileSettings = () => {
|
||||
// Helper to resolve photoURL with baseURL if needed
|
||||
|
||||
return (
|
||||
<SettingsLayout title='Profile Settings'>
|
||||
<SettingsLayout title={t('profile.title')}>
|
||||
<div className='grid gap-4 py-4' id='profile'>
|
||||
<Typography level='body-md'>
|
||||
Update your display name and profile photo.
|
||||
{t('profile.description')}
|
||||
</Typography>
|
||||
<Card
|
||||
sx={{
|
||||
@@ -163,7 +164,7 @@ const ProfileSettings = () => {
|
||||
loading={isUploading}
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Change Photo
|
||||
{t('profile.changePhoto')}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@@ -227,7 +228,7 @@ const ProfileSettings = () => {
|
||||
size='md'
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Save
|
||||
{t('profile.save')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
@@ -237,24 +238,24 @@ const ProfileSettings = () => {
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
>
|
||||
Cancel
|
||||
{t('profile.cancel')}
|
||||
</Button>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
<Box sx={{ maxWidth: 400, mt: 3 }}>
|
||||
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
||||
Display Name
|
||||
{t('profile.displayName')}
|
||||
</Typography>
|
||||
<Input
|
||||
value={displayName}
|
||||
onChange={e => setDisplayName(e.target.value)}
|
||||
placeholder='Enter your display name'
|
||||
placeholder={t('profile.displayNamePlaceholder')}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Typography level='body-sm' sx={{ mb: 0.5 }}>
|
||||
Timezone
|
||||
{t('profile.timezone')}
|
||||
</Typography>
|
||||
<Autocomplete
|
||||
value={timezone}
|
||||
@@ -283,7 +284,7 @@ const ProfileSettings = () => {
|
||||
)
|
||||
})
|
||||
}}
|
||||
placeholder='Select your timezone'
|
||||
placeholder={t('profile.timezonePlaceholder')}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
@@ -294,7 +295,7 @@ const ProfileSettings = () => {
|
||||
loading={isSaving}
|
||||
sx={{ width: 120 }}
|
||||
>
|
||||
Save
|
||||
{t('profile.save')}
|
||||
</Button>
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import RealTimeSettings from '../../components/RealTimeSettings'
|
||||
import SubscriptionModal from '../../components/SubscriptionModal'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import Logo from '../../Logo'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { useNotification } from '../../service/NotificationProvider'
|
||||
@@ -46,6 +47,7 @@ import NativeCancelSubscriptionModal from '../Modals/Inputs/NativeCancelSubscrip
|
||||
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
|
||||
import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
|
||||
import APITokenSettings from './APITokenSettings'
|
||||
import LocalizationSettings from './LocalizationSettings'
|
||||
import MFASettings from './MFASettings'
|
||||
import NotificationSetting from './NotificationSetting'
|
||||
import ProfileSettings from './ProfileSettings'
|
||||
@@ -58,6 +60,7 @@ const Settings = () => {
|
||||
const queryClient = useQueryClient()
|
||||
const { showNotification } = useNotification()
|
||||
const navigate = useNavigate()
|
||||
const { fmt } = useLocalization()
|
||||
|
||||
const [userCircles, setUserCircles] = useState([])
|
||||
const [circleMemberRequests, setCircleMemberRequests] = useState([])
|
||||
@@ -188,13 +191,13 @@ const Settings = () => {
|
||||
|
||||
const getSubscriptionDetails = () => {
|
||||
if (userProfile?.subscription === 'active') {
|
||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
|
||||
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date(
|
||||
userProfile?.expiration,
|
||||
).format('MMM DD, YYYY')}.`
|
||||
)}.`
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
|
||||
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date(
|
||||
userProfile?.expiration,
|
||||
).format('MMM DD, YYYY')}.`
|
||||
)}.`
|
||||
} else {
|
||||
return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.`
|
||||
}
|
||||
@@ -204,9 +207,7 @@ const Settings = () => {
|
||||
return `Plus`
|
||||
} else if (userProfile?.subscription === 'cancelled') {
|
||||
if (moment().isBefore(userProfile?.expiration)) {
|
||||
return `Plus(until ${moment(userProfile?.expiration).format(
|
||||
'MMM DD, YYYY',
|
||||
)})`
|
||||
return `Plus(until ${fmt.date(userProfile?.expiration)})`
|
||||
}
|
||||
return `Free`
|
||||
} else {
|
||||
@@ -339,12 +340,12 @@ const Settings = () => {
|
||||
</Typography>
|
||||
{member.isActive ? (
|
||||
<Typography level='body-sm'>
|
||||
Joined on {moment(member.createdAt).format('MMM DD, YYYY')}
|
||||
Joined on {fmt.date(member.createdAt)}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography level='body-sm' color='danger'>
|
||||
Request to join{' '}
|
||||
{moment(member.updatedAt).format('MMM DD, YYYY')}
|
||||
{fmt.date(member.updatedAt)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
@@ -485,7 +486,7 @@ const Settings = () => {
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{lastRefresh && (
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Last updated: {moment(lastRefresh).format('MMM DD, HH:mm')}
|
||||
Last updated: {fmt.dateTime(lastRefresh)}
|
||||
</Typography>
|
||||
)}
|
||||
<Button
|
||||
@@ -916,6 +917,16 @@ const Settings = () => {
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
<div className='grid gap-4 py-4' id='localization'>
|
||||
<Typography level='h3'>Localization</Typography>
|
||||
<Divider />
|
||||
<Typography level='body-md'>
|
||||
Customize language, date format, and regional preferences for your
|
||||
account. These settings will apply throughout the application.
|
||||
</Typography>
|
||||
<LocalizationSettings />
|
||||
</div>
|
||||
|
||||
{/* Modals */}
|
||||
{confirmModalConfig?.isOpen && (
|
||||
<ConfirmationModal config={confirmModalConfig} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Circle,
|
||||
Code,
|
||||
FamilyRestroom,
|
||||
Language,
|
||||
Notifications,
|
||||
Palette,
|
||||
Person,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
Container,
|
||||
List,
|
||||
ListItem,
|
||||
@@ -29,98 +31,95 @@ import {
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { isPlusAccount } from '../../utils/Helpers'
|
||||
import { isParentUser } from '../../utils/UserHelpers'
|
||||
|
||||
const SettingsOverview = () => {
|
||||
const { t } = useTranslation('settings')
|
||||
const navigate = useNavigate()
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
const settingsCards = [
|
||||
{
|
||||
id: 'profile',
|
||||
title: 'Profile Settings',
|
||||
description:
|
||||
'Update your profile information, photo, display name, and timezone preferences.',
|
||||
title: t('overview.sections.profile.title'),
|
||||
description: t('overview.sections.profile.description'),
|
||||
icon: <Person />,
|
||||
},
|
||||
{
|
||||
id: 'circle',
|
||||
title: 'Circle Settings',
|
||||
description:
|
||||
'Manage your circle, invite members, and handle join requests.',
|
||||
title: t('overview.sections.circle.title'),
|
||||
description: t('overview.sections.circle.description'),
|
||||
icon: <Circle />,
|
||||
},
|
||||
{
|
||||
id: 'account',
|
||||
title: 'Account Settings',
|
||||
description:
|
||||
'Manage your subscription, change password, and account deletion options.',
|
||||
title: t('overview.sections.account.title'),
|
||||
description: t('overview.sections.account.description'),
|
||||
icon: <AccountCircle />,
|
||||
},
|
||||
{
|
||||
id: 'subaccounts',
|
||||
title: 'Managed Accounts',
|
||||
description:
|
||||
'Create and manage sub accounts to log in and complete assigned tasks.',
|
||||
title: t('overview.sections.subaccounts.title'),
|
||||
description: t('overview.sections.subaccounts.description'),
|
||||
icon: <FamilyRestroom />,
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
title: 'Notifications',
|
||||
description:
|
||||
'Configure push notifications, email alerts, and notification targets for tasks.',
|
||||
title: t('overview.sections.notifications.title'),
|
||||
description: t('overview.sections.notifications.description'),
|
||||
icon: <Notifications />,
|
||||
},
|
||||
{
|
||||
id: 'mfa',
|
||||
title: 'Multi-Factor Authentication',
|
||||
description:
|
||||
'Add an extra layer of security with MFA using authenticator apps.',
|
||||
title: t('overview.sections.mfa.title'),
|
||||
description: t('overview.sections.mfa.description'),
|
||||
icon: <Security />,
|
||||
},
|
||||
{
|
||||
id: 'apitokens',
|
||||
title: 'API Tokens',
|
||||
description:
|
||||
'Generate and manage access tokens for third-party integrations and API access.',
|
||||
title: t('overview.sections.apitokens.title'),
|
||||
description: t('overview.sections.apitokens.description'),
|
||||
icon: <Api />,
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
title: 'Storage Settings',
|
||||
description:
|
||||
'Backup and restore your data, manage local storage and sync preferences.',
|
||||
title: t('overview.sections.storage.title'),
|
||||
description: t('overview.sections.storage.description'),
|
||||
icon: <Storage />,
|
||||
},
|
||||
{
|
||||
id: 'sidepanel',
|
||||
title: 'Sidepanel Customization',
|
||||
description:
|
||||
'Customize the layout and visibility of cards in the sidepanel interface.',
|
||||
title: t('overview.sections.sidepanel.title'),
|
||||
description: t('overview.sections.sidepanel.description'),
|
||||
icon: <ViewSidebar />,
|
||||
},
|
||||
{
|
||||
id: 'theme',
|
||||
title: 'Theme Preferences',
|
||||
description:
|
||||
'Choose your preferred theme and configure dark/light mode settings.',
|
||||
title: t('overview.sections.theme.title'),
|
||||
description: t('overview.sections.theme.description'),
|
||||
icon: <Palette />,
|
||||
},
|
||||
{
|
||||
id: 'localization',
|
||||
title: t('overview.sections.localization.title'),
|
||||
description: t('overview.sections.localization.description'),
|
||||
icon: <Language />,
|
||||
isBeta: true,
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'Advanced Settings',
|
||||
description:
|
||||
'Configure webhooks, real-time updates, and other advanced features for enhanced productivity.',
|
||||
title: t('overview.sections.advanced.title'),
|
||||
description: t('overview.sections.advanced.description'),
|
||||
icon: <Settings />,
|
||||
},
|
||||
{
|
||||
id: 'developer',
|
||||
title: 'Developer Settings',
|
||||
description:
|
||||
'View technical information about authentication tokens, SSE connections, and debug data.',
|
||||
title: t('overview.sections.developer.title'),
|
||||
description: t('overview.sections.developer.description'),
|
||||
icon: <Code />,
|
||||
},
|
||||
]
|
||||
@@ -159,10 +158,10 @@ const SettingsOverview = () => {
|
||||
level='h3'
|
||||
sx={{ fontWeight: 'lg', color: 'text.primary' }}
|
||||
>
|
||||
Settings
|
||||
{t('overview.title')}
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ color: 'text.secondary' }}>
|
||||
Customize your experience and manage your account preferences
|
||||
{t('overview.subtitle')}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
@@ -226,7 +225,7 @@ const SettingsOverview = () => {
|
||||
fontSize: { xs: '0.95rem', md: '1.25rem' },
|
||||
}}
|
||||
>
|
||||
Upgrade to Plus
|
||||
{t('overview.upgrade.title')}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-md'
|
||||
@@ -237,7 +236,7 @@ const SettingsOverview = () => {
|
||||
lineHeight: { xs: 1.3, md: 1.5 },
|
||||
}}
|
||||
>
|
||||
Unlock powerful features to enhance your productivity
|
||||
{t('overview.upgrade.description')}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
@@ -248,10 +247,16 @@ const SettingsOverview = () => {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
}}
|
||||
>
|
||||
<span>• Rich text descriptions</span>
|
||||
<span>• Task notifications</span>
|
||||
<span>• API integrations</span>
|
||||
<span>• Advanced automation</span>
|
||||
<span>• {t('overview.upgrade.features.richText')}</span>
|
||||
<span>
|
||||
• {t('overview.upgrade.features.notifications')}
|
||||
</span>
|
||||
<span>
|
||||
• {t('overview.upgrade.features.apiIntegrations')}
|
||||
</span>
|
||||
<span>
|
||||
• {t('overview.upgrade.features.advancedAutomation')}
|
||||
</span>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -279,7 +284,7 @@ const SettingsOverview = () => {
|
||||
navigate('/settings/account')
|
||||
}}
|
||||
>
|
||||
Upgrade Now
|
||||
{t('overview.upgrade.button')}
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
@@ -319,12 +324,33 @@ const SettingsOverview = () => {
|
||||
</Avatar>
|
||||
</ListItemDecorator>
|
||||
<ListItemContent sx={{ ml: 2 }}>
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{ mb: 0.5, fontWeight: 'lg' }}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
{setting.title}
|
||||
</Typography>
|
||||
<Typography level='title-md' sx={{ fontWeight: 'lg' }}>
|
||||
{setting.title}
|
||||
</Typography>
|
||||
{setting.isBeta && (
|
||||
<Chip
|
||||
variant='outlined'
|
||||
size='sm'
|
||||
sx={{
|
||||
height: '20px',
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 'bold',
|
||||
color: 'warning.main',
|
||||
borderColor: 'warning.main',
|
||||
}}
|
||||
>
|
||||
Early Access
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
color='neutral'
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
ToggleButtonGroup,
|
||||
useColorScheme,
|
||||
} from '@mui/joy'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ELEMENTID = 'select-theme-mode'
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const { t } = useTranslation('settings')
|
||||
const { mode, setMode } = useColorScheme()
|
||||
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
||||
|
||||
@@ -30,7 +32,7 @@ const ThemeToggle = () => {
|
||||
id={`${ELEMENTID}-label`}
|
||||
htmlFor='select-theme-mode'
|
||||
>
|
||||
Theme mode
|
||||
{t('theme.themeMode')}
|
||||
</FormLabel>
|
||||
)
|
||||
|
||||
@@ -45,13 +47,13 @@ const ThemeToggle = () => {
|
||||
onChange={handleThemeModeChange}
|
||||
>
|
||||
<Button startDecorator={<LightModeOutlined />} value='light'>
|
||||
Light
|
||||
{t('theme.light')}
|
||||
</Button>
|
||||
<Button startDecorator={<DarkModeOutlined />} value='dark'>
|
||||
Dark
|
||||
{t('theme.dark')}
|
||||
</Button>
|
||||
<Button startDecorator={<LaptopOutlined />} value='system'>
|
||||
System
|
||||
{t('theme.system')}
|
||||
</Button>
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { useTheme } from '@mui/joy/styles'
|
||||
import moment from 'moment'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import {
|
||||
Line,
|
||||
LineChart,
|
||||
@@ -40,6 +41,7 @@ import LoadingComponent from '../components/Loading'
|
||||
const ThingsHistory = () => {
|
||||
const { id } = useParams()
|
||||
const theme = useTheme()
|
||||
const { fmt } = useLocalization()
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
@@ -294,9 +296,7 @@ const ThingsHistory = () => {
|
||||
tick='false'
|
||||
tickLine='false'
|
||||
axisLine='false'
|
||||
tickFormatter={tick =>
|
||||
moment(tick).format('ddd MM/DD/yyyy HH:mm:ss')
|
||||
}
|
||||
tickFormatter={tick => fmt.dateTime(tick)}
|
||||
/>
|
||||
<YAxis
|
||||
hide='true'
|
||||
@@ -306,9 +306,7 @@ const ThingsHistory = () => {
|
||||
axisLine='false'
|
||||
/>
|
||||
<Tooltip
|
||||
labelFormatter={label =>
|
||||
moment(label).format('ddd MM/DD/yyyy HH:mm:ss')
|
||||
}
|
||||
labelFormatter={label => fmt.dateTime(label)}
|
||||
/>
|
||||
|
||||
<Line
|
||||
@@ -399,7 +397,7 @@ const ThingsHistory = () => {
|
||||
color='primary'
|
||||
startDecorator={<Schedule />}
|
||||
>
|
||||
{moment(history.updatedAt).format('MMM DD, h:mm A')}
|
||||
{fmt.dateTime(history.updatedAt)}
|
||||
</Chip>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import {
|
||||
useChoreTimer,
|
||||
usePauseChore,
|
||||
@@ -52,6 +53,7 @@ import LoadingComponent from '../components/Loading'
|
||||
|
||||
const TimerDetails = () => {
|
||||
const { choreId } = useParams()
|
||||
const { fmt } = useLocalization()
|
||||
const [timerData, setTimerData] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editingSessions, setEditingSessions] = useState({})
|
||||
@@ -819,14 +821,14 @@ const TimerDetails = () => {
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.tertiary' }}
|
||||
>
|
||||
Started: {moment(timerData.startTime).format('HH:mm')}
|
||||
Started: {fmt.time(timerData.startTime)}
|
||||
</Typography>
|
||||
{timerData.endTime && (
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'text.tertiary' }}
|
||||
>
|
||||
Ended: {moment(timerData.endTime).format('HH:mm')}
|
||||
Ended: {fmt.time(timerData.endTime)}
|
||||
</Typography>
|
||||
)}
|
||||
{!timerData.endTime && (
|
||||
@@ -834,7 +836,7 @@ const TimerDetails = () => {
|
||||
level='body-xs'
|
||||
sx={{ color: 'success.500' }}
|
||||
>
|
||||
Now: {moment(currentTime).format('HH:mm')}
|
||||
Now: {fmt.time(currentTime)}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography
|
||||
@@ -925,9 +927,9 @@ const TimerDetails = () => {
|
||||
const sessionDate = moment(pause.start).format(
|
||||
'MMM DD',
|
||||
)
|
||||
const startTime = moment(pause.start).format('HH:mm')
|
||||
const startTime = fmt.time(pause.start)
|
||||
const endTime = pause.end
|
||||
? moment(pause.end).format('HH:mm')
|
||||
? fmt.time(pause.end)
|
||||
: null
|
||||
|
||||
const realTimeDuration = isOngoing
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@mui/joy'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useChores, useChoresHistory } from '../../queries/ChoreQueries'
|
||||
import NoteViewerModal from '../Modals/Inputs/NoteViewerModal'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries.jsx'
|
||||
@@ -142,7 +143,10 @@ const ChoreHistoryItem = ({ time, name, points, status, performer, notes, onView
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
||||
const { fmt } = useLocalization()
|
||||
|
||||
const groupedHistory = groupByDate(history)
|
||||
|
||||
const sortedEntries = Object.entries(groupedHistory).sort(
|
||||
@@ -161,12 +165,7 @@ const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
||||
{Object.entries(groupedHistory).map(([date, items]) => (
|
||||
<Box key={date} sx={{ mb: 4 }}>
|
||||
<Typography level='title-sm' sx={{ mb: 0.5 }}>
|
||||
{new Date(date).toLocaleDateString([], {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
{fmt.date(date)}
|
||||
</Typography>
|
||||
<Divider />
|
||||
<Stack spacing={1}>
|
||||
@@ -174,12 +173,10 @@ const ChoreHistoryTimeline = ({ history, onViewNote }) => {
|
||||
<>
|
||||
<ChoreHistoryItem
|
||||
key={record.id}
|
||||
time={new Date(
|
||||
|
||||
time={fmt.time(
|
||||
record.performedAt || record.updatedAt,
|
||||
).toLocaleTimeString([], {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
)}
|
||||
name={record.choreName}
|
||||
points={record.points}
|
||||
status={record.status}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Avatar, Box, Chip, Grid, Typography } from '@mui/joy'
|
||||
import moment from 'moment'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
||||
|
||||
@@ -15,6 +16,7 @@ const getAssigneeColor = (assignee, userProfile) => {
|
||||
}
|
||||
const CalendarCard = ({ chores }) => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { fmt } = useLocalization()
|
||||
|
||||
const [selectedDate, setSeletedDate] = useState(null)
|
||||
const Navigate = useNavigate()
|
||||
@@ -197,7 +199,7 @@ const CalendarCard = ({ chores }) => {
|
||||
}}
|
||||
>
|
||||
<Typography level='title-md'>
|
||||
{moment(selectedDate).format('MMMM D, YYYY')}
|
||||
{fmt.date(selectedDate)}
|
||||
</Typography>
|
||||
<Chip variant='soft' color='primary' size='md'>
|
||||
{(() => {
|
||||
@@ -291,7 +293,7 @@ const CalendarCard = ({ chores }) => {
|
||||
color: 'neutral.500',
|
||||
}}
|
||||
>
|
||||
{moment(chore.nextDueDate).format('h:mm A')}
|
||||
{fmt.time(chore.nextDueDate)}
|
||||
</Typography>
|
||||
{/* <Typography
|
||||
level='body-xs'
|
||||
|
||||
@@ -4,6 +4,7 @@ import Calendar from 'react-calendar'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import { getPriorityColor, TASK_COLOR } from '../../utils/Colors'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import styles from './CalendarDual.module.css'
|
||||
|
||||
const getAssigneeColor = (assignee, userProfile) => {
|
||||
@@ -13,6 +14,9 @@ const getAssigneeColor = (assignee, userProfile) => {
|
||||
}
|
||||
const CalendarDual = ({ chores, onDateChange }) => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const { firstDayOfWeek, fmt } = useLocalization()
|
||||
const calendarType =
|
||||
firstDayOfWeek === 1 ? 'iso8601' : firstDayOfWeek === 6 ? 'islamic' : 'gregory'
|
||||
|
||||
const [selectedDate, setSeletedDate] = useState(null)
|
||||
const [currentDate, setCurrentDate] = useState(new Date())
|
||||
@@ -96,16 +100,14 @@ const CalendarDual = ({ chores, onDateChange }) => {
|
||||
{isSecondary && (
|
||||
<div className={styles.secondaryCalendarHeader}>
|
||||
<Typography level='title-md' sx={{ textAlign: 'center', mb: 1 }}>
|
||||
{date.toLocaleDateString('en-US', {
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
})}
|
||||
{fmt.date(date, 'MMMM YYYY')}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
<Calendar
|
||||
className={styles.reactCalendar}
|
||||
tileContent={tileContent}
|
||||
calendarType={calendarType}
|
||||
onChange={d => {
|
||||
let date = new Date(d)
|
||||
setSeletedDate(date)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import Calendar from 'react-calendar'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { getPriorityColor } from '../../utils/Colors'
|
||||
import styles from './Calendar.module.css'
|
||||
const CalendarMonthly = ({ chores, onDateChange }) => {
|
||||
const { firstDayOfWeek } = useLocalization()
|
||||
const calendarType =
|
||||
firstDayOfWeek === 1 ? 'iso8601' : firstDayOfWeek === 6 ? 'islamic' : 'gregory'
|
||||
const tileContent = ({ date, view }) => {
|
||||
if (view === 'month') {
|
||||
const dayChores = chores.filter(chore => {
|
||||
@@ -51,6 +55,7 @@ const CalendarMonthly = ({ chores, onDateChange }) => {
|
||||
<div className={styles.reactCalendar}>
|
||||
<Calendar
|
||||
tileContent={tileContent}
|
||||
calendarType={calendarType}
|
||||
onChange={d => {
|
||||
onDateChange(new Date(d))
|
||||
}}
|
||||
|
||||
@@ -25,78 +25,12 @@ import {
|
||||
} from '@mui/joy'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { version } from '../../../package.json'
|
||||
import UserProfileAvatar from '../../components/UserProfileAvatar'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import NavBarLink from './NavBarLink'
|
||||
const links = [
|
||||
{
|
||||
to: '/chores',
|
||||
label: 'All Tasks',
|
||||
icon: <Inbox />,
|
||||
},
|
||||
{
|
||||
to: '/archived',
|
||||
label: 'Archived',
|
||||
icon: <Archive />,
|
||||
},
|
||||
|
||||
// {
|
||||
// to: '/chores',
|
||||
// label: 'Desktop View',
|
||||
// icon: <ListAltRounded />,
|
||||
// },
|
||||
{
|
||||
to: '/things',
|
||||
label: 'Things',
|
||||
icon: <Widgets />,
|
||||
},
|
||||
{
|
||||
to: 'labels',
|
||||
label: 'Labels',
|
||||
icon: <ListAlt />,
|
||||
},
|
||||
{
|
||||
to: 'projects',
|
||||
label: 'Projects',
|
||||
icon: <FolderOpen />,
|
||||
},
|
||||
{
|
||||
to: 'filters',
|
||||
label: 'Filters',
|
||||
icon: <FilterAlt />,
|
||||
},
|
||||
{
|
||||
to: 'activities',
|
||||
label: 'Activities',
|
||||
icon: <History />,
|
||||
},
|
||||
{
|
||||
to: 'points',
|
||||
label: 'Points',
|
||||
icon: <Toll />,
|
||||
},
|
||||
// {
|
||||
// to: '/settings#sharing',
|
||||
// label: 'Sharing',
|
||||
// icon: <ShareOutlined />,
|
||||
// },
|
||||
// {
|
||||
// to: '/settings#notifications',
|
||||
// label: 'Notifications',
|
||||
// icon: <Message />,
|
||||
// },
|
||||
// {
|
||||
// to: '/settings#account',
|
||||
// label: 'Account',
|
||||
// icon: <AccountBox />,
|
||||
// },
|
||||
{
|
||||
to: '/settings',
|
||||
label: 'Settings',
|
||||
icon: <SettingsOutlined />,
|
||||
},
|
||||
]
|
||||
|
||||
import { SafeArea } from 'capacitor-plugin-safe-area'
|
||||
import Z_INDEX from '../../constants/zIndex'
|
||||
@@ -105,10 +39,60 @@ import { apiClient } from '../../utils/ApiClient'
|
||||
|
||||
const publicPages = ['/landing', '/privacy', '/terms']
|
||||
const NavBar = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const { isRTL } = useLocalization()
|
||||
const { data: resource } = useResource()
|
||||
|
||||
const navigate = useNavigate()
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
|
||||
const links = [
|
||||
{
|
||||
to: '/chores',
|
||||
label: t('navigation.allTasks'),
|
||||
icon: <Inbox />,
|
||||
},
|
||||
{
|
||||
to: '/archived',
|
||||
label: t('navigation.archived'),
|
||||
icon: <Archive />,
|
||||
},
|
||||
{
|
||||
to: '/things',
|
||||
label: t('navigation.things'),
|
||||
icon: <Widgets />,
|
||||
},
|
||||
{
|
||||
to: 'labels',
|
||||
label: t('navigation.labels'),
|
||||
icon: <ListAlt />,
|
||||
},
|
||||
{
|
||||
to: 'projects',
|
||||
label: t('navigation.projects'),
|
||||
icon: <FolderOpen />,
|
||||
},
|
||||
{
|
||||
to: 'filters',
|
||||
label: t('navigation.filters'),
|
||||
icon: <FilterAlt />,
|
||||
},
|
||||
{
|
||||
to: 'activities',
|
||||
label: t('navigation.activities'),
|
||||
icon: <History />,
|
||||
},
|
||||
{
|
||||
to: 'points',
|
||||
label: t('navigation.points'),
|
||||
icon: <Toll />,
|
||||
},
|
||||
{
|
||||
to: '/settings',
|
||||
label: t('navigation.settings'),
|
||||
icon: <SettingsOutlined />,
|
||||
},
|
||||
]
|
||||
const [openDrawer, closeDrawer] = [
|
||||
() => setDrawerOpen(true),
|
||||
() => setDrawerOpen(false),
|
||||
@@ -157,7 +141,7 @@ const NavBar = () => {
|
||||
}
|
||||
}}
|
||||
title={
|
||||
searchParams.get('from') === 'calendar' ? 'Back to Calendar' : 'Back'
|
||||
searchParams.get('from') === 'calendar' ? t('backToCalendar') : t('back')
|
||||
}
|
||||
>
|
||||
<ArrowBack />
|
||||
@@ -221,13 +205,14 @@ const NavBar = () => {
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onClose={closeDrawer}
|
||||
anchor={isRTL ? 'right' : 'left'}
|
||||
size='sm'
|
||||
onClick={closeDrawer}
|
||||
sx={{
|
||||
'& .MuiDrawer-content': {
|
||||
position: 'fixed',
|
||||
// pt: 'calc(var(--safe-area-inset-top, 0px))',
|
||||
left: 0,
|
||||
...(isRTL ? { right: 0 } : { left: 0 }),
|
||||
// pb: 'calc(var(--safe-area-inset-bottom, 0px))',
|
||||
// height:
|
||||
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',
|
||||
@@ -297,7 +282,7 @@ const NavBar = () => {
|
||||
<ListItemDecorator>
|
||||
<Logout />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>Logout</ListItemContent>
|
||||
<ListItemContent>{t('logout')}</ListItemContent>
|
||||
</ListItemButton>
|
||||
<Typography
|
||||
onClick={
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useLocalization } from '../../contexts/LocalizationContext'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
||||
import { useUserProfile } from '../../queries/UserQueries'
|
||||
import { CompleteSubTask } from '../../utils/Fetcher'
|
||||
@@ -48,6 +49,7 @@ function SortableItem({
|
||||
editMode,
|
||||
performers = [],
|
||||
}) {
|
||||
const { fmt } = useLocalization()
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({
|
||||
id: task.id,
|
||||
@@ -210,7 +212,7 @@ function SortableItem({
|
||||
fontSize: 'sm',
|
||||
}}
|
||||
>
|
||||
{new Date(task.completedAt).toLocaleString()}
|
||||
{fmt.dateTime(task.completedAt)}
|
||||
{performers.find(p => p.userId === task.completedBy) ? (
|
||||
<Chip>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user