From e7eb060643946827be267dac496233f2bfec7d86 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 29 May 2026 11:53:03 +0200 Subject: [PATCH] feat: add location autocomplete and custom reminder times to event modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Location field shows dropdown suggestions from previously saved locations as the user types; selected/submitted locations are persisted to profile.viewSettings.savedLocations (max 20, most recent first) - Reminder dropdown gains a "Custom…" option that opens an inline editor for entering X minutes / hours / days before the event; confirmed custom values are persisted to profile.viewSettings.customReminderMinutes (max 10) and appear in future reminder dropdowns v1.112.0 --- package.json | 2 +- src/components/CalendarEventModal.tsx | 238 +++++++++++++++++++++++--- 2 files changed, 215 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 8481648..0fcb189 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.111.6", + "version": "1.112.0", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/components/CalendarEventModal.tsx b/src/components/CalendarEventModal.tsx index 9934afc..72086a5 100644 --- a/src/components/CalendarEventModal.tsx +++ b/src/components/CalendarEventModal.tsx @@ -1,5 +1,5 @@ -import React, { useState, useEffect, lazy, Suspense, useRef } from 'react'; -import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity } from 'lucide-react'; +import React, { useState, useEffect, lazy, Suspense, useRef, useCallback } from 'react'; +import { ChevronDown, ChevronUp, Plus, X, Bell, Users, Paperclip, MapPin, Calendar, Clock, Repeat, Link2, Eye, Activity, Check } from 'lucide-react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faGoogle, faApple, faMicrosoft } from '@fortawesome/free-brands-svg-icons'; import { faServer } from '@fortawesome/free-solid-svg-icons'; @@ -27,7 +27,7 @@ function AnimatedDots() { } // Reminder preset options (minutes) -const REMINDER_OPTIONS = [ +const REMINDER_PRESETS = [ { label: 'None', value: -1 }, { label: 'At time of event', value: 0 }, { label: '5 minutes before', value: 5 }, @@ -40,6 +40,25 @@ const REMINDER_OPTIONS = [ { label: '1 week before', value: 10080 }, ]; +const CUSTOM_REMINDER_SENTINEL = -9999; + +function formatReminderMinutes(minutes: number, language: string): string { + if (minutes === 0) return language === 'de' ? 'Zum Zeitpunkt' : 'At time of event'; + if (minutes % 10080 === 0) { + const w = minutes / 10080; + return language === 'de' ? `${w} ${w === 1 ? 'Woche' : 'Wochen'} vorher` : `${w} week${w !== 1 ? 's' : ''} before`; + } + if (minutes % 1440 === 0) { + const d = minutes / 1440; + return language === 'de' ? `${d} ${d === 1 ? 'Tag' : 'Tage'} vorher` : `${d} day${d !== 1 ? 's' : ''} before`; + } + if (minutes % 60 === 0) { + const h = minutes / 60; + return language === 'de' ? `${h} ${h === 1 ? 'Stunde' : 'Stunden'} vorher` : `${h} hour${h !== 1 ? 's' : ''} before`; + } + return language === 'de' ? `${minutes} Minuten vorher` : `${minutes} minutes before`; +} + const BUSY_STATUS_OPTIONS = [ { label: 'Busy', value: 'busy' }, { label: 'Free', value: 'free' }, @@ -63,6 +82,10 @@ interface CalendarEventModalProps { connections: any[]; weekStartDay?: number; // 0=Sunday, 1=Monday language?: string; + savedLocations?: string[]; + onSaveLocation?: (loc: string) => void; + customReminderMinutes?: number[]; + onSaveCustomReminder?: (minutes: number) => void; onClose: () => void; onSave: (eventData: any) => Promise; onDelete?: (eventId: string, calendarId: string, deleteMode?: string) => Promise; @@ -76,6 +99,10 @@ export default function CalendarEventModal({ connections, weekStartDay = 0, language = 'en', + savedLocations = [], + onSaveLocation, + customReminderMinutes = [], + onSaveCustomReminder, onClose, onSave, onDelete @@ -167,6 +194,53 @@ export default function CalendarEventModal({ const calendarSelectorRef = useRef(null); const dialogRef = useRef(null); + // Location autocomplete + const [locationFocused, setLocationFocused] = useState(false); + const [locationSuggestions, setLocationSuggestions] = useState([]); + const locationRef = useRef(null); + + const updateLocationSuggestions = useCallback((val: string) => { + if (!val.trim() || savedLocations.length === 0) { + setLocationSuggestions([]); + return; + } + const lower = val.toLowerCase(); + const matches = savedLocations.filter(l => l.toLowerCase().includes(lower) && l !== val); + setLocationSuggestions(matches.slice(0, 6)); + }, [savedLocations]); + + // Custom reminder state + // customReminderIdx tracks which reminder row is showing the custom editor + const [customEditorIdx, setCustomEditorIdx] = useState(null); + const [customAmount, setCustomAmount] = useState(30); + const [reminderUnit, setReminderUnit] = useState<'minutes' | 'hours' | 'days'>('minutes'); + + const customAmountToMinutes = () => { + if (reminderUnit === 'hours') return customAmount * 60; + if (reminderUnit === 'days') return customAmount * 1440; + return customAmount; + }; + + // Build full reminder options list: presets + saved custom + "Custom..." + const buildReminderOptions = () => [ + ...REMINDER_PRESETS, + ...customReminderMinutes + .filter(m => !REMINDER_PRESETS.some(p => p.value === m)) + .map(m => ({ label: formatReminderMinutes(m, language), value: m })), + { label: language === 'de' ? 'Benutzerdefiniert…' : 'Custom…', value: CUSTOM_REMINDER_SENTINEL }, + ]; + + // Close location suggestions on outside click + useEffect(() => { + const handler = (e: MouseEvent) => { + if (locationRef.current && !locationRef.current.contains(e.target as Node)) { + setLocationSuggestions([]); + } + }; + document.addEventListener('mousedown', handler); + return () => document.removeEventListener('mousedown', handler); + }, []); + // Focus trap useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -261,6 +335,11 @@ export default function CalendarEventModal({ return; } + // Save location to user profile if non-empty and new + if (location.trim() && onSaveLocation) { + onSaveLocation(location.trim()); + } + setIsSaving(true); setError(''); try { @@ -348,11 +427,29 @@ export default function CalendarEventModal({ const updateReminder = (index: number, minutes: number) => { if (minutes === -1) { setReminders(reminders.filter((_, i) => i !== index)); + setCustomEditorIdx(null); + return; + } + if (minutes === CUSTOM_REMINDER_SENTINEL) { + setCustomEditorIdx(index); + setCustomAmount(30); + setReminderUnit('minutes'); return; } const updated = [...reminders]; updated[index] = { ...updated[index], minutes }; setReminders(updated); + setCustomEditorIdx(null); + }; + + const confirmCustomReminder = (index: number) => { + const mins = customAmountToMinutes(); + if (mins <= 0) return; + const updated = [...reminders]; + updated[index] = { ...updated[index], minutes: mins }; + setReminders(updated); + setCustomEditorIdx(null); + if (onSaveCustomReminder) onSaveCustomReminder(mins); }; const addReminder = () => { @@ -687,24 +784,61 @@ export default function CalendarEventModal({ )} - {/* Location */} -
+ {/* Location with autocomplete */} +
setLocation(e.target.value)} + onChange={e => { setLocation(e.target.value); updateLocationSuggestions(e.target.value); }} + onFocus={() => { setLocationFocused(true); updateLocationSuggestions(location); }} + onBlur={() => setTimeout(() => setLocationSuggestions([]), 150)} + onKeyDown={e => { if (e.key === 'Escape') setLocationSuggestions([]); }} placeholder={language === 'de' ? 'Ort hinzufügen' : 'Add location'} aria-label={language === 'de' ? 'Ort' : 'Location'} + aria-autocomplete="list" + aria-expanded={locationSuggestions.length > 0} style={{ ...fieldCol, padding: '2px 0', border: 'none', background: 'transparent', outline: 'none', fontSize: '0.8rem', color: 'var(--weekly-text)', }} /> + {locationSuggestions.length > 0 && ( +
+ {locationSuggestions.map((suggestion, i) => ( +
{ e.preventDefault(); setLocation(suggestion); setLocationSuggestions([]); }} + style={{ + padding: '6px 10px', fontSize: '0.78rem', + cursor: 'pointer', color: 'var(--weekly-text)', + display: 'flex', alignItems: 'center', gap: '6px', + }} + onMouseEnter={e => (e.currentTarget.style.background = 'var(--weekly-hover, rgba(0,0,0,0.05))')} + onMouseLeave={e => (e.currentTarget.style.background = 'transparent')} + > + + {suggestion} +
+ ))} +
+ )}
- {/* Alert */} + {/* Alert / Reminders */}
@@ -714,23 +848,79 @@ export default function CalendarEventModal({ ) : ( reminders.map((reminder, idx) => ( -
- - +
+
+ + +
+ {/* Custom reminder editor row */} + {customEditorIdx === idx && ( +
+ setCustomAmount(Math.max(1, parseInt(e.target.value) || 1))} + aria-label={language === 'de' ? 'Erinnerungsmenge' : 'Reminder amount'} + style={{ + width: '48px', textAlign: 'center', fontSize: '0.78rem', + background: 'var(--weekly-bg, #fff)', borderRadius: '5px', + padding: '2px 4px', border: '1px solid var(--weekly-border, #ddd)', + outline: 'none', color: 'var(--weekly-text)', + }} + /> + + + {language === 'de' ? 'vorher' : 'before'} + + + +
+ )}
)) )}