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:
parent
236da90af2
commit
864e0d938a
@ -140,7 +140,8 @@ class CalendarController extends Controller
|
|||||||
|
|
||||||
$allDayEvents = $events->filter(fn ($event) => !empty($event['all_day']));
|
$allDayEvents = $events->filter(fn ($event) => !empty($event['all_day']));
|
||||||
$timeEvents = $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')) {
|
if ($request->boolean('debug_all_day')) {
|
||||||
$calendarIds = $calendars->pluck('id')->values();
|
$calendarIds = $calendars->pluck('id')->values();
|
||||||
@ -297,7 +298,7 @@ class CalendarController extends Controller
|
|||||||
'hgroup' => $viewBuilder->viewHeaders($view, $range, $tz, $weekStart),
|
'hgroup' => $viewBuilder->viewHeaders($view, $range, $tz, $weekStart),
|
||||||
'events' => $events, // keyed by occurrence
|
'events' => $events, // keyed by occurrence
|
||||||
'events_time' => $timeEvents,
|
'events_time' => $timeEvents,
|
||||||
'events_all_day'=> $allDayEvents,
|
'events_all_day'=> $allDayDisplay,
|
||||||
'has_all_day' => $hasAllDayEvents,
|
'has_all_day' => $hasAllDayEvents,
|
||||||
'grid' => $grid, // day objects hold only ID-sets
|
'grid' => $grid, // day objects hold only ID-sets
|
||||||
'mini' => $mini, // mini calendar days with events for indicators
|
'mini' => $mini, // mini calendar days with events for indicators
|
||||||
@ -371,6 +372,52 @@ class CalendarController extends Controller
|
|||||||
return view('calendar.index', $payload);
|
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
|
* create sabre calendar + meta
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -58,6 +58,7 @@ class EventController extends Controller
|
|||||||
'tz',
|
'tz',
|
||||||
'rrule',
|
'rrule',
|
||||||
);
|
);
|
||||||
|
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start, $tz, $rrule));
|
||||||
|
|
||||||
if ($request->header('HX-Request') === 'true') {
|
if ($request->header('HX-Request') === 'true') {
|
||||||
return view('event.partials.form-modal', $data);
|
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 = compact('calendar', 'instance', 'event', 'start', 'end', 'tz', 'rrule');
|
||||||
|
$data = array_merge($data, $this->buildRecurrenceFormData($request, $start ?? '', $tz, $rrule));
|
||||||
|
|
||||||
if ($request->header('HX-Request') === 'true') {
|
if ($request->header('HX-Request') === 'true') {
|
||||||
return view('event.partials.form-modal', $data);
|
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);
|
: 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
|
private function mergeRecurrenceExtra(array $extra, ?string $rrule, string $tz, Request $request): array
|
||||||
{
|
{
|
||||||
if ($rrule === null) {
|
if ($rrule === null) {
|
||||||
|
|||||||
@ -58,14 +58,40 @@ class CalendarViewBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (empty($occurrences) && !$isRecurring) {
|
if (empty($occurrences) && !$isRecurring) {
|
||||||
$startUtc = $e->meta?->start_at
|
$meta = $e->meta;
|
||||||
? Carbon::parse($e->meta->start_at)->utc()
|
$isAllDay = (bool) ($meta?->all_day ?? false);
|
||||||
: Carbon::createFromTimestamp($e->firstoccurence, 'UTC');
|
$startUtc = null;
|
||||||
$endUtc = $e->meta?->end_at
|
$endUtc = null;
|
||||||
? Carbon::parse($e->meta->end_at)->utc()
|
|
||||||
: ($e->lastoccurence
|
if ($isAllDay && $meta?->start_on && $meta?->end_on) {
|
||||||
? Carbon::createFromTimestamp($e->lastoccurence, 'UTC')
|
$allDayTz = $meta?->tzid
|
||||||
: $startUtc->copy());
|
?? ($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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$occurrences[] = [
|
$occurrences[] = [
|
||||||
'start' => $startUtc,
|
'start' => $startUtc,
|
||||||
@ -86,12 +112,18 @@ class CalendarViewBuilder
|
|||||||
$colorFg,
|
$colorFg,
|
||||||
$gridStartMinutes,
|
$gridStartMinutes,
|
||||||
$gridEndMinutes,
|
$gridEndMinutes,
|
||||||
$daytimeHours
|
$daytimeHours,
|
||||||
|
$spanStartUtc,
|
||||||
|
$spanEndUtc
|
||||||
) {
|
) {
|
||||||
$startUtc = $occ['start'];
|
$startUtc = $occ['start'];
|
||||||
$endUtc = $occ['end'];
|
$endUtc = $occ['end'];
|
||||||
$isAllDay = (bool) ($e->meta?->all_day ?? false);
|
$isAllDay = (bool) ($e->meta?->all_day ?? false);
|
||||||
|
|
||||||
|
if ($endUtc->lte($spanStartUtc) || $startUtc->gte($spanEndUtc)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$startLocal = $startUtc->copy()->timezone($timezone);
|
$startLocal = $startUtc->copy()->timezone($timezone);
|
||||||
$endLocal = $endUtc->copy()->timezone($timezone);
|
$endLocal = $endUtc->copy()->timezone($timezone);
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,7 @@ return [
|
|||||||
'notes' => 'Notes',
|
'notes' => 'Notes',
|
||||||
'no_description' => 'No description yet.',
|
'no_description' => 'No description yet.',
|
||||||
'all_day_events' => 'All-day events',
|
'all_day_events' => 'All-day events',
|
||||||
|
'show_more' => ':count more',
|
||||||
'recurrence' => [
|
'recurrence' => [
|
||||||
'label' => 'Repeat',
|
'label' => 'Repeat',
|
||||||
'frequency' => 'Frequency',
|
'frequency' => 'Frequency',
|
||||||
|
|||||||
@ -82,6 +82,7 @@ return [
|
|||||||
'notes' => 'Note',
|
'notes' => 'Note',
|
||||||
'no_description' => 'Nessuna descrizione.',
|
'no_description' => 'Nessuna descrizione.',
|
||||||
'all_day_events' => 'Eventi di tutto il giorno',
|
'all_day_events' => 'Eventi di tutto il giorno',
|
||||||
|
'show_more' => 'Mostra altri :count',
|
||||||
'recurrence' => [
|
'recurrence' => [
|
||||||
'label' => 'Ripeti',
|
'label' => 'Ripeti',
|
||||||
'frequency' => 'Frequenza',
|
'frequency' => 'Frequenza',
|
||||||
|
|||||||
@ -271,6 +271,16 @@ main {
|
|||||||
&#settings {
|
&#settings {
|
||||||
/* */
|
/* */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* main content */
|
||||||
|
section {
|
||||||
|
|
||||||
|
/* content footer defaults */
|
||||||
|
footer {
|
||||||
|
transition: padding 250ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* expanded */
|
/* expanded */
|
||||||
@ -404,6 +414,10 @@ main {
|
|||||||
@apply pl-6;
|
@apply pl-6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
@apply pb-3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,14 @@ button,
|
|||||||
box-shadow: var(--shadows);
|
box-shadow: var(--shadows);
|
||||||
--shadows: none;
|
--shadows: none;
|
||||||
|
|
||||||
|
&.button--icon {
|
||||||
|
@apply rounded-none h-full top-0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply bg-cyan-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@apply bg-cyan-200;
|
@apply bg-cyan-200;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* calendar
|
||||||
|
*
|
||||||
|
* z-index: top is currently 10; overlapping events increment, assuming this won't be 10!
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* month view
|
* month view
|
||||||
**/
|
*/
|
||||||
.calendar.month {
|
.calendar.month {
|
||||||
@apply grid col-span-3 pb-6 2xl:pb-8 pt-2;
|
@apply grid col-span-3 pb-6 2xl:pb-8 pt-2;
|
||||||
/*grid-template-rows: 2rem 1fr; */
|
/*grid-template-rows: 2rem 1fr; */
|
||||||
@ -166,7 +172,7 @@
|
|||||||
|
|
||||||
/* top day bar */
|
/* top day bar */
|
||||||
hgroup {
|
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;
|
@apply top-20;
|
||||||
|
|
||||||
span.name {
|
span.name {
|
||||||
@ -206,24 +212,29 @@
|
|||||||
|
|
||||||
/* all day bar */
|
/* all day bar */
|
||||||
ol.day {
|
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;
|
@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 var(--color-gray-200);
|
box-shadow: 0 0.25rem 0.5rem -0.25rem rgba(0,0,0,0.15);
|
||||||
|
padding: 0.25rem 0 0.2rem 6rem;
|
||||||
|
|
||||||
&::before {
|
&::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;
|
@apply uppercase text-xs font-mono text-secondary font-medium;
|
||||||
content: 'All day';
|
content: 'All day';
|
||||||
}
|
}
|
||||||
|
|
||||||
li.events {
|
li.event-wrapper {
|
||||||
@apply flex flex-col gap-1 relative overflow-x-hidden;
|
@apply flex relative overflow-x-hidden;
|
||||||
grid-row-start: var(--event-row);
|
grid-row-start: var(--event-row);
|
||||||
grid-row-end: var(--event-end);
|
grid-row-end: var(--event-end);
|
||||||
grid-column-start: var(--event-col);
|
grid-column-start: var(--event-col);
|
||||||
grid-column-end: calc(var(--event-col) + 1);
|
grid-column-end: calc(var(--event-col) + 1);
|
||||||
|
|
||||||
|
&.event-wrapper--overflow {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
a.event {
|
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);
|
background-color: var(--event-bg);
|
||||||
color: var(--event-fg);
|
color: var(--event-fg);
|
||||||
|
|
||||||
@ -235,6 +246,27 @@
|
|||||||
background-color: color-mix(in srgb, var(--event-bg) 100%, #000 10%);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,10 +21,10 @@ dialog {
|
|||||||
@apply relative rounded-xl bg-white border-gray-200 p-0;
|
@apply relative rounded-xl bg-white border-gray-200 p-0;
|
||||||
@apply flex flex-col items-start col-start-1 translate-y-4;
|
@apply flex flex-col items-start col-start-1 translate-y-4;
|
||||||
@apply overscroll-contain overflow-y-auto;
|
@apply overscroll-contain overflow-y-auto;
|
||||||
max-height: calc(100vh - 5em);
|
max-height: calc(100dvh - 5rem);
|
||||||
width: 91.666667%;
|
width: 91.666667%;
|
||||||
max-width: 36rem;
|
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);
|
box-shadow: 0 1.5rem 4rem -0.5rem rgba(0, 0, 0, 0.4);
|
||||||
|
|
||||||
> .close-modal {
|
> .close-modal {
|
||||||
@ -36,7 +36,7 @@ dialog {
|
|||||||
|
|
||||||
/* modal header */
|
/* modal header */
|
||||||
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 {
|
h2 {
|
||||||
@apply pr-12;
|
@apply pr-12;
|
||||||
@ -55,7 +55,7 @@ dialog {
|
|||||||
|
|
||||||
/* footer */
|
/* footer */
|
||||||
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 */
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -19,6 +19,11 @@ const SELECTORS = {
|
|||||||
monthlyMode: '[data-monthly-mode]',
|
monthlyMode: '[data-monthly-mode]',
|
||||||
monthlyDays: '[data-monthly-days]',
|
monthlyDays: '[data-monthly-days]',
|
||||||
monthlyWeekday: '[data-monthly-weekday]',
|
monthlyWeekday: '[data-monthly-weekday]',
|
||||||
|
modalDialog: 'dialog',
|
||||||
|
modalContent: '#modal',
|
||||||
|
tabsRoot: '[data-tabs]',
|
||||||
|
tabButton: '[role=\"tab\"]',
|
||||||
|
tabPanel: '[role=\"tabpanel\"]',
|
||||||
monthDay: '.calendar.month .day',
|
monthDay: '.calendar.month .day',
|
||||||
monthDayEvent: 'a.event',
|
monthDayEvent: 'a.event',
|
||||||
monthDayMore: '[data-day-more]',
|
monthDayMore: '[data-day-more]',
|
||||||
@ -43,6 +48,14 @@ document.addEventListener('htmx:configRequest', (evt) => {
|
|||||||
if (token) evt.detail.headers['X-CSRF-TOKEN'] = token
|
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)
|
* 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)
|
* auto-scroll time views to 8am on load (when daytime hours are disabled)
|
||||||
*/
|
*/
|
||||||
function initTimeViewAutoScroll(root = document)
|
function initTimeViewAutoScroll(root = document)
|
||||||
@ -266,8 +347,8 @@ function initTimeViewAutoScroll(root = document)
|
|||||||
if (calendar.dataset.autoscrolled === '1') return;
|
if (calendar.dataset.autoscrolled === '1') return;
|
||||||
if (calendar.dataset.daytimeHoursEnabled === '1') return;
|
if (calendar.dataset.daytimeHoursEnabled === '1') return;
|
||||||
|
|
||||||
// find the target minute (7:45am)
|
// find the target minute (8:00am)
|
||||||
const target = calendar.querySelector('[data-slot-minutes="465"]');
|
const target = calendar.querySelector('[data-slot-minutes="480"]');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
// get the scroll container and offset
|
// get the scroll container and offset
|
||||||
@ -540,6 +621,8 @@ function initUI() {
|
|||||||
initColorPickers();
|
initColorPickers();
|
||||||
initEventAllDayToggles();
|
initEventAllDayToggles();
|
||||||
initRecurrenceControls();
|
initRecurrenceControls();
|
||||||
|
initModalHandlers();
|
||||||
|
initTabs();
|
||||||
initTimeViewAutoScroll();
|
initTimeViewAutoScroll();
|
||||||
initMonthOverflow();
|
initMonthOverflow();
|
||||||
}
|
}
|
||||||
@ -549,9 +632,16 @@ document.addEventListener('DOMContentLoaded', initUI);
|
|||||||
|
|
||||||
// rebind in htmx for swapped content
|
// rebind in htmx for swapped content
|
||||||
document.addEventListener('htmx:afterSwap', (e) => {
|
document.addEventListener('htmx:afterSwap', (e) => {
|
||||||
|
const target = e.detail?.target || e.target;
|
||||||
|
if (target && target.id === 'modal') {
|
||||||
|
target.closest('dialog')?.showModal();
|
||||||
|
}
|
||||||
|
|
||||||
initColorPickers(e.target);
|
initColorPickers(e.target);
|
||||||
initEventAllDayToggles(e.target);
|
initEventAllDayToggles(e.target);
|
||||||
initRecurrenceControls(e.target);
|
initRecurrenceControls(e.target);
|
||||||
|
initModalHandlers(e.target);
|
||||||
|
initTabs(e.target);
|
||||||
initTimeViewAutoScroll(e.target);
|
initTimeViewAutoScroll(e.target);
|
||||||
initMonthOverflow(e.target);
|
initMonthOverflow(e.target);
|
||||||
});
|
});
|
||||||
|
|||||||
1
resources/svg/icons/repeat.svg
Normal file
1
resources/svg/icons/repeat.svg
Normal 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 |
@ -121,6 +121,7 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
name="date"
|
name="date"
|
||||||
value="{{ $nav['prev'] }}"
|
value="{{ $nav['prev'] }}"
|
||||||
|
class="button--icon"
|
||||||
aria-label="Go back 1 month">
|
aria-label="Go back 1 month">
|
||||||
<x-icon-chevron-left />
|
<x-icon-chevron-left />
|
||||||
</x-button.group-button>
|
</x-button.group-button>
|
||||||
@ -135,6 +136,7 @@
|
|||||||
type="submit"
|
type="submit"
|
||||||
name="date"
|
name="date"
|
||||||
value="{{ $nav['next'] }}"
|
value="{{ $nav['next'] }}"
|
||||||
|
class="button--icon"
|
||||||
aria-label="Go forward 1 month">
|
aria-label="Go forward 1 month">
|
||||||
<x-icon-chevron-right />
|
<x-icon-chevron-right />
|
||||||
</x-button.group-button>
|
</x-button.group-button>
|
||||||
|
|||||||
@ -2,26 +2,46 @@
|
|||||||
'event' => [],
|
'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-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="
|
style="
|
||||||
--event-col: {{ $event['start_col'] ?? 1 }};
|
--event-col: {{ $event['start_col'] ?? 1 }};
|
||||||
--event-bg: {{ $event['color'] }};
|
--event-row: {{ $row }};
|
||||||
--event-fg: {{ $event['color_fg'] }};">
|
--event-end: {{ $end }};
|
||||||
@php
|
--event-bg: {{ $event['color'] ?? 'var(--color-gray-100)' }};
|
||||||
$showParams = [$event['calendar_slug'], $event['id']];
|
--event-fg: {{ $event['color_fg'] ?? 'var(--color-primary)' }};">
|
||||||
if (!empty($event['occurrence'])) {
|
@if($isMore)
|
||||||
$showParams['occurrence'] = $event['occurrence'];
|
@php
|
||||||
}
|
$moreId = 'more-col-'.$event['start_col'];
|
||||||
@endphp
|
@endphp
|
||||||
<a class="event{{ $event['visible'] ? '' : ' hidden' }}"
|
<label class="event event--more" for="{{ $moreId }}" tabindex="0">
|
||||||
href="{{ route('calendar.event.show', $showParams) }}"
|
<input id="{{ $moreId }}" type="checkbox" class="more-checkbox" />
|
||||||
hx-get="{{ route('calendar.event.show', $showParams) }}"
|
<span>{{ __('calendar.event.show_more', ['count' => $event['all_day_more_count'] ?? 0]) }}</span>
|
||||||
hx-target="#modal"
|
</label>
|
||||||
hx-push-url="true"
|
@else
|
||||||
hx-swap="innerHTML"
|
@php
|
||||||
data-calendar="{{ $event['calendar_slug'] }}">
|
$showParams = [$event['calendar_slug'], $event['id']];
|
||||||
<span>{{ $event['title'] }}</span>
|
if (!empty($event['occurrence'])) {
|
||||||
</a>
|
$showParams['occurrence'] = $event['occurrence'];
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
<a class="event{{ $event['visible'] ? '' : ' hidden' }}"
|
||||||
|
href="{{ route('calendar.event.show', $showParams) }}"
|
||||||
|
hx-get="{{ route('calendar.event.show', $showParams) }}"
|
||||||
|
hx-target="#modal"
|
||||||
|
hx-push-url="true"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
data-calendar="{{ $event['calendar_slug'] }}">
|
||||||
|
<span>{{ $event['title'] }}</span>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -1,11 +1,6 @@
|
|||||||
<dialog
|
<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);"
|
|
||||||
>
|
|
||||||
<div id="modal"
|
<div id="modal"
|
||||||
hx-target="this"
|
hx-target="this"
|
||||||
hx-on::before-swap="this.dataset.prevUrl = window.location.href"
|
|
||||||
hx-on::after-swap="this.closest('dialog')?.showModal()"
|
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<x-slot name="header">
|
<x-slot name="header">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-xl font-semibold leading-tight">
|
<h2 class="text-xl font-semibold leading-tight">
|
||||||
{{ $event->exists ? __('Edit Event') : __('Create Event') }}
|
{{ $event->exists ? __('Edit event details') : __('Create a new event') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{{-- “Back” breadcrumb --}}
|
{{-- “Back” breadcrumb --}}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<x-modal.content>
|
<x-modal.content class="modal-tabs">
|
||||||
<x-modal.title>
|
<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.title>
|
||||||
<x-modal.body>
|
<x-modal.body>
|
||||||
@include('event.partials.form', [
|
@include('event.partials.form', [
|
||||||
@ -17,7 +17,7 @@
|
|||||||
{{ __('common.cancel') }}
|
{{ __('common.cancel') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
<x-button variant="primary" type="submit" form="event-form">
|
<x-button variant="primary" type="submit" form="event-form">
|
||||||
{{ $event->exists ? __('Save') : __('Create') }}
|
{{ $event->exists ? __('common.save') : __('Create event') }}
|
||||||
</x-button>
|
</x-button>
|
||||||
</x-modal.footer>
|
</x-modal.footer>
|
||||||
</x-modal.content>
|
</x-modal.content>
|
||||||
|
|||||||
@ -3,83 +3,6 @@
|
|||||||
$formAction = $event->exists
|
$formAction = $event->exists
|
||||||
? route('calendar.event.update', [$calendar, $event])
|
? route('calendar.event.update', [$calendar, $event])
|
||||||
: route('calendar.event.store', $calendar);
|
: 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
|
@endphp
|
||||||
|
|
||||||
<form method="POST" id="event-form" action="{{ $formAction }}" class="settings modal">
|
<form method="POST" id="event-form" action="{{ $formAction }}" class="settings modal">
|
||||||
@ -88,219 +11,244 @@
|
|||||||
@method('PUT')
|
@method('PUT')
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{-- Title --}}
|
<div class="event-tabs" data-tabs>
|
||||||
<div class="input-row input-row--1">
|
<div class="tab-panels">
|
||||||
<div class="input-cell">
|
<section id="tab-details" role="tabpanel" aria-labelledby="tab-btn-details">
|
||||||
<x-input.label for="title" :value="__('Title')" />
|
{{-- Title --}}
|
||||||
<x-input.text
|
<div class="input-row input-row--1">
|
||||||
id="title"
|
<div class="input-cell">
|
||||||
name="title"
|
<x-input.label for="title" :value="__('Title')" />
|
||||||
type="text"
|
<x-input.text
|
||||||
:value="old('title', $event->meta?->title ?? '')"
|
id="title"
|
||||||
required
|
name="title"
|
||||||
autofocus
|
type="text"
|
||||||
/>
|
:value="old('title', $event->meta?->title ?? '')"
|
||||||
<x-input.error class="mt-2" :messages="$errors->get('title')" />
|
required
|
||||||
</div>
|
autofocus
|
||||||
</div>
|
/>
|
||||||
|
<x-input.error class="mt-2" :messages="$errors->get('title')" />
|
||||||
{{-- Description --}}
|
|
||||||
<div class="input-row input-row--1">
|
|
||||||
<div class="input-cell">
|
|
||||||
<x-input.label for="description" :value="__('Description')" />
|
|
||||||
<x-input.textarea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
rows="3">{{ old('description', $event->meta?->description ?? '') }}</x-input.textarea>
|
|
||||||
<x-input.error :messages="$errors->get('description')" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Location --}}
|
|
||||||
<div class="input-row input-row--1">
|
|
||||||
<div class="input-cell">
|
|
||||||
<x-input.label for="location" :value="__('Location')" />
|
|
||||||
<x-input.text
|
|
||||||
id="location"
|
|
||||||
name="location"
|
|
||||||
:value="old('location', $event->meta?->location ?? '')"
|
|
||||||
{{-- live suggestions via htmx --}}
|
|
||||||
hx-get="{{ route('location.suggest') }}"
|
|
||||||
hx-trigger="keyup changed delay:300ms"
|
|
||||||
hx-target="#location-suggestions"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
/>
|
|
||||||
<x-input.error :messages="$errors->get('location')" />
|
|
||||||
|
|
||||||
{{-- suggestion dropdown target --}}
|
|
||||||
<div id="location-suggestions" class="relative z-20"></div>
|
|
||||||
{{-- hidden fields (filled when user clicks a suggestion; handy for step #2) --}}
|
|
||||||
<input type="hidden" id="loc_display_name" name="loc_display_name" />
|
|
||||||
<input type="hidden" id="loc_place_name" name="loc_place_name" />
|
|
||||||
<input type="hidden" id="loc_street" name="loc_street" />
|
|
||||||
<input type="hidden" id="loc_city" name="loc_city" />
|
|
||||||
<input type="hidden" id="loc_state" name="loc_state" />
|
|
||||||
<input type="hidden" id="loc_postal" name="loc_postal" />
|
|
||||||
<input type="hidden" id="loc_country" name="loc_country" />
|
|
||||||
<input type="hidden" id="loc_lat" name="loc_lat" />
|
|
||||||
<input type="hidden" id="loc_lon" name="loc_lon" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Start / End --}}
|
|
||||||
<div class="input-row input-row--1-1">
|
|
||||||
<div class="input-cell">
|
|
||||||
<x-input.label for="start_at" :value="__('Starts')" />
|
|
||||||
<x-input.text
|
|
||||||
id="start_at"
|
|
||||||
name="start_at"
|
|
||||||
type="datetime-local"
|
|
||||||
:value="old('start_at', $start)"
|
|
||||||
data-event-start
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<x-input.error :messages="$errors->get('start_at')" />
|
|
||||||
</div>
|
|
||||||
<div class="input-cell">
|
|
||||||
<x-input.label for="end_at" :value="__('Ends')" />
|
|
||||||
<x-input.text
|
|
||||||
id="end_at"
|
|
||||||
name="end_at"
|
|
||||||
type="datetime-local"
|
|
||||||
:value="old('end_at', $end)"
|
|
||||||
data-event-end
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<x-input.error :messages="$errors->get('end_at')" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- All-day --}}
|
|
||||||
<div class="input-row input-row--1">
|
|
||||||
<div class="input-cell">
|
|
||||||
<x-input.checkbox-label
|
|
||||||
label="{{ __('All day event') }}"
|
|
||||||
id="all_day"
|
|
||||||
name="all_day"
|
|
||||||
value="1"
|
|
||||||
data-all-day-toggle
|
|
||||||
:checked="(bool) old('all_day', $event->meta?->all_day)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Recurrence (advanced) --}}
|
|
||||||
<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
|
|
||||||
id="repeat_frequency"
|
|
||||||
name="repeat_frequency"
|
|
||||||
:options="$rruleOptions"
|
|
||||||
:selected="$repeatFrequency"
|
|
||||||
:placeholder="__('calendar.event.recurrence.none')"
|
|
||||||
data-recurrence-frequency
|
|
||||||
/>
|
|
||||||
<x-input.error :messages="$errors->get('repeat_frequency')" />
|
|
||||||
|
|
||||||
<div class="mt-3 {{ $repeatFrequency === '' ? 'hidden' : '' }}" data-recurrence-interval>
|
|
||||||
<x-input.label for="repeat_interval" :value="__('calendar.event.recurrence.every')" />
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<x-input.text
|
|
||||||
id="repeat_interval"
|
|
||||||
name="repeat_interval"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="365"
|
|
||||||
:value="$repeatInterval"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-gray-600" data-recurrence-unit></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 {{ $repeatFrequency !== 'weekly' ? 'hidden' : '' }}" data-recurrence-section="weekly">
|
|
||||||
<p class="text-sm text-gray-600">{{ __('calendar.event.recurrence.on_days') }}</p>
|
|
||||||
<div class="flex flex-wrap gap-2 mt-2">
|
|
||||||
@foreach ($weekdayOptions as $code => $label)
|
|
||||||
<label class="inline-flex items-center gap-2 text-sm">
|
|
||||||
<x-input.checkbox
|
|
||||||
name="repeat_weekdays[]"
|
|
||||||
value="{{ $code }}"
|
|
||||||
:checked="in_array($code, (array) $repeatWeekdays, true)"
|
|
||||||
/>
|
|
||||||
<span title="{{ $weekdayLong[$code] ?? $code }}">{{ $label }}</span>
|
|
||||||
</label>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 {{ $repeatFrequency !== 'monthly' ? 'hidden' : '' }}" data-recurrence-section="monthly">
|
|
||||||
<div class="flex flex-col gap-3">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<x-input.radio-label
|
|
||||||
id="repeat_monthly_mode_days"
|
|
||||||
name="repeat_monthly_mode"
|
|
||||||
value="days"
|
|
||||||
label="{{ __('calendar.event.recurrence.on_days') }}"
|
|
||||||
:checked="$repeatMonthMode === 'days'"
|
|
||||||
data-monthly-mode
|
|
||||||
/>
|
|
||||||
<x-input.radio-label
|
|
||||||
id="repeat_monthly_mode_weekday"
|
|
||||||
name="repeat_monthly_mode"
|
|
||||||
value="weekday"
|
|
||||||
label="{{ __('calendar.event.recurrence.on_the') }}"
|
|
||||||
:checked="$repeatMonthMode === 'weekday'"
|
|
||||||
data-monthly-mode
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-7 gap-2 {{ $repeatMonthMode !== 'days' ? 'hidden' : '' }}" data-monthly-days>
|
|
||||||
@for ($day = 1; $day <= 31; $day++)
|
|
||||||
<label class="inline-flex items-center gap-2 text-xs">
|
|
||||||
<x-input.checkbox
|
|
||||||
name="repeat_month_days[]"
|
|
||||||
value="{{ $day }}"
|
|
||||||
:checked="in_array($day, (array) $repeatMonthDays)"
|
|
||||||
/>
|
|
||||||
<span>{{ $day }}</span>
|
|
||||||
</label>
|
|
||||||
@endfor
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-3 {{ $repeatMonthMode !== 'weekday' ? 'hidden' : '' }}" data-monthly-weekday>
|
|
||||||
<x-input.select
|
|
||||||
id="repeat_month_week"
|
|
||||||
name="repeat_month_week"
|
|
||||||
:options="[
|
|
||||||
'first' => __('calendar.event.recurrence.week_order.first'),
|
|
||||||
'second' => __('calendar.event.recurrence.week_order.second'),
|
|
||||||
'third' => __('calendar.event.recurrence.week_order.third'),
|
|
||||||
'fourth' => __('calendar.event.recurrence.week_order.fourth'),
|
|
||||||
'last' => __('calendar.event.recurrence.week_order.last'),
|
|
||||||
]"
|
|
||||||
:selected="$repeatMonthWeek"
|
|
||||||
/>
|
|
||||||
<x-input.select
|
|
||||||
id="repeat_month_weekday"
|
|
||||||
name="repeat_month_weekday"
|
|
||||||
:options="$weekdayLong"
|
|
||||||
:selected="$repeatMonthWeekday"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 text-sm text-gray-600 {{ $repeatFrequency !== 'yearly' ? 'hidden' : '' }}" data-recurrence-section="yearly">
|
|
||||||
{{ __('calendar.event.recurrence.yearly_hint') }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
|
||||||
|
{{-- Description --}}
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="description" :value="__('Description')" />
|
||||||
|
<x-input.textarea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
rows="3">{{ old('description', $event->meta?->description ?? '') }}</x-input.textarea>
|
||||||
|
<x-input.error :messages="$errors->get('description')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Location --}}
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="location" :value="__('Location')" />
|
||||||
|
<x-input.text
|
||||||
|
id="location"
|
||||||
|
name="location"
|
||||||
|
:value="old('location', $event->meta?->location ?? '')"
|
||||||
|
{{-- live suggestions via htmx --}}
|
||||||
|
hx-get="{{ route('location.suggest') }}"
|
||||||
|
hx-trigger="keyup changed delay:300ms"
|
||||||
|
hx-target="#location-suggestions"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
/>
|
||||||
|
<x-input.error :messages="$errors->get('location')" />
|
||||||
|
|
||||||
|
{{-- suggestion dropdown target --}}
|
||||||
|
<div id="location-suggestions" class="relative z-20"></div>
|
||||||
|
{{-- hidden fields (filled when user clicks a suggestion; handy for step #2) --}}
|
||||||
|
<input type="hidden" id="loc_display_name" name="loc_display_name" />
|
||||||
|
<input type="hidden" id="loc_place_name" name="loc_place_name" />
|
||||||
|
<input type="hidden" id="loc_street" name="loc_street" />
|
||||||
|
<input type="hidden" id="loc_city" name="loc_city" />
|
||||||
|
<input type="hidden" id="loc_state" name="loc_state" />
|
||||||
|
<input type="hidden" id="loc_postal" name="loc_postal" />
|
||||||
|
<input type="hidden" id="loc_country" name="loc_country" />
|
||||||
|
<input type="hidden" id="loc_lat" name="loc_lat" />
|
||||||
|
<input type="hidden" id="loc_lon" name="loc_lon" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Start / End --}}
|
||||||
|
<div class="input-row input-row--1-1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="start_at" :value="__('Starts')" />
|
||||||
|
<x-input.text
|
||||||
|
id="start_at"
|
||||||
|
name="start_at"
|
||||||
|
type="datetime-local"
|
||||||
|
:value="old('start_at', $start)"
|
||||||
|
data-event-start
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<x-input.error :messages="$errors->get('start_at')" />
|
||||||
|
</div>
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.label for="end_at" :value="__('Ends')" />
|
||||||
|
<x-input.text
|
||||||
|
id="end_at"
|
||||||
|
name="end_at"
|
||||||
|
type="datetime-local"
|
||||||
|
:value="old('end_at', $end)"
|
||||||
|
data-event-end
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<x-input.error :messages="$errors->get('end_at')" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- All-day --}}
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<x-input.checkbox-label
|
||||||
|
label="{{ __('All day event') }}"
|
||||||
|
id="all_day"
|
||||||
|
name="all_day"
|
||||||
|
value="1"
|
||||||
|
data-all-day-toggle
|
||||||
|
:checked="(bool) old('all_day', $event->meta?->all_day)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="tab-repeat" role="tabpanel" aria-labelledby="tab-btn-repeat" hidden>
|
||||||
|
<div class="input-row input-row--1">
|
||||||
|
<div class="input-cell">
|
||||||
|
<div class="mt-3">
|
||||||
|
<x-input.label for="repeat_frequency" :value="__('calendar.event.recurrence.frequency')" />
|
||||||
|
<x-input.select
|
||||||
|
id="repeat_frequency"
|
||||||
|
name="repeat_frequency"
|
||||||
|
:options="$rruleOptions"
|
||||||
|
:selected="$repeatFrequency"
|
||||||
|
:placeholder="__('calendar.event.recurrence.none')"
|
||||||
|
data-recurrence-frequency
|
||||||
|
/>
|
||||||
|
<x-input.error :messages="$errors->get('repeat_frequency')" />
|
||||||
|
|
||||||
|
<div class="mt-3 {{ $repeatFrequency === '' ? 'hidden' : '' }}" data-recurrence-interval>
|
||||||
|
<x-input.label for="repeat_interval" :value="__('calendar.event.recurrence.every')" />
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<x-input.text
|
||||||
|
id="repeat_interval"
|
||||||
|
name="repeat_interval"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="365"
|
||||||
|
:value="$repeatInterval"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-gray-600" data-recurrence-unit></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 {{ $repeatFrequency !== 'weekly' ? 'hidden' : '' }}" data-recurrence-section="weekly">
|
||||||
|
<p class="text-sm text-gray-600">{{ __('calendar.event.recurrence.on_days') }}</p>
|
||||||
|
<div class="flex flex-wrap gap-2 mt-2">
|
||||||
|
@foreach ($weekdayOptions as $code => $label)
|
||||||
|
<label class="inline-flex items-center gap-2 text-sm">
|
||||||
|
<x-input.checkbox
|
||||||
|
name="repeat_weekdays[]"
|
||||||
|
value="{{ $code }}"
|
||||||
|
:checked="in_array($code, (array) $repeatWeekdays, true)"
|
||||||
|
/>
|
||||||
|
<span title="{{ $weekdayLong[$code] ?? $code }}">{{ $label }}</span>
|
||||||
|
</label>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 {{ $repeatFrequency !== 'monthly' ? 'hidden' : '' }}" data-recurrence-section="monthly">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<x-input.radio-label
|
||||||
|
id="repeat_monthly_mode_days"
|
||||||
|
name="repeat_monthly_mode"
|
||||||
|
value="days"
|
||||||
|
label="{{ __('calendar.event.recurrence.on_days') }}"
|
||||||
|
:checked="$repeatMonthMode === 'days'"
|
||||||
|
data-monthly-mode
|
||||||
|
/>
|
||||||
|
<x-input.radio-label
|
||||||
|
id="repeat_monthly_mode_weekday"
|
||||||
|
name="repeat_monthly_mode"
|
||||||
|
value="weekday"
|
||||||
|
label="{{ __('calendar.event.recurrence.on_the') }}"
|
||||||
|
:checked="$repeatMonthMode === 'weekday'"
|
||||||
|
data-monthly-mode
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 gap-2 {{ $repeatMonthMode !== 'days' ? 'hidden' : '' }}" data-monthly-days>
|
||||||
|
@for ($day = 1; $day <= 31; $day++)
|
||||||
|
<label class="inline-flex items-center gap-2 text-xs">
|
||||||
|
<x-input.checkbox
|
||||||
|
name="repeat_month_days[]"
|
||||||
|
value="{{ $day }}"
|
||||||
|
:checked="in_array($day, (array) $repeatMonthDays)"
|
||||||
|
/>
|
||||||
|
<span>{{ $day }}</span>
|
||||||
|
</label>
|
||||||
|
@endfor
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 {{ $repeatMonthMode !== 'weekday' ? 'hidden' : '' }}" data-monthly-weekday>
|
||||||
|
<x-input.select
|
||||||
|
id="repeat_month_week"
|
||||||
|
name="repeat_month_week"
|
||||||
|
:options="[
|
||||||
|
'first' => __('calendar.event.recurrence.week_order.first'),
|
||||||
|
'second' => __('calendar.event.recurrence.week_order.second'),
|
||||||
|
'third' => __('calendar.event.recurrence.week_order.third'),
|
||||||
|
'fourth' => __('calendar.event.recurrence.week_order.fourth'),
|
||||||
|
'last' => __('calendar.event.recurrence.week_order.last'),
|
||||||
|
]"
|
||||||
|
:selected="$repeatMonthWeek"
|
||||||
|
/>
|
||||||
|
<x-input.select
|
||||||
|
id="repeat_month_weekday"
|
||||||
|
name="repeat_month_weekday"
|
||||||
|
:options="$weekdayLong"
|
||||||
|
:selected="$repeatMonthWeekday"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 text-sm text-gray-600 {{ $repeatFrequency !== 'yearly' ? 'hidden' : '' }}" data-recurrence-section="yearly">
|
||||||
|
{{ __('calendar.event.recurrence.yearly_hint') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user