feat: Enhance UserPoints component with improved filtering and layout

- Updated UserPoints component to include a more user-friendly filter bar with enhanced styling.
- Added a summary section to display the current filter context.
- Refactored user selection and time period filtering logic for better clarity and performance.
- Improved the layout of points cards and history sections for better visual hierarchy.
- Integrated a bar chart for visual representation of points over time.
- Updated redeem points functionality with better user feedback.

feat: Add keyboard shortcuts in AddTaskModal for improved usability

- Implemented keyboard shortcuts for adding descriptions, subtasks, and due dates.
- Enhanced user experience by providing visual hints for keyboard shortcuts.
- Refactored task creation logic to streamline the process.

fix: Refactor ChoreActionMenu to handle mouse events and improve accessibility

- Added mouse enter and leave event handlers for better interaction feedback.
- Adjusted menu positioning for improved usability.

refactor: Update RichTextEditor to support focus handling from parent components

- Converted RichTextEditor to use forwardRef for better integration with parent components.
- Exposed focus and blur methods for external control.
- Improved image upload handling with better error management.

fix: Adjust SubTask component to handle Enter key behavior correctly

- Modified key event handling to prevent unintended task creation when holding meta or ctrl keys.
- Added autoFocus prop to new task input for better user experience.
This commit is contained in:
Mo Tarbin
2025-07-11 20:10:28 -04:00
parent c2f5569010
commit 953c62cc66
42 changed files with 6262 additions and 1887 deletions

View File

@@ -1,128 +1,222 @@
import { Capacitor } from '@capacitor/core';
import { LocalNotifications } from '@capacitor/local-notifications';
import { Preferences } from '@capacitor/preferences';
import { Capacitor } from '@capacitor/core'
import { LocalNotifications } from '@capacitor/local-notifications'
import { Preferences } from '@capacitor/preferences'
import murmurhash from 'murmurhash'
const getNotificationPreferences = async () => {
const ret = await Preferences.get({ key: 'notificationPreferences' });
return JSON.parse(ret.value);
};
const canScheduleNotification = () => {
if (Capacitor.isNativePlatform() === false) {
return false;
}
const notificationPreferences = getNotificationPreferences();
if (notificationPreferences["granted"] === false) {
return false;
}
return true;
const ret = await Preferences.get({ key: 'notificationPreferences' })
return JSON.parse(ret.value)
}
const canScheduleNotification = async () => {
if (Capacitor.isNativePlatform() === false) {
return false
}
const notificationPreferences = await getNotificationPreferences()
console.log('Notification preferences:', notificationPreferences)
const scheduleChoreNotification = async (chores, userProfile,allPerformers) => {
// for each chore will create local notification:
const notifications = [];
if (notificationPreferences['granted'] === false) {
return false
}
return true
}
const getIdFromTemplate = (choreId, template) => {
// convert to base 32 int for notification id using murmurhash :
return murmurhash.v3(`${choreId}-${template.value}-${template.unit}`)
}
const getTimeFromTemplate = (template, relativeTime) => {
let time = relativeTime
switch (template.unit) {
case 'm':
time = new Date(relativeTime.getTime() + template.value * 60 * 1000)
break
case 'h':
time = new Date(relativeTime.getTime() + template.value * 60 * 60 * 1000)
break
case 'd':
time = new Date(
relativeTime.getTime() + template.value * 24 * 60 * 60 * 1000,
)
break
default:
time = relativeTime
}
return time
}
const scheduleNotificationFromTemplate = (
chore,
userProfile,
allPerformers,
notifications,
) => {
for (const template of chore.notificationMetadata?.templates || []) {
// convert the template to time:
console.log(
'Scheduling notification for chore:',
chore.id,
'with template:',
template,
)
const dueDate = new Date(chore.nextDueDate)
const now = new Date()
const devicePreferences = await getNotificationPreferences();
for (let i = 0; i < chores.length; i++) {
const time = getTimeFromTemplate(template, dueDate)
const notificationId = getIdFromTemplate(chore.id, template)
const { title, body } = getNotificationText(chore.name, template)
if (time > now) {
notifications.push({
title,
body: `${body} at ${time.toLocaleTimeString()}`,
id: notificationId,
allowWhileIdle: true,
schedule: {
at: time,
},
extra: {
choreId: chore.id,
},
})
}
}
}
const chore = chores[i];
const chorePreferences = JSON.parse(chore.notificationMetadata)
if ( chore.notification ===false || chore.nextDueDate === null) {
continue;
const getNotificationText = (choreName, template = {}) => {
// Determine notification type based on template value
const getNotificationType = () => {
if (!template || template.value === undefined) {
return 'due'
}
if (template.value < 0) {
return 'reminder' // Before due date
} else if (template.value === 0) {
return 'due' // Due now
} else {
return 'overdue' // After due date
}
}
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`
}
scheduleDueNotification(chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications)
schedulePreDueNotification(chore, userProfile, allPerformers,chorePreferences, devicePreferences,notifications)
scheduleNaggingNotification(chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications)
break
default:
return value < 0 ? `in ${absValue} ${unit}` : `${absValue} ${unit} ago`
}
LocalNotifications.schedule({
return value < 0 ? `in ${absValue} ${unit}` : `${absValue} ${unit} ago`
}
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,
}
}
const cancelPendingNotifications = async () => {
try {
const pending = await LocalNotifications.getPending()
if (pending.notifications.length > 0) {
await LocalNotifications.cancel({ notifications: pending.notifications })
console.log('Cancelled pending notifications:', pending.notifications)
} else {
console.log('No pending notifications to cancel.')
}
} catch (error) {
console.error('Error cancelling pending notifications:', error)
}
}
const scheduleChoreNotification = async (
chores,
userProfile,
allPerformers,
) => {
await cancelPendingNotifications()
const notifications = []
const devicePreferences = await getNotificationPreferences()
for (let i = 0; i < chores.length; i++) {
const chore = chores[i]
try {
if (chore.notification === false || chore.nextDueDate === null) {
continue
}
scheduleNotificationFromTemplate(
chore,
userProfile,
allPerformers,
notifications,
});
)
} catch (error) {
console.error(
'Error parsing notification metadata for chore:',
chore.id,
error,
)
continue
}
}
LocalNotifications.schedule({
notifications,
})
console.log('Scheduled notifications:', notifications)
}
const scheduleDueNotification = (chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications) => {
if (devicePreferences['dueNotification'] !== true || chorePreferences['dueDate'] !== true){
return
}
const nextDueDate = new Date(chore.nextDueDate)
const diff = nextDueDate - now
if (diff < 0) {
return
}
const notification = {
title: `${chore.name} is due! 🕒`,
body: userProfile.id === chore.assignedTo ? `It's assigned to you!` : `It is ${allPerformers[chore.assignedTo].name}'s turn`,
id: chore.id,
allowWhileIdle: true,
schedule: {
at: new Date(chore.nextDueDate),
},
extra: {
choreId: chore.id,
},
};
notifications.push(notification);
}
const schedulePreDueNotification = (chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications) => {
if (devicePreferences['preDueNotification'] !== true || chorePreferences['preDue'] !== true){
return
}
const nextDueDate = new Date(chore.nextDueDate)
const diff = nextDueDate - now
if (diff < 0 || userProfile.id !== chore.assignedTo) {
return
}
const notification = {
title: `${chore.name} is due soon! 🕒`,
body: `is due at ${nextDueDate.toLocaleTimeString()}`,
id: chore.id,
allowWhileIdle: true,
schedule: {
// 1 hour before
at: new Date(nextDueDate - 60 * 60 * 1000),
},
extra: {
choreId: chore.id,
},
};
notifications.push(notification);
}
const scheduleNaggingNotification = (chore, userProfile, allPerformers,chorePreferences,devicePreferences, notifications) => {
if (devicePreferences['naggingNotification'] === false || chorePreferences.nagging !== true){
return
}
const nextDueDate = new Date(chore.nextDueDate)
const diff = nextDueDate - now
if (diff > 0 || userProfile.id !== chore.assignedTo) {
return
}
const notification = {
title: `${chore.name} is overdue! 🕒`,
body: `❗ It was due at ${nextDueDate.toLocaleTimeString()}`,
id: chore.id,
allowWhileIdle: true,
schedule: {
at: new Date(chore.nextDueDate),
},
extra: {
choreId: chore.id,
},
};
notifications.push(notification);
}
export{ scheduleChoreNotification, canScheduleNotification }
export { canScheduleNotification, scheduleChoreNotification }