diff --git a/src/components/EventEditor.vue b/src/components/EventEditor.vue index 5048384..4c65cb5 100644 --- a/src/components/EventEditor.vue +++ b/src/components/EventEditor.vue @@ -223,6 +223,13 @@ watch( @update:time-zone="entityObject!.timeZone = $event" /> + + - - { - return date.toISOString().split('T')[0] + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` } const formatTime = (date: Date): string => { @@ -45,16 +48,57 @@ const formatDateTime = (isoString: string | null | undefined): string => { const parseDateTime = (date: string, time: string): Date => { if (timelessValue.value) { - return new Date(date) + return new Date(`${date}T00:00:00`) } return new Date(`${date}T${time}`) } +const formatDisplayDate = (value: string): string => { + if (!value) return '' + return new Date(`${value}T00:00:00`).toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }) +} + +const formatDisplayTime = (value: string): string => { + if (!value) return '' + const [hour, minute] = value.split(':').map(Number) + const date = new Date() + date.setHours(hour, minute, 0, 0) + return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }) +} + +const pickerDate = (value: string): Date | null => value ? new Date(`${value}T00:00:00`) : null + +const normalizePickerDate = (value: unknown): string => { + const selected = Array.isArray(value) ? value[0] : value + const date = selected instanceof Date ? selected : new Date(String(selected)) + return Number.isNaN(date.getTime()) ? '' : formatDate(date) +} + // Date/time fields const startDate = ref('') const startTime = ref('') const endDate = ref('') const endTime = ref('') +const startDateMenu = ref(false) +const startTimeMenu = ref(false) +const endDateMenu = ref(false) +const endTimeMenu = ref(false) + +const selectStartDate = (value: unknown) => { + const date = normalizePickerDate(value) + if (date) startDate.value = date + startDateMenu.value = false +} + +const selectEndDate = (value: unknown) => { + const date = normalizePickerDate(value) + if (date) endDate.value = date + endDateMenu.value = false +} // Initialize date/time from props watch(() => props.startsOn, (newValue) => { @@ -81,17 +125,23 @@ watch(() => props.endsOn, (newValue) => { } }, { immediate: true }) -// Update parent when date/time changes -watch([startDate, startTime], () => { - if (props.mode === 'edit') { - emit('update:startsOn', parseDateTime(startDate.value, startTime.value).toISOString()) - } -}) +const ensureValidRange = (): boolean => { + if (!startDate.value || !endDate.value || !startTime.value || !endTime.value) return false + const start = parseDateTime(startDate.value, startTime.value) + const end = parseDateTime(endDate.value, endTime.value) + if (end.getTime() >= start.getTime()) return false -watch([endDate, endTime], () => { - if (props.mode === 'edit') { - emit('update:endsOn', parseDateTime(endDate.value, endTime.value).toISOString()) - } + const correctedEnd = new Date(start) + if (!timelessValue.value) correctedEnd.setHours(correctedEnd.getHours() + 1) + endDate.value = formatDate(correctedEnd) + endTime.value = formatTime(correctedEnd) + return true +} + +watch([startDate, startTime, endDate, endTime, timelessValue], () => { + if (props.mode !== 'edit' || ensureValidRange()) return + emit('update:startsOn', parseDateTime(startDate.value, startTime.value).toISOString()) + emit('update:endsOn', parseDateTime(endDate.value, endTime.value).toISOString()) }) @@ -127,22 +177,46 @@ watch([endDate, endTime], () => {
Start
- + + + + - + + + + + + Done + + + @@ -151,22 +225,47 @@ watch([endDate, endTime], () => {
End
- + + + + - + + + + + + Done + + + diff --git a/src/components/editors/EventEditorOccurrence.vue b/src/components/editors/EventEditorOccurrence.vue index 002259e..4708fa8 100644 --- a/src/components/editors/EventEditorOccurrence.vue +++ b/src/components/editors/EventEditorOccurrence.vue @@ -1,43 +1,223 @@ @@ -46,92 +226,265 @@ const formatRecurrence = (pattern: any) => {
Recurrence
- - mdi-plus - Add Recurrence + @click="addRecurrence" + > + + Add - - Remove - + variant="text" + @click="$emit('update:pattern', null)" + />
-
-
-
{{ formatRecurrence(pattern) }}
-
- Days: {{ pattern.onDayOfWeek.join(', ') }} -
+
+ + {{ formatRecurrence(pattern) }}
- -
- - - - - - - - - - - +
+
+ Repeat + +
+ + +
+
+ On days + + + {{ day.short.slice(0, 2) }} + {{ day.title }} + + +
+ +
+ Using + + Calendar date + Weekday pattern + + + + + +
+ +
+ Using + + Calendar date + Weekday pattern + + + + + + + +
+ +
+ Ends + + + + + + + + + occurrences +
+ +
+ + {{ formatRecurrence(pattern) }} +
+
+
@@ -140,4 +493,122 @@ const formatRecurrence = (pattern: any) => { .event-editor-section { margin-bottom: 1.5rem; } + +.recurrence-editor { + border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity)); +} + +.recurrence-summary { + background: rgba(var(--v-theme-primary), 0.08); + color: rgb(var(--v-theme-on-surface)); +} + +.recurrence-controls, +.recurrence-end-controls, +.weekly-on-controls, +.monthly-rule-controls, +.yearly-rule-controls, +.recurrence-sentence { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.recurrence-controls, +.recurrence-end-controls, +.weekly-on-controls, +.monthly-rule-controls, +.yearly-rule-controls { + flex-wrap: nowrap; +} + +.frequency-field { + flex: 0 1 9.5rem; + min-width: 0; + max-width: 9.5rem; +} + +.interval-field { + flex: 0 0 5rem; +} + +.end-mode-field { + flex: 0 0 7rem; + max-width: 7rem; +} + +.end-date-field { + flex: 0 1 12rem; + min-width: 0; + max-width: 12rem; +} + +.field-label { + color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity)); + font-size: 0.875rem; + font-weight: 500; +} + +.weekday-toggle, +.rule-toggle { + display: flex; + flex-wrap: wrap; + height: auto; +} + +.weekday-toggle :deep(.v-btn) { + flex: 1 1 2.5rem; + min-width: 2.5rem; + min-height: 40px; + border: 1px solid rgba(var(--v-theme-on-surface), 0.35) !important; +} + +.weekday-toggle :deep(.v-btn--active) { + border-color: rgb(var(--v-theme-primary)) !important; +} + +.weekly-on-controls .weekday-toggle { + flex: 1 1 auto; + flex-wrap: nowrap; + min-width: 0; +} + +.monthly-rule-controls .rule-toggle, +.yearly-rule-controls .rule-toggle { + flex: 0 0 auto; + flex-wrap: nowrap; +} + +.monthly-rule-controls .rule-toggle :deep(.v-btn), +.yearly-rule-controls .rule-toggle :deep(.v-btn) { + min-height: 40px; +} + +.monthly-day-field { + flex: 0 0 6rem; + max-width: 6rem; +} + +.monthly-position-field { + flex: 0 0 6.5rem; + max-width: 6.5rem; +} + +.monthly-weekday-field { + flex: 0 1 8.5rem; + min-width: 0; + max-width: 8.5rem; +} + +.yearly-month-field { + flex: 0 1 8rem; + min-width: 0; + max-width: 8rem; +} + +@media (max-width: 599px) { + .rule-toggle :deep(.v-btn) { + flex: 1 1 auto; + } +}