From 1a08e0e855fefd972b0c6c1a1afb9c731d708cc1 Mon Sep 17 00:00:00 2001 From: Andrew Gioia Date: Thu, 19 Feb 2026 19:33:08 -0500 Subject: [PATCH] Adds calendar dropdown to new event modal --- app/Http/Controllers/EventController.php | 79 ++++++++++++++++++- resources/js/app.js | 72 +++++++++++++++++ resources/views/event/partials/form.blade.php | 57 ++++++++++++- 3 files changed, 202 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index 4dab18d..da3d246 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\Calendar; +use App\Models\CalendarInstance; use App\Models\Event; use App\Models\Location; use App\Services\Event\EventAttendeeSynchronizer; @@ -60,6 +61,7 @@ class EventController extends Controller ); $data = array_merge($data, $this->buildRecurrenceFormData($request, $start, $tz, $rrule)); $data = array_merge($data, $this->buildAttendeeFormData($request)); + $data = array_merge($data, $this->buildCalendarPickerData($request, $calendar, null)); if ($request->header('HX-Request') === 'true') { return view('event.partials.form-modal', $data); @@ -100,6 +102,7 @@ class EventController extends Controller $data = compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule'); $data = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule)); $data = array_merge($data, $this->buildAttendeeFormData($request, $event)); + $data = array_merge($data, $this->buildCalendarPickerData($request, $calendar, $event)); if ($request->header('HX-Request') === 'true') { return view('event.partials.form-modal', $data); @@ -202,6 +205,7 @@ class EventController extends Controller 'location' => ['nullable', 'string'], 'all_day' => ['sometimes', 'boolean'], 'category' => ['nullable', 'string', 'max:50'], + 'calendar_uri' => ['nullable', 'string', 'max:255'], 'repeat_frequency' => [ function (string $attribute, mixed $value, $fail) { if ($value === null || $value === '') { @@ -254,6 +258,8 @@ class EventController extends Controller } $data = $request->validate($rules); + $targetCalendar = $this->resolveTargetCalendar($request, $calendar); + $this->authorize('update', $targetCalendar); $tz = $this->displayTimezone($calendar, $request); $isAllDay = (bool) ($data['all_day'] ?? false); @@ -294,7 +300,7 @@ class EventController extends Controller ]); $event = Event::create([ - 'calendarid' => $calendar->id, + 'calendarid' => $targetCalendar->id, 'uri' => Str::uuid() . '.ics', 'lastmodified' => time(), 'etag' => md5($ical), @@ -323,7 +329,7 @@ class EventController extends Controller $attendeeSync->syncRows($event, $attendees); - return redirect()->route('calendar.show', $calendar); + return redirect()->route('calendar.show', $targetCalendar); } /** @@ -351,6 +357,7 @@ class EventController extends Controller 'location' => ['nullable', 'string'], 'all_day' => ['sometimes', 'boolean'], 'category' => ['nullable', 'string', 'max:50'], + 'calendar_uri' => ['nullable', 'string', 'max:255'], 'repeat_frequency' => [ function (string $attribute, mixed $value, $fail) { if ($value === null || $value === '') { @@ -392,6 +399,8 @@ class EventController extends Controller } $data = $request->validate($rules); + $targetCalendar = $this->resolveTargetCalendar($request, $calendar); + $this->authorize('update', $targetCalendar); $tz = $this->displayTimezone($calendar, $request); $isAllDay = (bool) ($data['all_day'] ?? false); @@ -435,6 +444,7 @@ class EventController extends Controller ]); $event->update([ + 'calendarid' => $targetCalendar->id, 'calendardata' => $ical, 'etag' => md5($ical), 'lastmodified' => time(), @@ -456,7 +466,7 @@ class EventController extends Controller $attendeeSync->syncRows($event, $attendees); - return redirect()->route('calendar.show', $calendar); + return redirect()->route('calendar.show', $targetCalendar); } /** @@ -835,6 +845,69 @@ class EventController extends Controller return $rows; } + private function buildCalendarPickerData(Request $request, Calendar $calendar, ?Event $event = null): array + { + $instances = CalendarInstance::query() + ->forUser($request->user()) + ->withUiMeta() + ->ordered() + ->get(); + + $selectedUri = old('calendar_uri'); + + if (!$selectedUri) { + if ($event) { + $selectedUri = $instances->firstWhere('calendarid', $event->calendarid)?->uri; + } else { + $selectedUri = $instances->first()?->uri; + } + } + + $selected = $instances->firstWhere('uri', $selectedUri) + ?? $instances->firstWhere('calendarid', $calendar->id) + ?? $instances->first(); + + $calendarPickerOptions = $instances->map(function (CalendarInstance $instance) { + $color = $instance->resolvedColor($instance->calendarcolor); + + return [ + 'uri' => $instance->uri, + 'name' => $instance->displayname ?: __('common.calendar'), + 'color' => $color, + ]; + })->values()->all(); + + $selectedCalendarUri = $selected?->uri; + $selectedCalendarName = $selected?->displayname ?: __('common.calendar'); + $selectedCalendarColor = $selected?->resolvedColor($selected?->calendarcolor) ?? '#64748b'; + + return compact( + 'calendarPickerOptions', + 'selectedCalendarUri', + 'selectedCalendarName', + 'selectedCalendarColor', + ); + } + + private function resolveTargetCalendar(Request $request, Calendar $fallback): Calendar + { + $uri = trim((string) $request->input('calendar_uri', '')); + if ($uri === '') { + return $fallback; + } + + $instance = CalendarInstance::query() + ->forUser($request->user()) + ->where('uri', $uri) + ->first(); + + if (!$instance) { + return $fallback; + } + + return Calendar::query()->find($instance->calendarid) ?? $fallback; + } + /** * resolve location_id from hints or geocoding */ diff --git a/resources/js/app.js b/resources/js/app.js index 66ce0c5..31a1958 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -9,6 +9,13 @@ const SELECTORS = { colorPickerColor: '[data-colorpicker-color]', colorPickerHex: '[data-colorpicker-hex]', colorPickerRandom: '[data-colorpicker-random]', + calendarPicker: '[data-calendar-picker]', + calendarPickerInput: '[data-calendar-picker-input]', + calendarPickerToggle: '[data-calendar-picker-toggle]', + calendarPickerMenu: '[data-calendar-picker-menu]', + calendarPickerLabel: '[data-calendar-picker-label]', + calendarPickerColorDot: '[data-calendar-picker-color]', + calendarPickerOption: '[data-calendar-picker-option]', eventAllDayToggle: '[data-all-day-toggle]', eventStartInput: '[data-event-start]', eventEndInput: '[data-event-end]', @@ -1510,6 +1517,69 @@ function initColorPickers(root = document) { root.querySelectorAll(SELECTORS.colorPicker).forEach(wire); } +/** + * + * calendar picker (custom dropdown with color chip) + */ +function initCalendarPickers(root = document) { + root.querySelectorAll(SELECTORS.calendarPicker).forEach((picker) => { + if (picker.__calendarPickerWired) return; + picker.__calendarPickerWired = true; + + const input = picker.querySelector(SELECTORS.calendarPickerInput); + const toggle = picker.querySelector(SELECTORS.calendarPickerToggle); + const menu = picker.querySelector(SELECTORS.calendarPickerMenu); + const label = picker.querySelector(SELECTORS.calendarPickerLabel); + const colorDot = picker.querySelector(SELECTORS.calendarPickerColorDot); + + if (!input || !toggle || !menu || !label || !colorDot) return; + + const close = () => { + menu.classList.add('hidden'); + toggle.setAttribute('aria-expanded', 'false'); + }; + + const open = () => { + menu.classList.remove('hidden'); + toggle.setAttribute('aria-expanded', 'true'); + }; + + toggle.addEventListener('click', (event) => { + event.preventDefault(); + if (menu.classList.contains('hidden')) open(); + else close(); + }); + + picker.addEventListener('click', (event) => { + const option = event.target.closest(SELECTORS.calendarPickerOption); + if (!option || !picker.contains(option)) return; + + event.preventDefault(); + + const uri = option.dataset.calendarPickerUri || ''; + const name = option.dataset.calendarPickerName || ''; + const color = option.dataset.calendarPickerColor || '#64748b'; + + input.value = uri; + label.textContent = name; + colorDot.style.backgroundColor = color; + close(); + }); + + document.addEventListener('click', (event) => { + if (!picker.contains(event.target)) { + close(); + } + }); + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + close(); + } + }); + }); +} + /** * * month view overflow handling (progressive enhancement) @@ -1652,6 +1722,7 @@ window.addEventListener('resize', () => { */ function initUI() { + initCalendarPickers(); initColorPickers(); initNaturalEventParser(); initEventAllDayToggles(); @@ -1675,6 +1746,7 @@ document.addEventListener('htmx:afterSwap', (e) => { } initColorPickers(e.target); + initCalendarPickers(e.target); initNaturalEventParser(e.target); initEventAllDayToggles(e.target); initRecurrenceControls(e.target); diff --git a/resources/views/event/partials/form.blade.php b/resources/views/event/partials/form.blade.php index 736d2b6..5d9d519 100644 --- a/resources/views/event/partials/form.blade.php +++ b/resources/views/event/partials/form.blade.php @@ -25,15 +25,64 @@ Repeat - +
+ {{-- Calendar --}} +
+
+ + +
+ + + + + +
+
+
{{-- Title --}}
@@ -137,6 +186,7 @@
+ {{-- recurrence --}}