diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 29bf322..f6e0819 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -545,7 +545,7 @@ export interface EventUpdatePayload { description?: string; location?: string; color?: string; - recurrence?: string; + recurrence?: string | null; project_id?: number; } diff --git a/frontend/src/components/EventSlideOver.vue b/frontend/src/components/EventSlideOver.vue index 4943a9e..a962e12 100644 --- a/frontend/src/components/EventSlideOver.vue +++ b/frontend/src/components/EventSlideOver.vue @@ -37,6 +37,36 @@ const description = ref(""); const location = ref(""); const color = ref(""); const projectId = ref(null); +const recurrence = ref(""); + +// Preset RRULE strings. The select binds to `recurrencePreset`, which writes +// through to `recurrence`. CalDAV-imported rules with extra parts +// (e.g. `FREQ=WEEKLY;BYDAY=MO,WE,FR`) fall through to "custom" and the raw +// string is shown read-only below the select. +const RECURRENCE_PRESETS: Record = { + none: "", + daily: "FREQ=DAILY", + weekly: "FREQ=WEEKLY", + monthly: "FREQ=MONTHLY", + yearly: "FREQ=YEARLY", +}; + +const recurrencePreset = computed({ + get() { + const r = (recurrence.value || "").trim(); + if (!r) return "none"; + for (const [key, val] of Object.entries(RECURRENCE_PRESETS)) { + if (val && val === r) return key; + } + return "custom"; + }, + set(key: string) { + if (key === "custom") return; // no-op; can't pick custom from dropdown + recurrence.value = RECURRENCE_PRESETS[key] ?? ""; + }, +}); + +const isCustomRecurrence = computed(() => recurrencePreset.value === "custom"); function dateFromIso(iso: string): string { const d = new Date(iso); @@ -115,6 +145,7 @@ function resetForm() { location.value = props.event.location || ""; color.value = props.event.color || ""; projectId.value = props.event.project_id; + recurrence.value = props.event.recurrence || ""; _lastDurationMin = !allDay.value && startTime.value && endTime.value ? durationMin(startTime.value, endTime.value) : 60; if (_lastDurationMin <= 0) _lastDurationMin = 60; } else { @@ -130,6 +161,7 @@ function resetForm() { location.value = ""; color.value = ""; projectId.value = null; + recurrence.value = ""; _lastDurationMin = 60; } deleteConfirm.value = false; @@ -257,6 +289,7 @@ async function save() { location: location.value, color: color.value, project_id: projectId.value ?? undefined, + recurrence: recurrence.value || null, }; const updated = await updateEvent(props.event.id, payload); emit("updated", updated); @@ -270,6 +303,7 @@ async function save() { location: location.value, color: color.value, project_id: projectId.value ?? undefined, + recurrence: recurrence.value || undefined, }; const created = await createEvent(payload); emit("created", created); @@ -374,6 +408,24 @@ async function doDelete() { + +
+ + +

+ Custom rule: {{ recurrence }} +
+ Picking a preset will replace this rule. +

+
+
@@ -539,6 +591,22 @@ async function doDelete() { color: var(--color-text-secondary); } +.recurrence-custom-hint { + margin: 4px 0 0; + font-size: 0.75rem; + color: var(--color-text-secondary); + line-height: 1.4; +} +.recurrence-custom-hint code { + background: var(--color-input-bg, #111113); + border: 1px solid var(--color-border, #2a2b30); + border-radius: 4px; + padding: 1px 5px; + font-family: var(--font-mono, ui-monospace, monospace); + font-size: 0.72rem; + color: var(--color-text, #e8e9f0); +} + .toggle-btn { background: var(--color-input-bg, #111113); border: 1px solid var(--color-border, #2a2b30);