diff --git a/public/locales/en/labels.json b/public/locales/en/labels.json
index 93f96cb..990bb5d 100644
--- a/public/locales/en/labels.json
+++ b/public/locales/en/labels.json
@@ -5,5 +5,11 @@
"message": "Are you sure you want to delete this label? This will remove the label from all tasks."
},
"loadError": "Failed to load labels. Please try again.",
+ "search": {
+ "placeholder": "Search labels",
+ "noResultsTitle": "No labels match",
+ "noResultsDescription": "No label matches \"{{searchTerm}}\".",
+ "clear": "Clear search"
+ },
"blurb": "Manage your labels and organize your tasks effectively. Labels will be automatically shared with your circle if they are used on a shared task."
}
diff --git a/src/views/Chores/LocalNotificationScheduler.js b/src/views/Chores/LocalNotificationScheduler.js
index 6642ac1..2a3dfc3 100644
--- a/src/views/Chores/LocalNotificationScheduler.js
+++ b/src/views/Chores/LocalNotificationScheduler.js
@@ -78,11 +78,16 @@ const scheduleNotificationFromTemplate = (
const now = new Date()
const time = getTimeFromTemplate(template, dueDate)
const notificationId = getIdFromTemplate(chore.id, template)
- const { title, body } = getNotificationText(chore.name, template)
+ const { title, body } = getNotificationText(
+ chore.name,
+ template,
+ dueDate,
+ time,
+ )
if (time > now) {
notifications.push({
title,
- body: `${body} at ${time.toLocaleTimeString()}`,
+ body,
id: notificationId,
allowWhileIdle: true,
schedule: {
@@ -96,91 +101,50 @@ const scheduleNotificationFromTemplate = (
}
}
-const getNotificationText = (choreName, template = {}) => {
- // Determine notification type based on template value
- const getNotificationType = () => {
- if (!template || template.value === undefined) {
- return 'due'
- }
+const getNotificationText = (
+ choreName,
+ template = {},
+ dueDate,
+ notificationTime,
+) => {
+ const startOfDay = date =>
+ new Date(date.getFullYear(), date.getMonth(), date.getDate())
+ const dayDifference = Math.round(
+ (startOfDay(dueDate) - startOfDay(notificationTime)) /
+ (24 * 60 * 60 * 1000),
+ )
+ const time = dueDate.toLocaleTimeString([], {
+ hour: 'numeric',
+ minute: '2-digit',
+ })
- if (template.value < 0) {
- return 'reminder'
- } else if (template.value === 0) {
- return 'due'
- } else {
- return 'overdue'
- }
+ let dueTime
+ if (dayDifference === 0) {
+ dueTime = `today at ${time}`
+ } else if (dayDifference === 1) {
+ dueTime = `tomorrow at ${time}`
+ } else if (dayDifference === -1) {
+ dueTime = `yesterday at ${time}`
+ } else {
+ const date = dueDate.toLocaleDateString([], {
+ month: 'short',
+ day: 'numeric',
+ })
+ dueTime = `${date} at ${time}`
}
- const notificationType = getNotificationType()
-
- // Truncate chore name if too long for better readability
- const maxChoreNameLength = 25
- const truncatedName =
- choreName.length > maxChoreNameLength
- ? `${choreName.substring(0, maxChoreNameLength)}...`
- : choreName
-
- // Generate time-based descriptive text
- const getTimeDescription = () => {
- if (!template || !template.value || !template.unit) {
- return 'soon'
- }
-
- const { value, unit } = template
- const absValue = Math.abs(value)
-
- switch (unit) {
- case 'm':
- if (absValue === 1) return value < 0 ? 'in 1 minute' : '1 minute ago'
- if (absValue < 60)
- return value < 0
- ? `in ${absValue} minutes`
- : `${absValue} minutes ago`
- break
- case 'h':
- if (absValue === 1) return value < 0 ? 'in 1 hour' : '1 hour ago'
- if (absValue < 24)
- return value < 0 ? `in ${absValue} hours` : `${absValue} hours ago`
- break
- case 'd':
- if (absValue === 1) return value < 0 ? 'tomorrow' : 'yesterday'
- if (absValue === 7) return value < 0 ? 'next week' : 'last week'
- if (absValue < 7)
- return value < 0 ? `in ${absValue} days` : `${absValue} days ago`
- if (absValue < 30) {
- const weeks = Math.round(absValue / 7)
- return value < 0 ? `in ${weeks} weeks` : `${weeks} weeks ago`
- }
- break
- default:
- return value < 0 ? `in ${absValue} ${unit}` : `${absValue} ${unit} ago`
- }
-
- return value < 0 ? `in ${absValue} ${unit}` : `${absValue} ${unit} ago`
+ let body
+ if (template.value < 0) {
+ body = `Due ${dueTime}`
+ } else if (template.value > 0) {
+ body = `Overdue ยท Was due ${dueTime}`
+ } else {
+ body = 'Due now'
}
- const messages = {
- reminder: {
- title: `๐ ${truncatedName}`,
- body: `Reminder: Due ${getTimeDescription()}`,
- },
- due: {
- title: `๐ ${truncatedName}`,
- body: 'Due now - Time to get started!',
- },
- overdue: {
- title: `โ ${truncatedName}`,
- body: `Overdue ${getTimeDescription()} - Complete when you can`,
- },
- }
-
- // Fallback to due if type not found
- const messageTemplate = messages[notificationType] || messages.due
-
return {
- title: messageTemplate.title,
- body: messageTemplate.body,
+ title: choreName,
+ body,
}
}
const cancelPendingNotifications = async () => {
diff --git a/src/views/Labels/LabelView.jsx b/src/views/Labels/LabelView.jsx
index 841215e..10628b6 100644
--- a/src/views/Labels/LabelView.jsx
+++ b/src/views/Labels/LabelView.jsx
@@ -7,10 +7,12 @@ import {
CircularProgress,
Container,
IconButton,
+ Input,
Stack,
Typography,
} from '@mui/joy'
-import { useEffect, useState } from 'react'
+import Fuse from 'fuse.js'
+import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import LabelModal from '../Modals/Inputs/LabelModal'
@@ -22,7 +24,14 @@ import {
TrailingActions,
} from '@meauxt/react-swipeable-list'
import '@meauxt/react-swipeable-list/dist/styles.css'
-import { Add, MoreVert, Style } from '@mui/icons-material'
+import {
+ Add,
+ Close,
+ MoreVert,
+ Search,
+ SearchOff,
+ Style,
+} from '@mui/icons-material'
import EmptyState from '../../components/common/EmptyState'
import { useQueryClient } from '@tanstack/react-query'
import { useUserProfile } from '../../queries/UserQueries'
@@ -162,6 +171,36 @@ const LabelView = () => {
const queryClient = useQueryClient()
const [confirmationModel, setConfirmationModel] = useState({})
const [showMoreInfoId, setShowMoreInfoId] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+ const searchInputRef = useRef(null)
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(userLabels, {
+ keys: ['name'],
+ includeScore: true,
+ isCaseSensitive: false,
+ findAllMatches: true,
+ }),
+ [userLabels],
+ )
+
+ const filteredLabels = useMemo(() => {
+ if (!searchTerm) {
+ return userLabels
+ }
+ return fuse.search(searchTerm).map(result => result.item)
+ }, [fuse, searchTerm, userLabels])
+
+ const handleSearchChange = e => {
+ setSearchTerm(e.target.value.toLowerCase())
+ setShowMoreInfoId(null)
+ }
+
+ const handleSearchClose = () => {
+ setSearchTerm('')
+ searchInputRef.current?.blur()
+ }
const handleAddLabel = () => {
setCurrentLabel(null)
@@ -251,6 +290,36 @@ const LabelView = () => {
+ {userLabels.length > 0 && (
+
+ }
+ endDecorator={
+ searchTerm && (
+
+
+
+ )
+ }
+ />
+
+ )}
{
}}
/>
)}
+ {userLabels.length > 0 && filteredLabels.length === 0 && (
+ }
+ title={t('search.noResultsTitle')}
+ description={t('search.noResultsDescription', {
+ searchTerm,
+ })}
+ primaryAction={{
+ label: t('search.clear'),
+ onClick: handleSearchClose,
+ }}
+ />
+ )}
- {userLabels.map(label => (
+ {filteredLabels.map(label => (