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 => (