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:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user