Improves event edit modal; updates 4day and day views to properly show all-day events; cleans up the all-day event row

This commit is contained in:
Andrew Gioia 2026-02-13 16:48:01 -05:00
parent 236da90af2
commit 864e0d938a
Signed by: andrew
GPG Key ID: FC09694A000800C8
17 changed files with 698 additions and 342 deletions

View File

@ -140,7 +140,8 @@ class CalendarController extends Controller
$allDayEvents = $events->filter(fn ($event) => !empty($event['all_day']));
$timeEvents = $events->filter(fn ($event) => empty($event['all_day']));
$hasAllDayEvents = $allDayEvents->isNotEmpty();
$allDayDisplay = $this->buildAllDayDisplay($allDayEvents);
$hasAllDayEvents = $allDayDisplay->isNotEmpty();
if ($request->boolean('debug_all_day')) {
$calendarIds = $calendars->pluck('id')->values();
@ -297,7 +298,7 @@ class CalendarController extends Controller
'hgroup' => $viewBuilder->viewHeaders($view, $range, $tz, $weekStart),
'events' => $events, // keyed by occurrence
'events_time' => $timeEvents,
'events_all_day'=> $allDayEvents,
'events_all_day'=> $allDayDisplay,
'has_all_day' => $hasAllDayEvents,
'grid' => $grid, // day objects hold only ID-sets
'mini' => $mini, // mini calendar days with events for indicators
@ -371,6 +372,52 @@ class CalendarController extends Controller
return view('calendar.index', $payload);
}
private function buildAllDayDisplay($allDayEvents)
{
$items = collect();
$groups = $allDayEvents->groupBy(fn ($event) => (int) ($event['start_col'] ?? 1));
foreach ($groups as $col => $events) {
$sorted = $events->sortBy('start')->values();
$total = $sorted->count();
if ($total <= 3) {
foreach ($sorted as $index => $event) {
$items->push(array_merge($event, [
'all_day_row' => $index + 1,
'all_day_more' => false,
'all_day_overflow' => false,
]));
}
continue;
}
foreach ($sorted as $index => $event) {
$items->push(array_merge($event, [
'all_day_row' => $index + 1,
'all_day_more' => false,
'all_day_overflow' => $index >= 2,
]));
}
$items->push([
'id' => 'more-'.$col,
'calendar_slug' => $sorted[0]['calendar_slug'] ?? null,
'start_col' => $col,
'all_day_row' => 3,
'all_day_more' => true,
'all_day_overflow' => false,
'all_day_more_count' => $total - 2,
'visible' => true,
]);
}
return $items->sortBy([
['start_col', 'asc'],
['all_day_row', 'asc'],
])->values();
}
/**
* create sabre calendar + meta
*/

View File

@ -58,6 +58,7 @@ class EventController extends Controller
'tz',
'rrule',
);
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start, $tz, $rrule));
if ($request->header('HX-Request') === 'true') {
return view('event.partials.form-modal', $data);
@ -96,6 +97,7 @@ class EventController extends Controller
?? '';
$data = compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule');
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule));
if ($request->header('HX-Request') === 'true') {
return view('event.partials.form-modal', $data);
@ -561,6 +563,112 @@ class EventController extends Controller
: Carbon::createFromFormat('Y-m-d\\TH:i', $value, $tz)->seconds(0);
}
private function buildRecurrenceFormData(Request $request, string $startValue, string $tz, ?string $rrule): array
{
$rruleValue = trim((string) ($rrule ?? ''));
$rruleParts = [];
foreach (array_filter(explode(';', $rruleValue)) as $chunk) {
if (!str_contains($chunk, '=')) {
continue;
}
[$key, $value] = explode('=', $chunk, 2);
$rruleParts[strtoupper($key)] = $value;
}
$freq = strtolower($rruleParts['FREQ'] ?? '');
$interval = (int) ($rruleParts['INTERVAL'] ?? 1);
if ($interval < 1) {
$interval = 1;
}
$byday = array_filter(explode(',', $rruleParts['BYDAY'] ?? ''));
$bymonthday = array_filter(explode(',', $rruleParts['BYMONTHDAY'] ?? ''));
$bysetpos = $rruleParts['BYSETPOS'] ?? null;
$startDate = null;
if ($startValue !== '') {
try {
$startDate = Carbon::parse($startValue, $tz);
} catch (\Throwable $e) {
$startDate = null;
}
}
$startDate = $startDate ?? Carbon::now($tz);
$weekdayMap = [
'Sun' => 'SU',
'Mon' => 'MO',
'Tue' => 'TU',
'Wed' => 'WE',
'Thu' => 'TH',
'Fri' => 'FR',
'Sat' => 'SA',
];
$defaultWeekday = $weekdayMap[$startDate->format('D')] ?? 'MO';
$defaultMonthDay = (int) $startDate->format('j');
$weekMap = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth'];
$startWeek = $startDate->copy();
$isLastWeek = $startWeek->copy()->addWeek()->month !== $startWeek->month;
$defaultMonthWeek = $isLastWeek ? 'last' : ($weekMap[$startDate->weekOfMonth] ?? 'first');
$monthMode = 'days';
if (!empty($bymonthday)) {
$monthMode = 'days';
} elseif (!empty($byday) && $bysetpos) {
$monthMode = 'weekday';
}
$setposMap = ['1' => 'first', '2' => 'second', '3' => 'third', '4' => 'fourth', '-1' => 'last'];
$repeatFrequency = $request->old('repeat_frequency', $freq ?: '');
$repeatInterval = $request->old('repeat_interval', $interval);
$repeatWeekdays = $request->old('repeat_weekdays', $byday ?: [$defaultWeekday]);
$repeatMonthDays = $request->old('repeat_month_days', $bymonthday ?: [$defaultMonthDay]);
$repeatMonthMode = $request->old('repeat_monthly_mode', $monthMode);
$repeatMonthWeek = $request->old('repeat_month_week', $setposMap[(string) $bysetpos] ?? $defaultMonthWeek);
$repeatMonthWeekday = $request->old('repeat_month_weekday', $byday[0] ?? $defaultWeekday);
$rruleOptions = [
'daily' => __('calendar.event.recurrence.daily'),
'weekly' => __('calendar.event.recurrence.weekly'),
'monthly' => __('calendar.event.recurrence.monthly'),
'yearly' => __('calendar.event.recurrence.yearly'),
];
$weekdayOptions = [
'SU' => __('calendar.event.recurrence.weekdays.sun_short'),
'MO' => __('calendar.event.recurrence.weekdays.mon_short'),
'TU' => __('calendar.event.recurrence.weekdays.tue_short'),
'WE' => __('calendar.event.recurrence.weekdays.wed_short'),
'TH' => __('calendar.event.recurrence.weekdays.thu_short'),
'FR' => __('calendar.event.recurrence.weekdays.fri_short'),
'SA' => __('calendar.event.recurrence.weekdays.sat_short'),
];
$weekdayLong = [
'SU' => __('calendar.event.recurrence.weekdays.sun'),
'MO' => __('calendar.event.recurrence.weekdays.mon'),
'TU' => __('calendar.event.recurrence.weekdays.tue'),
'WE' => __('calendar.event.recurrence.weekdays.wed'),
'TH' => __('calendar.event.recurrence.weekdays.thu'),
'FR' => __('calendar.event.recurrence.weekdays.fri'),
'SA' => __('calendar.event.recurrence.weekdays.sat'),
];
return compact(
'repeatFrequency',
'repeatInterval',
'repeatWeekdays',
'repeatMonthDays',
'repeatMonthMode',
'repeatMonthWeek',
'repeatMonthWeekday',
'rruleOptions',
'weekdayOptions',
'weekdayLong'
);
}
private function mergeRecurrenceExtra(array $extra, ?string $rrule, string $tz, Request $request): array
{
if ($rrule === null) {

View File

@ -58,14 +58,40 @@ class CalendarViewBuilder
}
if (empty($occurrences) && !$isRecurring) {
$startUtc = $e->meta?->start_at
? Carbon::parse($e->meta->start_at)->utc()
: Carbon::createFromTimestamp($e->firstoccurence, 'UTC');
$endUtc = $e->meta?->end_at
? Carbon::parse($e->meta->end_at)->utc()
: ($e->lastoccurence
$meta = $e->meta;
$isAllDay = (bool) ($meta?->all_day ?? false);
$startUtc = null;
$endUtc = null;
if ($isAllDay && $meta?->start_on && $meta?->end_on) {
$allDayTz = $meta?->tzid
?? ($meta?->extra['tzid'] ?? null)
?? $timezone;
$startUtc = Carbon::parse($meta->start_on->toDateString(), $allDayTz)
->startOfDay()
->utc();
$endUtc = Carbon::parse($meta->end_on->toDateString(), $allDayTz)
->startOfDay()
->utc();
} else {
if ($meta?->start_at instanceof Carbon) {
$startUtc = $meta->start_at->copy()->utc();
} elseif ($meta?->start_at) {
$startUtc = Carbon::parse($meta->start_at, 'UTC');
} else {
$startUtc = Carbon::createFromTimestamp($e->firstoccurence, 'UTC');
}
if ($meta?->end_at instanceof Carbon) {
$endUtc = $meta->end_at->copy()->utc();
} elseif ($meta?->end_at) {
$endUtc = Carbon::parse($meta->end_at, 'UTC');
} else {
$endUtc = $e->lastoccurence
? Carbon::createFromTimestamp($e->lastoccurence, 'UTC')
: $startUtc->copy());
: $startUtc->copy();
}
}
$occurrences[] = [
'start' => $startUtc,
@ -86,12 +112,18 @@ class CalendarViewBuilder
$colorFg,
$gridStartMinutes,
$gridEndMinutes,
$daytimeHours
$daytimeHours,
$spanStartUtc,
$spanEndUtc
) {
$startUtc = $occ['start'];
$endUtc = $occ['end'];
$isAllDay = (bool) ($e->meta?->all_day ?? false);
if ($endUtc->lte($spanStartUtc) || $startUtc->gte($spanEndUtc)) {
return null;
}
$startLocal = $startUtc->copy()->timezone($timezone);
$endLocal = $endUtc->copy()->timezone($timezone);

View File

@ -82,6 +82,7 @@ return [
'notes' => 'Notes',
'no_description' => 'No description yet.',
'all_day_events' => 'All-day events',
'show_more' => ':count more',
'recurrence' => [
'label' => 'Repeat',
'frequency' => 'Frequency',

View File

@ -82,6 +82,7 @@ return [
'notes' => 'Note',
'no_description' => 'Nessuna descrizione.',
'all_day_events' => 'Eventi di tutto il giorno',
'show_more' => 'Mostra altri :count',
'recurrence' => [
'label' => 'Ripeti',
'frequency' => 'Frequenza',

View File

@ -271,6 +271,16 @@ main {
&#settings {
/* */
}
/* main content */
section {
/* content footer defaults */
footer {
transition: padding 250ms ease-in-out;
}
}
}
/* expanded */
@ -404,6 +414,10 @@ main {
@apply pl-6;
}
}
footer {
@apply pb-3;
}
}
}
}

View File

@ -83,6 +83,14 @@ button,
box-shadow: var(--shadows);
--shadows: none;
&.button--icon {
@apply rounded-none h-full top-0;
&:hover {
@apply bg-cyan-200;
}
}
&:hover {
@apply bg-cyan-200;
}

View File

@ -1,6 +1,12 @@
/**
* calendar
*
* z-index: top is currently 10; overlapping events increment, assuming this won't be 10!
*/
/**
* month view
**/
*/
.calendar.month {
@apply grid col-span-3 pb-6 2xl:pb-8 pt-2;
/*grid-template-rows: 2rem 1fr; */
@ -166,7 +172,7 @@
/* top day bar */
hgroup {
@apply bg-white col-span-2 border-b-2 border-primary pl-24 sticky z-10;
@apply bg-white col-span-2 border-b-2 border-primary pl-24 sticky z-11;
@apply top-20;
span.name {
@ -206,24 +212,29 @@
/* all day bar */
ol.day {
@apply sticky top-42 grid col-span-2 bg-white py-2 border-b border-primary col-span-2 pl-24 z-2 overflow-x-hidden;
box-shadow: 0 0.25rem 0.5rem -0.25rem var(--color-gray-200);
@apply sticky top-42 grid col-span-2 bg-white border-b border-primary z-10 overflow-x-hidden;
box-shadow: 0 0.25rem 0.5rem -0.25rem rgba(0,0,0,0.15);
padding: 0.25rem 0 0.2rem 6rem;
&::before {
@apply absolute left-0 top-1/2 -translate-y-1/2 w-24 pr-4 text-right;
@apply absolute left-0 top-1.5 w-24 pr-4 text-right;
@apply uppercase text-xs font-mono text-secondary font-medium;
content: 'All day';
}
li.events {
@apply flex flex-col gap-1 relative overflow-x-hidden;
li.event-wrapper {
@apply flex relative overflow-x-hidden;
grid-row-start: var(--event-row);
grid-row-end: var(--event-end);
grid-column-start: var(--event-col);
grid-column-end: calc(var(--event-col) + 1);
&.event-wrapper--overflow {
@apply hidden;
}
a.event {
@apply flex items-center text-xs gap-1 px-1 py-px font-medium rounded-sm w-full;
@apply flex items-center text-xs gap-1 px-1 py-px mb-2px font-medium rounded-sm w-full;
background-color: var(--event-bg);
color: var(--event-fg);
@ -235,6 +246,27 @@
background-color: color-mix(in srgb, var(--event-bg) 100%, #000 10%);
}
}
&.event-wrapper--more {
label.event--more {
@apply text-xs cursor-pointer rounded m-1 h-3.5 leading-none;
@apply focus:outline-2 focus:outline-offset-2 focus:outline-cyan-600;
input.more-checkbox {
@apply hidden;
}
}
}
}
&:has(input.more-checkbox:checked) {
li.event-wrapper--overflow {
@apply flex;
}
li.event-wrapper--more {
@apply hidden;
}
}
}

View File

@ -21,10 +21,10 @@ dialog {
@apply relative rounded-xl bg-white border-gray-200 p-0;
@apply flex flex-col items-start col-start-1 translate-y-4;
@apply overscroll-contain overflow-y-auto;
max-height: calc(100vh - 5em);
max-height: calc(100dvh - 5rem);
width: 91.666667%;
max-width: 36rem;
transition: all 150ms cubic-bezier(0,0,.2,1);
transition: translate 150ms cubic-bezier(0,0,.2,1);
box-shadow: 0 1.5rem 4rem -0.5rem rgba(0, 0, 0, 0.4);
> .close-modal {
@ -36,7 +36,7 @@ dialog {
/* modal header */
header {
@apply relative flex items-center px-6 h-20 z-2;
@apply sticky top-0 bg-white flex items-center px-6 h-20 z-2;
h2 {
@apply pr-12;
@ -55,7 +55,7 @@ dialog {
/* footer */
footer {
@apply px-6 py-4 border-t-md border-gray-400 flex justify-between;
@apply sticky bottom-0 bg-white px-6 py-4 border-t-md border-gray-400 flex justify-between;
}
/* event modal with a map */
@ -84,3 +84,60 @@ dialog {
}
}
}
/**
* tabs using <details> and <summary>
*/
dialog:has(.modal-tabs) {
@apply items-start justify-items-center;
#modal {
@apply relative;
max-height: 85dvh;
max-width: 48rem;
top: 10dvh;
}
form.modal {
@apply !block;
}
}
.modal-tab {
> summary {
@apply sticky left-6 w-32 flex flex-row gap-2;
top: 6rem;
&::before {
@apply bg-gray-10;
}
&::after {
@apply hidden;
}
}
&:nth-of-type(2) {
> summary { top: 8.75rem; }
}
&:nth-of-type(3) {
> summary { top: 11.5rem; }
}
&:nth-of-type(4) {
> summary { top: 14.25rem; }
}
&:nth-of-type(5) {
> summary { top: 17rem; }
}
&[open] {
> summary::before {
@apply bg-gray-100;
}
}
div.tab-content {
@apply flex flex-col gap-4 pl-40 pt-4 min-h-48;
}
}

View File

@ -19,6 +19,11 @@ const SELECTORS = {
monthlyMode: '[data-monthly-mode]',
monthlyDays: '[data-monthly-days]',
monthlyWeekday: '[data-monthly-weekday]',
modalDialog: 'dialog',
modalContent: '#modal',
tabsRoot: '[data-tabs]',
tabButton: '[role=\"tab\"]',
tabPanel: '[role=\"tabpanel\"]',
monthDay: '.calendar.month .day',
monthDayEvent: 'a.event',
monthDayMore: '[data-day-more]',
@ -43,6 +48,14 @@ document.addEventListener('htmx:configRequest', (evt) => {
if (token) evt.detail.headers['X-CSRF-TOKEN'] = token
})
// modal htmx tracking
document.addEventListener('htmx:beforeSwap', (evt) => {
const target = evt.detail?.target || evt.target;
if (target && target.id === 'modal') {
target.dataset.prevUrl = window.location.href;
}
});
/**
*
* global auth expiry redirect (fetch/axios)
@ -254,6 +267,74 @@ function initRecurrenceControls(root = document) {
}
/**
*
* modal behaviors (backdrop close + url restore)
*/
function initModalHandlers(root = document) {
const dialog = root.querySelector(SELECTORS.modalDialog);
if (!dialog || dialog.__modalWired) return;
dialog.__modalWired = true;
dialog.addEventListener('click', (event) => {
if (event.target === dialog) {
dialog.close();
}
});
dialog.addEventListener('close', () => {
const modal = dialog.querySelector(SELECTORS.modalContent);
if (!modal) return;
const isEvent = modal.querySelector('[data-modal-kind="event"]');
const prevUrl = modal.dataset.prevUrl;
modal.innerHTML = '';
if (isEvent && prevUrl) {
history.replaceState({}, '', prevUrl);
}
});
}
/**
* tabs (simple modal panels)
*/
function initTabs(root = document) {
root.querySelectorAll(SELECTORS.tabsRoot).forEach((tabs) => {
if (tabs.__tabsWired) return;
tabs.__tabsWired = true;
const buttons = tabs.querySelectorAll(SELECTORS.tabButton);
const panels = tabs.querySelectorAll(SELECTORS.tabPanel);
if (!buttons.length || !panels.length) return;
const activate = (button) => {
buttons.forEach((btn) => {
const isActive = btn === button;
btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
});
panels.forEach((panel) => {
const id = button.getAttribute('aria-controls');
panel.hidden = panel.id !== id;
});
};
buttons.forEach((btn) => {
btn.addEventListener('click', (event) => {
event.preventDefault();
activate(btn);
});
});
const current = tabs.querySelector('[role="tab"][aria-selected="true"]') || buttons[0];
if (current) {
activate(current);
}
});
}
/**
*
* auto-scroll time views to 8am on load (when daytime hours are disabled)
*/
function initTimeViewAutoScroll(root = document)
@ -266,8 +347,8 @@ function initTimeViewAutoScroll(root = document)
if (calendar.dataset.autoscrolled === '1') return;
if (calendar.dataset.daytimeHoursEnabled === '1') return;
// find the target minute (7:45am)
const target = calendar.querySelector('[data-slot-minutes="465"]');
// find the target minute (8:00am)
const target = calendar.querySelector('[data-slot-minutes="480"]');
if (!target) return;
// get the scroll container and offset
@ -540,6 +621,8 @@ function initUI() {
initColorPickers();
initEventAllDayToggles();
initRecurrenceControls();
initModalHandlers();
initTabs();
initTimeViewAutoScroll();
initMonthOverflow();
}
@ -549,9 +632,16 @@ document.addEventListener('DOMContentLoaded', initUI);
// rebind in htmx for swapped content
document.addEventListener('htmx:afterSwap', (e) => {
const target = e.detail?.target || e.target;
if (target && target.id === 'modal') {
target.closest('dialog')?.showModal();
}
initColorPickers(e.target);
initEventAllDayToggles(e.target);
initRecurrenceControls(e.target);
initModalHandlers(e.target);
initTabs(e.target);
initTimeViewAutoScroll(e.target);
initMonthOverflow(e.target);
});

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@ -121,6 +121,7 @@
type="submit"
name="date"
value="{{ $nav['prev'] }}"
class="button--icon"
aria-label="Go back 1 month">
<x-icon-chevron-left />
</x-button.group-button>
@ -135,6 +136,7 @@
type="submit"
name="date"
value="{{ $nav['next'] }}"
class="button--icon"
aria-label="Go forward 1 month">
<x-icon-chevron-right />
</x-button.group-button>

View File

@ -2,13 +2,32 @@
'event' => [],
])
<li class="events"
@php
$row = (int) ($event['all_day_row'] ?? 1);
$end = $row + 1;
$isMore = (bool) ($event['all_day_more'] ?? false);
$isOverflow = (bool) ($event['all_day_overflow'] ?? false);
@endphp
<li class="event-wrapper{{ $isMore ? ' event-wrapper--more' : '' }}{{ $isOverflow ? ' event-wrapper--overflow' : '' }}"
data-event-id="{{ $event['occurrence_id'] ?? $event['id'] }}"
data-calendar-id="{{ $event['calendar_slug'] }}"
data-calendar-id="{{ $event['calendar_slug'] ?? '' }}"
data-more-count="{{ $event['all_day_more_count'] ?? '' }}"
style="
--event-col: {{ $event['start_col'] ?? 1 }};
--event-bg: {{ $event['color'] }};
--event-fg: {{ $event['color_fg'] }};">
--event-row: {{ $row }};
--event-end: {{ $end }};
--event-bg: {{ $event['color'] ?? 'var(--color-gray-100)' }};
--event-fg: {{ $event['color_fg'] ?? 'var(--color-primary)' }};">
@if($isMore)
@php
$moreId = 'more-col-'.$event['start_col'];
@endphp
<label class="event event--more" for="{{ $moreId }}" tabindex="0">
<input id="{{ $moreId }}" type="checkbox" class="more-checkbox" />
<span>{{ __('calendar.event.show_more', ['count' => $event['all_day_more_count'] ?? 0]) }}</span>
</label>
@else
@php
$showParams = [$event['calendar_slug'], $event['id']];
if (!empty($event['occurrence'])) {
@ -24,4 +43,5 @@
data-calendar="{{ $event['calendar_slug'] }}">
<span>{{ $event['title'] }}</span>
</a>
@endif
</li>

View File

@ -1,11 +1,6 @@
<dialog
hx-on:click="if(event.target === this) this.close()"
hx-on:close="const modal = document.getElementById('modal'); const isEvent = modal?.querySelector('[data-modal-kind=\'event\']'); const prevUrl = modal?.dataset?.prevUrl; modal.innerHTML=''; if (isEvent && prevUrl) history.replaceState({}, '', prevUrl);"
>
<dialog>
<div id="modal"
hx-target="this"
hx-on::before-swap="this.dataset.prevUrl = window.location.href"
hx-on::after-swap="this.closest('dialog')?.showModal()"
hx-swap="innerHTML">
</div>
</dialog>

View File

@ -2,7 +2,7 @@
<x-slot name="header">
<div class="flex items-center justify-between">
<h2 class="text-xl font-semibold leading-tight">
{{ $event->exists ? __('Edit Event') : __('Create Event') }}
{{ $event->exists ? __('Edit event details') : __('Create a new event') }}
</h2>
{{-- “Back” breadcrumb --}}

View File

@ -1,6 +1,6 @@
<x-modal.content>
<x-modal.content class="modal-tabs">
<x-modal.title>
<h2>{{ $event->exists ? __('Edit Event') : __('Create Event') }}</h2>
<h2>{{ $event->exists ? __('Edit event details') : __('Create a new event') }}</h2>
</x-modal.title>
<x-modal.body>
@include('event.partials.form', [
@ -17,7 +17,7 @@
{{ __('common.cancel') }}
</x-button>
<x-button variant="primary" type="submit" form="event-form">
{{ $event->exists ? __('Save') : __('Create') }}
{{ $event->exists ? __('common.save') : __('Create event') }}
</x-button>
</x-modal.footer>
</x-modal.content>

View File

@ -3,83 +3,6 @@
$formAction = $event->exists
? route('calendar.event.update', [$calendar, $event])
: route('calendar.event.store', $calendar);
$rruleValue = trim((string) ($rrule ?? ''));
$rruleParts = [];
foreach (array_filter(explode(';', $rruleValue)) as $chunk) {
if (!str_contains($chunk, '=')) continue;
[$key, $value] = explode('=', $chunk, 2);
$rruleParts[strtoupper($key)] = $value;
}
$freq = strtolower($rruleParts['FREQ'] ?? '');
$interval = (int) ($rruleParts['INTERVAL'] ?? 1);
if ($interval < 1) $interval = 1;
$byday = array_filter(explode(',', $rruleParts['BYDAY'] ?? ''));
$bymonthday = array_filter(explode(',', $rruleParts['BYMONTHDAY'] ?? ''));
$bysetpos = $rruleParts['BYSETPOS'] ?? null;
$startDefault = old('start_at', $start ?? null);
$startDate = $startDefault ? \Carbon\Carbon::parse($startDefault) : \Carbon\Carbon::now();
$weekdayMap = [
'Sun' => 'SU',
'Mon' => 'MO',
'Tue' => 'TU',
'Wed' => 'WE',
'Thu' => 'TH',
'Fri' => 'FR',
'Sat' => 'SA',
];
$defaultWeekday = $weekdayMap[$startDate->format('D')] ?? 'MO';
$defaultMonthDay = (int) $startDate->format('j');
$weekMap = [1 => 'first', 2 => 'second', 3 => 'third', 4 => 'fourth'];
$startWeek = $startDate->copy();
$isLastWeek = $startWeek->copy()->addWeek()->month !== $startWeek->month;
$defaultMonthWeek = $isLastWeek ? 'last' : ($weekMap[$startDate->weekOfMonth] ?? 'first');
$monthMode = 'days';
if (!empty($bymonthday)) {
$monthMode = 'days';
} elseif (!empty($byday) && $bysetpos) {
$monthMode = 'weekday';
}
$repeatFrequency = old('repeat_frequency', $freq ?: '');
$repeatInterval = old('repeat_interval', $interval);
$repeatWeekdays = old('repeat_weekdays', $byday ?: [$defaultWeekday]);
$repeatMonthDays = old('repeat_month_days', $bymonthday ?: [$defaultMonthDay]);
$repeatMonthMode = old('repeat_monthly_mode', $monthMode);
$setposMap = ['1' => 'first', '2' => 'second', '3' => 'third', '4' => 'fourth', '-1' => 'last'];
$repeatMonthWeek = old('repeat_month_week', $setposMap[(string) $bysetpos] ?? $defaultMonthWeek);
$repeatMonthWeekday = old('repeat_month_weekday', $byday[0] ?? $defaultWeekday);
$rruleOptions = [
'daily' => __('calendar.event.recurrence.daily'),
'weekly' => __('calendar.event.recurrence.weekly'),
'monthly' => __('calendar.event.recurrence.monthly'),
'yearly' => __('calendar.event.recurrence.yearly'),
];
$weekdayOptions = [
'SU' => __('calendar.event.recurrence.weekdays.sun_short'),
'MO' => __('calendar.event.recurrence.weekdays.mon_short'),
'TU' => __('calendar.event.recurrence.weekdays.tue_short'),
'WE' => __('calendar.event.recurrence.weekdays.wed_short'),
'TH' => __('calendar.event.recurrence.weekdays.thu_short'),
'FR' => __('calendar.event.recurrence.weekdays.fri_short'),
'SA' => __('calendar.event.recurrence.weekdays.sat_short'),
];
$weekdayLong = [
'SU' => __('calendar.event.recurrence.weekdays.sun'),
'MO' => __('calendar.event.recurrence.weekdays.mon'),
'TU' => __('calendar.event.recurrence.weekdays.tue'),
'WE' => __('calendar.event.recurrence.weekdays.wed'),
'TH' => __('calendar.event.recurrence.weekdays.thu'),
'FR' => __('calendar.event.recurrence.weekdays.fri'),
'SA' => __('calendar.event.recurrence.weekdays.sat'),
];
@endphp
<form method="POST" id="event-form" action="{{ $formAction }}" class="settings modal">
@ -88,6 +11,9 @@
@method('PUT')
@endif
<div class="event-tabs" data-tabs>
<div class="tab-panels">
<section id="tab-details" role="tabpanel" aria-labelledby="tab-btn-details">
{{-- Title --}}
<div class="input-row input-row--1">
<div class="input-cell">
@ -188,14 +114,11 @@
/>
</div>
</div>
</section>
{{-- Recurrence (advanced) --}}
<section id="tab-repeat" role="tabpanel" aria-labelledby="tab-btn-repeat" hidden>
<div class="input-row input-row--1">
<div class="input-cell">
<details>
<summary class="cursor-pointer text-sm text-gray-600">
{{ __('calendar.event.recurrence.label') }}
</summary>
<div class="mt-3">
<x-input.label for="repeat_frequency" :value="__('calendar.event.recurrence.frequency')" />
<x-input.select
@ -300,7 +223,32 @@
{{ __('calendar.event.recurrence.yearly_hint') }}
</div>
</div>
</details>
</div>
</div>
</section>
<section id="tab-invitees" role="tabpanel" aria-labelledby="tab-btn-invitees" hidden>
<div class="input-row input-row--1">
<div class="input-cell">
<p class="text-sm text-gray-600">More to come</p>
</div>
</div>
</section>
</div>
<div class="tab-buttons" role="tablist" aria-orientation="vertical">
<button type="button" id="tab-btn-details" role="tab" aria-controls="tab-details" aria-selected="true">
<x-icon-info-circle width="20" />
<span>Details</span>
</button>
<button type="button" id="tab-btn-repeat" role="tab" aria-controls="tab-repeat" aria-selected="false">
<x-icon-repeat width="20" />
<span>Repeat</span>
</button>
<button type="button" id="tab-btn-invitees" role="tab" aria-controls="tab-invitees" aria-selected="false">
<x-icon-user-circle width="20" />
<span>Invitees</span>
</button>
</div>
</div>