Summary
- This is a summary of your chores
+ {t('summaryOfChores')}
diff --git a/src/views/TestView/AutocompleteDropdown.jsx b/src/views/TestView/AutocompleteDropdown.jsx
index aeb77a9..dae4f5b 100644
--- a/src/views/TestView/AutocompleteDropdown.jsx
+++ b/src/views/TestView/AutocompleteDropdown.jsx
@@ -3,8 +3,6 @@ import { Add } from '@mui/icons-material'
import { Divider, Menu, MenuItem } from '@mui/joy'
import React, { useEffect } from 'react'
-import { Z_INDEX } from '../../constants/zIndex'
-
const AutocompleteDropdown = ({
currentValue,
onCreateSuggestion, // Called when the "Create new" row is chosen
@@ -62,7 +60,6 @@ const AutocompleteDropdown = ({
position: 'relative',
bottom: 0,
left: 0,
- zIndex: Z_INDEX.MODAL_POPOVER,
}}
>
{filteredOptions.map((option, index) => (
diff --git a/src/views/Things/ThingsHistory.jsx b/src/views/Things/ThingsHistory.jsx
index 0b53c8e..38b7379 100644
--- a/src/views/Things/ThingsHistory.jsx
+++ b/src/views/Things/ThingsHistory.jsx
@@ -10,6 +10,7 @@ import {
TrendingUp,
Update,
} from '@mui/icons-material'
+import { useTranslation } from 'react-i18next'
import {
Avatar,
Box,
@@ -41,6 +42,7 @@ import { useThingHistory } from '../../queries/ThingQueries'
import LoadingComponent from '../components/Loading'
const ThingsHistory = () => {
+ const { t } = useTranslation('things')
const { id } = useParams()
const theme = useTheme()
const { fmt } = useLocalization()
@@ -186,7 +188,7 @@ const ThingsHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Things Overview
+ {t('history.overview')}
@@ -266,7 +268,7 @@ const ThingsHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Data Visualization
+ {t('history.visualization')}
)}
@@ -328,7 +330,7 @@ const ThingsHistory = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Change History
+ {t('history.changeHistory')}
@@ -379,7 +381,7 @@ const ThingsHistory = () => {
display: { xs: 'none', sm: 'block' },
}}
>
- Updated
+ {t('history.updated')}
{
const aggregated = {}
for (let i = 0; i < history.length; i++) {
const item = history[i]
- const date = new Date(
- item.performedAt || item.updatedAt,
- ).toLocaleDateString()
+ // Key by a stable local ISO day (YYYY-MM-DD) so the render-time
+ // formatter (fmt.date) receives a parseable date instead of a
+ // locale-formatted string, which produced "Invalid date".
+ const d = new Date(item.performedAt || item.updatedAt || item.createdAt)
+ const date = isNaN(d.getTime())
+ ? 'unknown'
+ : `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
+ d.getDate(),
+ ).padStart(2, '0')}`
if (!aggregated[date]) {
aggregated[date] = []
}
@@ -83,6 +90,7 @@ const ChoreHistoryItem = ({
onViewNote,
onViewDetails,
}) => {
+ const { t } = useTranslation('history')
const cfg = statusConfig[status] ?? statusConfig[1]
return (
@@ -136,7 +144,7 @@ const ChoreHistoryItem = ({
{points && (
}>
- {`${points} points`}
+ {t('detail.points', { count: points })}
)}
{notes && (
@@ -151,7 +159,7 @@ const ChoreHistoryItem = ({
onViewNote?.(notes)
}}
>
- Note
+ {t('detail.note')}
)}
@@ -174,7 +182,7 @@ const ChoreHistoryTimeline = ({
{Object.entries(groupedHistory).map(([date, items]) => (
- {fmt.date(date)}
+ {date === 'unknown' ? '—' : fmt.date(date)}
@@ -197,7 +205,7 @@ const ChoreHistoryTimeline = ({
)
}
-const renderPieChart = (data, size, isPrimary, chartType = null) => {
+const renderPieChart = (t, data, size, isPrimary, chartType = null) => {
// Filter out items with zero or negative values
const validData = data.filter(item => item.value > 0)
@@ -216,7 +224,7 @@ const renderPieChart = (data, size, isPrimary, chartType = null) => {
}}
>
- No data available
+ {t('charts.noData')}
)
@@ -400,6 +408,7 @@ const USER_FILTER = (history, userId) => {
}
const UserActivites = () => {
+ const { t } = useTranslation('history')
const { data: userProfile } = useUserProfile()
const [tabValue, setTabValue] = React.useState(7)
@@ -448,16 +457,16 @@ const UserActivites = () => {
() => [
{
id: 'status',
- label: 'Status',
+ label: t('filter.status'),
type: 'multi-select',
icon: ,
options: [
- { value: 1, label: 'Completed', color: 'success', icon: },
- { value: 2, label: 'Skipped', color: 'warning', icon: },
- { value: 3, label: 'Pending', color: 'neutral', icon: },
- { value: 4, label: 'Rejected', color: 'danger', icon: },
- { value: 5, label: 'Missed', color: 'danger', icon: },
- { value: 6, label: 'Rescheduled', color: 'warning', icon: },
+ { value: 1, label: t('status.completed'), color: 'success', icon: },
+ { value: 2, label: t('status.skipped'), color: 'warning', icon: },
+ { value: 3, label: t('filter.pending'), color: 'neutral', icon: },
+ { value: 4, label: t('status.rejected'), color: 'danger', icon: },
+ { value: 5, label: t('status.missed'), color: 'danger', icon: },
+ { value: 6, label: t('status.rescheduled'), color: 'warning', icon: },
],
filterFn: (item, values) => values.includes(item.status),
},
@@ -465,7 +474,7 @@ const UserActivites = () => {
? [
{
id: 'label',
- label: 'Labels',
+ label: t('filter.labels'),
type: 'multi-select',
icon: ,
options: userLabels.map(l => ({
@@ -492,20 +501,20 @@ const UserActivites = () => {
: []),
{
id: 'hasNotes',
- label: 'Has Notes',
+ label: t('filter.hasNotes'),
type: 'boolean',
icon: ,
filterFn: item => !!item.notes,
},
{
id: 'hasPoints',
- label: 'Has Points',
+ label: t('filter.hasPoints'),
type: 'boolean',
icon: ,
filterFn: item => (item.points ?? 0) > 0,
},
],
- [userLabels],
+ [userLabels, t],
)
const {
@@ -520,20 +529,20 @@ const UserActivites = () => {
() => [
{
id: 'timePeriod',
- label: 'Time Period',
+ label: t('filter.timePeriod'),
type: 'single-select',
icon: ,
defaultValue: 7,
options: [
- { value: 7, label: '7 Days' },
- { value: 30, label: '30 Days' },
- { value: 90, label: '90 Days' },
- { value: 365, label: 'All Time' },
+ { value: 7, label: t('period.days', { count: 7 }) },
+ { value: 30, label: t('period.days', { count: 30 }) },
+ { value: 90, label: t('period.days', { count: 90 }) },
+ { value: 365, label: t('period.allTime') },
],
},
{
id: 'completedBy',
- label: 'User',
+ label: t('filter.user'),
type: 'single-select',
icon: ,
options: circleUsers.map(u => ({
@@ -544,7 +553,7 @@ const UserActivites = () => {
},
...clientFilterDefs,
],
- [circleUsers, clientFilterDefs],
+ [circleUsers, clientFilterDefs, t],
)
// Merge server-driven and client-driven active filter states for the bar
@@ -686,7 +695,7 @@ const UserActivites = () => {
// Add unlabeled tasks if there are any
if (unlabeledCount > 0) {
result.push({
- label: 'No Labels',
+ label: t('charts.noLabels'),
value: unlabeledCount,
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
@@ -709,7 +718,9 @@ const UserActivites = () => {
const assignee = circleUsers.find(
user => user.userId === chore.assignedTo,
)
- const assigneeName = assignee ? assignee.displayName : 'Unassigned'
+ const assigneeName = assignee
+ ? assignee.displayName
+ : t('charts.unassigned')
const assigneeId = chore.assignedTo || 'unassigned'
if (assigneeCounts[assigneeId]) {
@@ -802,7 +813,7 @@ const UserActivites = () => {
// Add unlabeled tasks duration if there is any
if (unlabeledDuration > 0) {
result.push({
- label: 'No Labels',
+ label: t('charts.noLabels'),
value: Math.round((unlabeledDuration / 3600) * 10) / 10, // Convert to hours and round to 1 decimal
color: TASK_COLOR.ANYTIME,
id: 'unlabeled',
@@ -823,7 +834,7 @@ const UserActivites = () => {
// Iterate through ChoreHistory to get actual time spent per task
history.forEach(historyItem => {
const duration = historyItem.duration || 0 // duration in seconds from ChoreHistory
- const taskName = historyItem.choreName || 'Unknown Task'
+ const taskName = historyItem.choreName || t('charts.unknownTask')
if (taskDurations[taskName]) {
taskDurations[taskName].duration += duration
@@ -886,7 +897,7 @@ const UserActivites = () => {
if (totalCompleted > 0) {
result.push({
- label: `On time`,
+ label: t('badge.onTimeLabel'),
value: totalCompleted,
color: TASK_COLOR.COMPLETED,
id: 1,
@@ -895,7 +906,7 @@ const UserActivites = () => {
if (totalLate > 0) {
result.push({
- label: `Late`,
+ label: t('charts.late'),
value: totalLate,
color: TASK_COLOR.LATE,
id: 2,
@@ -904,7 +915,7 @@ const UserActivites = () => {
if (totalNoDueDate > 0) {
result.push({
- label: `Completed`,
+ label: t('status.completed'),
value: totalNoDueDate,
color: TASK_COLOR.ANYTIME,
id: 3,
@@ -919,43 +930,43 @@ const UserActivites = () => {
const chartData = {
history: {
data: historyPieChartData || [],
- title: 'Status',
- description: 'Completed tasks status',
+ title: t('charts.status.title'),
+ description: t('charts.status.description'),
},
due: {
data: choreDuePieChartData || [],
- title: 'Due Date',
- description: 'Current tasks due date',
+ title: t('charts.due.title'),
+ description: t('charts.due.description'),
},
// assigned: {
// data: choresAssignedChartData,
- // title: 'Assigned to me',
+ // title: t('chores:sort.assignedToMe'),
// description: 'Tasks assigned to you vs others',
// },
priority: {
data: choresPriorityChartData || [],
- title: 'Priority',
- description: 'Tasks by priority',
+ title: t('charts.priority.title'),
+ description: t('charts.priority.description'),
},
labels: {
data: choresLabelsChartData || [],
- title: 'Labels',
- description: 'Tasks by labels',
+ title: t('charts.labels.title'),
+ description: t('charts.labels.description'),
},
labelsDuration: {
data: choresLabelsDurationChartData || [],
- title: 'Labels (time)',
- description: 'Time spent by labels (hours)',
+ title: t('charts.labelsDuration.title'),
+ description: t('charts.labelsDuration.description'),
},
tasksTime: {
data: tasksTimeChartData || [],
- title: 'Tasks (time)',
- description: 'Time spent by individual tasks (hours)',
+ title: t('charts.tasksTime.title'),
+ description: t('charts.tasksTime.description'),
},
assigneeBreakdown: {
data: choresAssigneeBreakdownChartData || [],
- title: 'by Assignee',
- description: 'Tasks grouped by assignee',
+ title: t('charts.assigneeBreakdown.title'),
+ description: t('charts.assigneeBreakdown.description'),
},
}
if (!userProfile) {
@@ -976,7 +987,7 @@ const UserActivites = () => {
level='title-md'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- Activities
+ {t('activities.title')}
@@ -1025,7 +1036,7 @@ const UserActivites = () => {
onViewNote={notes => {
setNoteViewerConfig({
isOpen: true,
- title: 'Note',
+ title: t('detail.note'),
content: notes,
onClose: () => setNoteViewerConfig({ isOpen: false }),
})
@@ -1153,6 +1164,7 @@ const UserActivites = () => {
}}
>
{renderPieChart(
+ t,
chartData[selectedChart].data,
300, // Increased size for better chart container
true,
@@ -1216,7 +1228,7 @@ const UserActivites = () => {
alignItems: 'center',
}}
>
- {renderPieChart(data, 70, false)}
+ {renderPieChart(t, data, 70, false)}
diff --git a/src/views/User/UserPoints.jsx b/src/views/User/UserPoints.jsx
index 7ab2dee..93c7979 100644
--- a/src/views/User/UserPoints.jsx
+++ b/src/views/User/UserPoints.jsx
@@ -40,6 +40,8 @@ import {
Typography,
} from '@mui/joy'
import { useEffect, useState } from 'react'
+import moment from 'moment'
+import { useTranslation } from 'react-i18next'
import LoadingComponent from '../components/Loading.jsx'
import { useChoresHistory } from '../../queries/ChoreQueries.jsx'
@@ -48,6 +50,7 @@ import { RedeemPoints } from '../../utils/Fetcher.jsx'
import { resolvePhotoURL } from '../../utils/Helpers.jsx'
import RedeemPointsModal from '../Modals/RedeemPointsModal'
const UserPoints = () => {
+ const { t } = useTranslation('points')
const [tabValue, setTabValue] = useState(7)
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
@@ -104,15 +107,13 @@ const UserPoints = () => {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
- label: currentDate.toLocaleString('en-US', { weekday: 'short' }),
+ label: moment(currentDate).format('ddd'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
- weekday: 'short',
- })
+ const dayName = moment(chore.performedAt).format('ddd')
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
@@ -133,15 +134,13 @@ const UserPoints = () => {
const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - i)
daysAggregated.push({
- label: currentDate.toLocaleString('en-US', { day: 'numeric' }),
+ label: moment(currentDate).format('D'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const dayName = new Date(chore.performedAt).toLocaleString('en-US', {
- day: 'numeric',
- })
+ const dayName = moment(chore.performedAt).format('D')
const dayIndex = daysAggregated.findIndex(dayData => {
if (userId)
@@ -164,15 +163,13 @@ const UserPoints = () => {
const currentMonth = new Date()
currentMonth.setMonth(currentMonth.getMonth() - i)
monthlyAggregated.push({
- label: currentMonth.toLocaleString('en-US', { month: 'short' }),
+ label: moment(currentMonth).format('MMM'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const monthName = new Date(chore.performedAt).toLocaleString('en-US', {
- month: 'short',
- })
+ const monthName = moment(chore.performedAt).format('MMM')
const monthIndex = monthlyAggregated.findIndex(monthData => {
if (userId)
@@ -195,15 +192,13 @@ const UserPoints = () => {
const currentYear = new Date()
currentYear.setFullYear(currentYear.getFullYear() - i)
yearlyAggregated.push({
- label: currentYear.toLocaleString('en-US', { year: 'numeric' }),
+ label: moment(currentYear).format('YYYY'),
points: 0,
tasks: 0,
})
}
history.forEach(chore => {
- const yearName = new Date(chore.performedAt).toLocaleString('en-US', {
- year: 'numeric',
- })
+ const yearName = moment(chore.performedAt).format('YYYY')
const yearIndex = yearlyAggregated.findIndex(yearData => {
if (userId)
@@ -305,14 +300,14 @@ const UserPoints = () => {
level='h3'
sx={{ fontWeight: 'lg', color: 'text.primary' }}
>
- {leaderboardMode === 'points' ? 'Points' : 'Tasks'} Leaderboard
+ {leaderboardMode === 'points'
+ ? t('leaderboard.titlePoints')
+ : t('leaderboard.titleTasks')}
- Rankings based on{' '}
{leaderboardMode === 'points'
- ? 'points earned'
- : 'tasks completed'}{' '}
- during the selected time period
+ ? t('leaderboard.subtitlePoints')
+ : t('leaderboard.subtitleTasks')}
@@ -358,9 +353,9 @@ const UserPoints = () => {
}}
>
{[
- { label: '7D', value: 7 },
- { label: '6M', value: 6 * 30 },
- { label: 'All', value: 24 * 30 },
+ { label: t('tabs.short7d'), value: 7 },
+ { label: t('tabs.short6m'), value: 6 * 30 },
+ { label: t('tabs.shortAll'), value: 24 * 30 },
].map((tab, index) => (
{
sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('points')}
>
- Points
+ {t('leaderboard.modePoints')}
{
sx={{ cursor: 'pointer' }}
onClick={() => setLeaderboardMode('tasks')}
>
- Tasks
+ {t('leaderboard.modeTasks')}
@@ -512,7 +507,7 @@ const UserPoints = () => {
color='primary'
sx={{ ml: 1 }}
>
- You
+ {t('leaderboard.you')}
)}
@@ -520,8 +515,10 @@ const UserPoints = () => {
level='body-xs'
sx={{ color: 'text.secondary' }}
>
- {user.periodTasks} tasks • {user.avgPointsPerTask} avg
- per task
+ {t('leaderboard.tasksAndAvg', {
+ tasks: user.periodTasks,
+ avg: user.avgPointsPerTask,
+ })}
@@ -552,8 +549,12 @@ const UserPoints = () => {
{leaderboardMode === 'points'
- ? `${user.availablePoints} available`
- : `${user.periodPoints} points`}
+ ? t('leaderboard.available', {
+ count: user.availablePoints,
+ })
+ : t('leaderboard.points', {
+ count: user.periodPoints,
+ })}
@@ -591,7 +592,7 @@ const UserPoints = () => {
- Filter & Analysis
+ {t('filter.analysisTitle')}
@@ -610,7 +611,7 @@ const UserPoints = () => {
>
- Filter Points
+ {t('filter.title')}
{
{/* User Filter */}
- Show points for:
+ {t('filter.showFor')}
diff --git a/src/views/components/AddTaskModal.jsx b/src/views/components/AddTaskModal.jsx
index 10849b8..a59bef1 100644
--- a/src/views/components/AddTaskModal.jsx
+++ b/src/views/components/AddTaskModal.jsx
@@ -1,5 +1,14 @@
-import { Add } from '@mui/icons-material'
-import { Box, Button, Typography } from '@mui/joy'
+import { Add, KeyboardArrowDown } from '@mui/icons-material'
+import {
+ Box,
+ Button,
+ Dropdown,
+ ListItemDecorator,
+ Menu,
+ MenuButton,
+ MenuItem,
+ Typography,
+} from '@mui/joy'
import { useMediaQuery } from '@mui/material'
import { useQueryClient } from '@tanstack/react-query'
import * as chrono from 'chrono-node'
@@ -20,6 +29,7 @@ import LABEL_COLORS, { TASK_COLOR } from '../../utils/Colors'
import { CreateLabel } from '../../utils/Fetcher'
import { imageSourceToFile } from '../../utils/FileConvert'
import { isPlusAccount } from '../../utils/Helpers'
+import { getIconComponent } from '../../utils/ProjectIcons'
import { generateUUID } from '../../utils/UUID'
import { useLabels } from '../Labels/LabelQueries'
import { useProjects } from '../Projects/ProjectQueries'
@@ -122,6 +132,13 @@ const getInitialProject = () => {
return 'default'
}
+const DEFAULT_PROJECT = {
+ id: 'default',
+ name: 'Default Project',
+ color: '#9CA3AF',
+ icon: 'FolderOpen',
+}
+
const PRIORITY_COLORS = {
0: TASK_COLOR.NO_PRIORITY,
1: TASK_COLOR.PRIORITY_1,
@@ -175,7 +192,7 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
const { data: userLabels, isLoading: userLabelsLoading } = useLabels()
const { data: circleMembers, isLoading: isCircleMembersLoading } =
useCircleMembers()
- const { isLoading: isProjectsLoading } = useProjects()
+ const { data: projects, isLoading: isProjectsLoading } = useProjects()
const createChoreMutation = useCreateChore()
const queryClient = useQueryClient()
@@ -298,6 +315,17 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
const [useCustomTime, setUseCustomTime] = useState(false)
const [showKeyboardShortcuts, setShowKeyboardShortcuts] = useState(false)
const [projectId, setProjectId] = useState(getInitialProject)
+ const selectedProject = useMemo(
+ () =>
+ (projectId !== 'default' &&
+ projects?.find(project => project.id === projectId)) ||
+ DEFAULT_PROJECT,
+ [projects, projectId],
+ )
+ const SelectedProjectIcon = useMemo(
+ () => getIconComponent(selectedProject.icon),
+ [selectedProject],
+ )
const [attachments, setAttachments] = useState([])
const [draftId, setDraftId] = useState(() => generateUUID())
@@ -1131,6 +1159,50 @@ const TaskInput = ({ initialMode, isModalOpen, onChoreUpdate, onClose }) => {
title='Create new task'
footer={
+ {!showScan && !showVoice && projects?.length >= 1 && (
+
+
+ }
+ endDecorator={}
+ sx={{
+ mr: 'auto',
+ color: 'text.secondary',
+ fontWeight: 'normal',
+ }}
+ >
+ {selectedProject.name}
+
+
+
+ )}
diff --git a/src/views/components/NavBar.jsx b/src/views/components/NavBar.jsx
index 1541fc4..9b16cc4 100644
--- a/src/views/components/NavBar.jsx
+++ b/src/views/components/NavBar.jsx
@@ -2,6 +2,7 @@ import { Capacitor } from '@capacitor/core'
import {
Archive,
ArrowBack,
+ BugReport,
FilterAlt,
FolderOpen,
History,
@@ -30,10 +31,11 @@ import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
import { version } from '../../../package.json'
import UserProfileAvatar from '../../components/UserProfileAvatar'
-import Z_INDEX from '../../constants/zIndex'
+import { useLocalization } from '../../contexts/LocalizationContext'
import { useResource } from '../../queries/ResourceQueries'
import { useGlobalSearch } from '../../search/GlobalSearchContext'
import { apiClient } from '../../utils/ApiClient'
+import ErrorReportModal from '../Modals/ErrorReportModal'
import NavBarLink from './NavBarLink'
import SyncStatusIndicator from './SyncStatusIndicator'
@@ -45,6 +47,7 @@ const NavBar = () => {
const navigate = useNavigate()
const [drawerOpen, setDrawerOpen] = useState(false)
+ const [bugReportOpen, setBugReportOpen] = useState(false)
const links = [
{
@@ -206,7 +209,7 @@ const NavBar = () => {
? `calc(var(--safe-area-inset-top, 0px))`
: '',
position: 'sticky',
- zIndex: Z_INDEX.NAVBAR,
+ zIndex: 'var(--joy-zIndex-popup)',
top: 0,
minHeight: '35px',
backgroundColor: 'var(--joy-palette-background-body)',
@@ -239,7 +242,6 @@ const NavBar = () => {
// height:
// 'calc(100vh - var(--safe-area-inset-top, 0px) - var(--safe-area-inset-bottom, 0px))',
overflow: 'auto',
- zIndex: Z_INDEX.DRAWER,
},
}}
>
@@ -296,6 +298,17 @@ const NavBar = () => {
Upgrade to Plus
*/}
+ setBugReportOpen(true)}
+ sx={{
+ py: 1.2,
+ }}
+ >
+
+
+
+ {t('navigation.reportBug')}
+
{
apiClient.handleLogout()
@@ -329,6 +342,10 @@ const NavBar = () => {
+