diff --git a/app/Http/Controllers/EventController.php b/app/Http/Controllers/EventController.php index 22ce043..1f40982 100644 --- a/app/Http/Controllers/EventController.php +++ b/app/Http/Controllers/EventController.php @@ -103,11 +103,24 @@ class EventController extends Controller $this->authorize('view', $event); - $event->load(['meta', 'meta.venue']); + $event->loadMissing(['meta', 'meta.venue']); $isHtmx = $request->header('HX-Request') === 'true'; $tz = $this->displayTimezone($calendar, $request); + $instance = $calendar->instanceForUser($request->user()); + if ($instance) { + $instance->loadMissing('meta'); + } + + $calendarColor = calendar_color( + ['color' => $instance?->meta?->color], + $instance?->calendarcolor + ); + $calendarColorFg = $instance?->meta?->color_fg + ?? contrast_text_color($calendarColor); + $calendarName = $instance?->displayname ?? __('common.calendar'); + // prefer occurrence when supplied (recurring events), fall back to meta, then sabre columns $occurrenceParam = $request->query('occurrence'); $occurrenceStart = null; @@ -136,7 +149,12 @@ class EventController extends Controller $start = $startUtc->copy()->timezone($tz); $end = $endUtc->copy()->timezone($tz); - $data = compact('calendar', 'event', 'start', 'end', 'tz'); + $map = $this->buildBasemapTiles($event->meta?->venue); + + $event->setAttribute('color', $calendarColor); + $event->setAttribute('color_fg', $calendarColorFg); + + $data = compact('calendar', 'event', 'start', 'end', 'tz', 'map', 'calendarName'); return $isHtmx ? view('event.partials.details', $data) @@ -412,4 +430,53 @@ class EventController extends Controller return Location::labelOnly($raw)->id; } + + /** + * build static basemap tiles for an event location + */ + private function buildBasemapTiles(?Location $venue): array + { + $map = [ + 'enabled' => false, + 'needs_key' => false, + 'url' => null, + 'zoom' => (int) config('services.geocoding.arcgis.basemap_zoom', 14), + ]; + + if (!$venue || !is_numeric($venue->lat) || !is_numeric($venue->lon)) { + return $map; + } + + $token = config('services.geocoding.arcgis.api_key'); + if (!$token) { + $map['needs_key'] = true; + return $map; + } + + $style = config('services.geocoding.arcgis.basemap_style', 'arcgis/light-gray'); + $zoom = max(0, (int) $map['zoom']); + + $lat = max(min((float) $venue->lat, 85.05112878), -85.05112878); + $lon = (float) $venue->lon; + $n = 2 ** $zoom; + + $x = (int) floor(($lon + 180.0) / 360.0 * $n); + $latRad = deg2rad($lat); + $y = (int) floor((1.0 - log(tan($latRad) + (1 / cos($latRad))) / M_PI) / 2.0 * $n); + + $base = 'https://static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/v1'; + + $tx = $x % $n; + if ($tx < 0) { + $tx += $n; + } + $ty = max(0, min($n - 1, $y)); + + $map['url'] = "{$base}/{$style}/static/tile/{$zoom}/{$ty}/{$tx}?token={$token}"; + + $map['enabled'] = true; + $map['zoom'] = $zoom; + + return $map; + } } diff --git a/app/Jobs/GeocodeEventLocations.php b/app/Jobs/GeocodeEventLocations.php index 888d7d0..e5ead19 100644 --- a/app/Jobs/GeocodeEventLocations.php +++ b/app/Jobs/GeocodeEventLocations.php @@ -69,13 +69,31 @@ class GeocodeEventLocations implements ShouldQueue } try { - // geocode the free-form location string - $norm = $geocoder->forward($meta->location); + // geocode the free-form location string; if it looks like a label, + // fall back to the normalized location record's raw address + $query = $meta->location; + $norm = $geocoder->forward($query); + + if (!$norm) { + $location = Location::where('display_name', $meta->location) + ->orWhere('raw_address', $meta->location) + ->first(); + + if ($location?->raw_address) { + $query = $location->raw_address; + $norm = $geocoder->forward($query); + } + } // skip obvious non-address labels or unresolved queries if (!$norm || (!$norm['lat'] && !$norm['street'])) { $skipped++; $processed++; + Log::info('GeocodeEventLocations: skipped', [ + 'event_id' => $meta->event_id, + 'location' => $meta->location, + 'query' => $query, + ]); continue; } diff --git a/app/Services/Calendar/CalendarViewBuilder.php b/app/Services/Calendar/CalendarViewBuilder.php index 9b4423d..1bac2c5 100644 --- a/app/Services/Calendar/CalendarViewBuilder.php +++ b/app/Services/Calendar/CalendarViewBuilder.php @@ -154,9 +154,11 @@ class CalendarViewBuilder })->filter(); // ensure chronological ordering across calendars for all views - return $payloads + $payloads = $payloads ->sortBy('start') ->keyBy('occurrence_id'); + + return $this->applyOverlapLayout($payloads, $view); } /** @@ -457,4 +459,105 @@ class CalendarViewBuilder $rounded = (int) round($minutes / $slot) * $slot; return max($min, min($rounded, $max)); } + + /** + * Apply overlap metadata for time-based views (day/four/week). + */ + private function applyOverlapLayout(Collection $events, string $view): Collection + { + if (!in_array($view, ['day', 'four', 'week'], true) || $events->isEmpty()) { + return $events; + } + + $items = $events->all(); // keyed by occurrence_id + $eventsByCol = []; + + foreach ($items as $id => $event) { + $col = (int) ($event['start_col'] ?? 1); + $eventsByCol[$col][$id] = $event; + } + + foreach ($eventsByCol as $group) { + uasort($group, function (array $a, array $b) { + $cmp = ($a['start_row'] ?? 0) <=> ($b['start_row'] ?? 0); + if ($cmp !== 0) { + return $cmp; + } + return ($a['end_row'] ?? 0) <=> ($b['end_row'] ?? 0); + }); + + $cluster = []; + $clusterEnd = null; + + foreach ($group as $id => $event) { + $startRow = (int) ($event['start_row'] ?? 0); + $endRow = (int) ($event['end_row'] ?? 0); + + if ($clusterEnd !== null && $startRow >= $clusterEnd) { + $this->assignOverlapCluster($cluster, $items); + $cluster = []; + $clusterEnd = null; + } + + $cluster[$id] = $event; + $clusterEnd = $clusterEnd === null + ? $endRow + : max($clusterEnd, $endRow); + } + + if ($cluster) { + $this->assignOverlapCluster($cluster, $items); + } + } + + return collect($items); + } + + private function assignOverlapCluster(array $cluster, array &$items): void + { + if (empty($cluster)) { + return; + } + + $active = []; + $availableCols = []; + $assigned = []; + $maxCol = 0; + + foreach ($cluster as $id => $event) { + $startRow = (int) ($event['start_row'] ?? 0); + $endRow = (int) ($event['end_row'] ?? 0); + + foreach ($active as $idx => $info) { + if ($info['end'] <= $startRow) { + $availableCols[] = $info['col']; + unset($active[$idx]); + } + } + + sort($availableCols); + + if (!empty($availableCols)) { + $col = array_shift($availableCols); + } else { + $col = $maxCol; + $maxCol++; + } + + $assigned[$id] = $col; + $active[] = ['end' => $endRow, 'col' => $col]; + } + + $totalCols = max(1, $maxCol); + $width = 100 / $totalCols; + + foreach ($cluster as $id => $event) { + $index = $assigned[$id] ?? 0; + $items[$id]['overlap_count'] = $totalCols; + $items[$id]['overlap_index'] = $index; + $items[$id]['overlap_width'] = round($width, 4); + $items[$id]['overlap_offset'] = round($width * $index, 4); + $items[$id]['overlap_z'] = $index + 1; + } + } } diff --git a/app/Services/Location/Geocoder.php b/app/Services/Location/Geocoder.php index ad81d39..31e4250 100644 --- a/app/Services/Location/Geocoder.php +++ b/app/Services/Location/Geocoder.php @@ -93,6 +93,29 @@ class Geocoder Log::warning('arcgis api key missing; geocoding disabled'); } + private function arcgisDebugEnabled(): bool + { + return (bool) ($this->cfg['arcgis']['debug'] ?? false); + } + + private function logArcgisResponse(string $label, array $params, $res): void + { + if (! $this->arcgisDebugEnabled()) { + return; + } + + $safeParams = $params; + if (isset($safeParams['token'])) { + $safeParams['token'] = '***'; + } + + Log::info("arcgis {$label} response", [ + 'status' => $res->status(), + 'params' => $safeParams, + 'body' => $res->body(), + ]); + } + /** * pull a bias from the user (zip -> centroid) and cache it */ @@ -132,6 +155,7 @@ class Geocoder ]; $res = $this->http()->get($this->arcgisBase().'/findAddressCandidates', $params); + $this->logArcgisResponse('zip-bias', $params, $res); if (!$res->ok()) { Log::info('arcgis zip bias lookup failed', ['zip' => $zip, 'status' => $res->status()]); return null; @@ -199,6 +223,7 @@ class Geocoder } $res = $this->http()->get($this->arcgisBase().'/findAddressCandidates', $params); + $this->logArcgisResponse('forward', $params, $res); if (!$res->ok()) { Log::warning('arcgis forward geocode failed', ['status' => $res->status(), 'q' => $query]); return null; @@ -206,6 +231,9 @@ class Geocoder $cands = $res->json('candidates', []); if (!$cands) { + if ($this->arcgisDebugEnabled()) { + Log::info('arcgis forward geocode empty', ['q' => $query]); + } return null; } @@ -238,6 +266,7 @@ class Geocoder } $res = $this->http()->get($this->arcgisBase().'/reverseGeocode', $params); + $this->logArcgisResponse('reverse', $params, $res); if (!$res->ok()) { Log::warning('arcgis reverse geocode failed', ['status' => $res->status()]); return null; @@ -344,6 +373,7 @@ class Geocoder $this->arcgisBase() . "/findAddressCandidates", $params, ); + $this->logArcgisResponse('suggestions', $params, $res); if (!$res->ok()) { return []; } diff --git a/bootstrap/app.php b/bootstrap/app.php index a521adb..e27afab 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ web(append: [ SetUserLocale::class, ]); + + // ensure HTMX requests redirect in the top-level window on auth expiry + $middleware->alias([ + 'auth' => HtmxAwareAuthenticate::class, + ]); }) ->withSchedule(function (Schedule $schedule) { diff --git a/config/services.php b/config/services.php index 3406d08..f161af9 100644 --- a/config/services.php +++ b/config/services.php @@ -42,6 +42,7 @@ return [ "arcgis" => [ "api_key" => env("ARCGIS_API_KEY"), "store" => (bool) env("ARCGIS_STORE_RESULTS", true), + "debug" => (bool) env("ARCGIS_DEBUG", false), "endpoint" => "https://geocode-api.arcgis.com/arcgis/rest/services/World/GeocodeServer", "country_code" => env("GEOCODER_COUNTRY", "USA"), @@ -49,6 +50,8 @@ return [ "out_fields" => "Match_addr,Addr_type,PlaceName,Place_addr,Address,City,Region,Postal,CountryCode,LongLabel", "max_results" => 1, + "basemap_style" => env("ARCGIS_BASEMAP_STYLE", "arcgis/community"), + "basemap_zoom" => (int) env("ARCGIS_BASEMAP_ZOOM", 16), ], ], ]; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 1c3c07f..70ab73a 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -307,6 +307,13 @@ ICS; $insertEvent($future5a, 'Teacher conference (3rd grade)', 'Fairview Elementary'); $insertEvent($future5b, 'Family game night', 'Living Room'); + // overlapping events 3 days after "now" + $overlapDay = $now->copy()->addDays(3)->setTime(9, 0); + $insertEvent($overlapDay->copy(), 'Overlap: Daily standup', 'Home'); + $insertEvent($overlapDay->copy()->addMinutes(15), 'Overlap: Design review', 'Living Room'); + $insertEvent($overlapDay->copy()->addMinutes(30), 'Overlap: Vendor call', 'McCahill Park'); + $insertEvent($overlapDay->copy()->addMinutes(45), 'Overlap: Planning session', 'Meadow Park'); + // recurring: weekly on Mon/Wed for 8 weeks at 6:30pm $recurringStart = $now->copy()->next(Carbon::MONDAY)->setTime(18, 30); $insertRecurringEvent( diff --git a/lang/en/calendar.php b/lang/en/calendar.php index 19e32af..4b22ff0 100644 --- a/lang/en/calendar.php +++ b/lang/en/calendar.php @@ -53,6 +53,8 @@ return [ 'all_day' => 'All day', 'location' => 'Location', 'map_coming' => 'Map preview coming soon.', + 'map_needs_key' => 'Map preview requires an ArcGIS basemap API key.', + 'map_attribution' => 'Basemap tiles © Esri and the GIS User Community.', 'no_location' => 'No location set.', 'details' => 'Details', 'repeats' => 'Repeats', diff --git a/lang/it/calendar.php b/lang/it/calendar.php index 2e22574..617322e 100644 --- a/lang/it/calendar.php +++ b/lang/it/calendar.php @@ -53,6 +53,8 @@ return [ 'all_day' => 'Tutto il giorno', 'location' => 'Luogo', 'map_coming' => 'Anteprima mappa in arrivo.', + 'map_needs_key' => 'Anteprima mappa richiede una chiave API ArcGIS.', + 'map_attribution' => 'Tessere mappa © Esri e la comunita GIS.', 'no_location' => 'Nessun luogo impostato.', 'details' => 'Dettagli', 'repeats' => 'Ripete', diff --git a/resources/css/app.css b/resources/css/app.css index a28e74e..ba2b19b 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -10,6 +10,7 @@ @import './lib/calendar.css'; @import './lib/checkbox.css'; @import './lib/color.css'; +@import './lib/event.css'; @import './lib/icon.css'; @import './lib/indicator.css'; @import './lib/input.css'; diff --git a/resources/css/etc/layout.css b/resources/css/etc/layout.css index 19409c2..9270799 100644 --- a/resources/css/etc/layout.css +++ b/resources/css/etc/layout.css @@ -183,11 +183,14 @@ main { /* main content wrapper */ article { - @apply bg-white grid grid-cols-1 ml-2 rounded-md; + @apply bg-white grid grid-cols-1 ml-2 rounded-md z-2; @apply overflow-y-auto; grid-template-rows: 5rem auto; container: content / inline-size; - transition: margin 200ms ease-in-out, width 200ms ease-in-out; + transition: + margin 250ms ease-in-out, + width 250ms ease-in-out, + padding 250ms ease-in-out; /* main content title and actions */ header { @@ -259,8 +262,8 @@ main { @apply ml-1 opacity-0 pointer-events-none transition-opacity duration-150; } - &:hover .calendar-expand-toggle, - &:focus-within .calendar-expand-toggle { + h2:hover .calendar-expand-toggle, + h2:focus-within .calendar-expand-toggle { @apply opacity-100 pointer-events-auto; } } @@ -281,21 +284,6 @@ main { } } -/* container sizing */ -@container content (width >= 64rem) -{ - main { - article { - header { - menu { - @apply relative top-auto right-auto h-auto w-auto rounded-none bg-transparent; - @apply flex flex-row items-center justify-end gap-4 p-0; - } - } - } - } -} - /* app logo */ .logo { @apply w-10 h-10 flex; @@ -321,13 +309,29 @@ main { * desktop handling */ -/* show app nav on the left at md */ +/* menu handling */ +@container content (width >= 64rem) +{ + main { + article { + header { + menu { + @apply relative top-auto right-auto h-auto w-auto rounded-none bg-transparent; + @apply flex flex-row items-center justify-end gap-4 p-0; + } + } + } + } +} + +/* default desktop handling */ @media (width >= theme(--breakpoint-md)) { body#app { grid-template-columns: 5rem auto; grid-template-rows: 1fr 0; + /* show app nav on the left at md */ > nav { @apply relative w-20 flex-col items-center justify-between px-0 pb-0 order-0; @@ -354,6 +358,7 @@ main { } } + /* main content with and without asides */ main { &:has(aside) { @@ -363,7 +368,11 @@ main { aside { @apply bg-white overflow-y-auto h-full min-w-48; - transition: translate 200ms ease-in-out, visibility 200ms ease-in-out, opacity 200ms ease-in-out; + transition: + translate 250ms ease-in-out, + visibility 250ms ease-in-out, + opacity 250ms ease-in-out, + scale 250ms ease-in-out; > h1 { @apply backdrop-blur-xs sticky top-0 z-1 shrink-0 h-20 min-h-20; @@ -373,19 +382,27 @@ main { article { @apply w-full ml-0 pl-3 2xl:pl-4 pr-6 2xl:pr-8 rounded-l-none rounded-r-lg; + + &#calendar { + @apply pl-0; + } } /* when the calendar is expanded and aside is gone */ &.expanded { aside { - @apply -translate-x-6 invisible opacity-0; + @apply translate-x-8 invisible opacity-0 scale-y-95; } article { - @apply pl-6; + @apply pl-6 pr-6; margin-left: min(-16rem, -20dvw) !important; width: 100cqw !important; + + &#calendar { + @apply pl-6; + } } } } @@ -394,6 +411,13 @@ main { /* increase size of some things at 2xl */ @media (width >= theme(--breakpoint-2xl)) { /* 96rem */ + body#app main { + aside { + h1 { + @apply h-22; + } + } + } main { aside { > h1 { diff --git a/resources/css/lib/button.css b/resources/css/lib/button.css index 6d43ea4..68c8b6c 100644 --- a/resources/css/lib/button.css +++ b/resources/css/lib/button.css @@ -63,6 +63,10 @@ button, &.button--sm { @apply text-base px-2 h-8; + + > svg { + @apply w-4 h-4; + } } } diff --git a/resources/css/lib/calendar.css b/resources/css/lib/calendar.css index b7afa09..09f4b5a 100644 --- a/resources/css/lib/calendar.css +++ b/resources/css/lib/calendar.css @@ -3,7 +3,11 @@ **/ .calendar.month { @apply grid col-span-3 pb-6 2xl:pb-8 pt-2; - grid-template-rows: 2rem 1fr; + /*grid-template-rows: 2rem 1fr; */ + + /* force month container to fit window */ + grid-template-rows: 2rem calc(100% - 2rem); + overflow-y: hidden; hgroup { @apply grid grid-cols-7 w-full gap-1; @@ -17,12 +21,16 @@ @apply grid grid-cols-7 w-full gap-1; contain: paint; grid-auto-rows: 1fr; + /*max-height: var(--month-calendar-height); */ + /* day element */ li { - @apply relative px-1 pt-8 border-t-md border-gray-900; + @apply bg-white relative px-1 pt-8 border-t-md border-gray-900 overflow-y-auto; + /* day number */ &::before { - @apply absolute top-0 right-px w-auto h-8 flex items-center justify-end pr-4 text-sm font-medium; + @apply sticky top-0 -mt-8 -translate-y-8 right-0 w-auto h-8 z-1; + @apply flex items-center justify-end pr-3 text-sm font-medium bg-inherit; content: attr(data-day-number); } @@ -82,7 +90,7 @@ .calendar.time { @apply grid; grid-template-columns: 6rem auto; - grid-template-rows: 4.5rem auto 5rem; + grid-template-rows: 5rem auto 5rem; --row-height: 2.5rem; --now-row: 1; --now-col-start: 1; @@ -91,7 +99,7 @@ /* top day bar */ hgroup { @apply bg-white col-span-2 border-b-2 border-primary pl-24 sticky z-10; - top: 5.5rem; + @apply top-20; span.name { @apply font-semibold uppercase text-sm; @@ -107,11 +115,11 @@ } div.day-header { - @apply relative flex flex-col gap-2px justify-start items-start; + @apply relative flex flex-col gap-1 justify-end items-start pb-2; animation: header-slide 250ms ease-in; &:not(:last-of-type)::after { - @apply block w-px bg-gray-200 absolute -right-2 top-18; + @apply block w-px bg-gray-200 absolute -right-2 top-20; content: ''; height: calc(100dvh - 16rem); } @@ -158,13 +166,16 @@ --event-fg: var(--color-primary); li.event { - @apply flex rounded-md relative border border-white; + @apply flex rounded-md relative border border-white overflow-hidden; background-color: var(--event-bg); color: var(--event-fg); 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); + width: calc(100% - var(--event-overlap-offset, 100%)); + margin-left: var(--event-overlap-offset, 0%); + z-index: var(--event-z, 1); top: 0.6rem; a.event { @@ -175,12 +186,17 @@ } > time { - @apply text-xs; + @apply text-2xs font-medium whitespace-nowrap; } } &:hover { - animation: event-hover 125ms ease forwards; + /*animation: event-hover 125ms ease forwards;*/ + transform: translateY(-2px); + transition: + transform 125ms ease-in; + /*width: 100%;*/ + /*z-index: 20; /* enough to make sure it's always on top of other events */ } } } @@ -188,7 +204,7 @@ /* bottom controls */ footer { @apply bg-white flex items-center justify-between col-span-2 border-t-md border-primary; - @apply sticky bottom-0 pt-2 pb-8 z-10; + @apply sticky bottom-0 pt-2 pb-6 z-10; a.timezone { @apply text-xs bg-gray-100 rounded px-2 py-1; @@ -231,6 +247,9 @@ &.week { ol.events { + a.event { + @apply p-2; + } li.event[data-span="1"] { a.event > span, a.event > time { @@ -239,8 +258,11 @@ } li.event[data-span="1"], li.event[data-span="2"] { - > a.event { - @apply flex-row items-center gap-3; + a.event { + @apply flex-col gap-0 py-1; + > span { + @apply min-h-4 line-clamp-1; + } } } } @@ -290,7 +312,7 @@ background-repeat: no-repeat; } - &[data-weekstart="0"] { + &[data-weekstart="1"] { ol.events { background-image: linear-gradient( @@ -336,7 +358,7 @@ /** * calendar list in the left bar - **/ + */ #calendar-toggles { @apply pb-6; @@ -363,7 +385,7 @@ /* hide the edit link by default */ .edit-link { - @apply hidden absolute pl-4 right-0 top-1/2 -translate-y-1/2 underline text-sm; + @apply hidden absolute pl-4 right-1 top-1/2 -translate-y-1/2 underline text-sm; background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 33%); } @@ -381,9 +403,41 @@ } } +/** + * media queries + */ +@media (width >= theme(--breakpoint-2xl)) /* 96rem */ +{ + .calendar.time { + hgroup { + @apply top-22; + } + } +} +@media (height <= 50rem) +{ + .calendar.time { + grid-template-rows: 4rem auto 5rem; + + hgroup { + div.day-header { + @apply flex-row items-center justify-start gap-2; + + .name { + order: 2; + } + + &:not(:last-of-type)::after { + @apply top-16; + } + } + } + } +} + /** * animations - **/ + */ @keyframes event-slide { from { opacity: 0; @@ -401,7 +455,7 @@ } to { transform: translateY(-2px); - z-index: 2; + z-index: 10; } } @keyframes header-slide { diff --git a/resources/css/lib/event.css b/resources/css/lib/event.css new file mode 100644 index 0000000..d989530 --- /dev/null +++ b/resources/css/lib/event.css @@ -0,0 +1,6 @@ +.event-map { + @apply relative -translate-y-20 -mb-12 -ml-6 w-full bg-cover; + aspect-ratio: 2 / 1; + background-image: var(--event-map); + width: calc(100% + 3rem); +} diff --git a/resources/css/lib/modal.css b/resources/css/lib/modal.css index 446f357..855e880 100644 --- a/resources/css/lib/modal.css +++ b/resources/css/lib/modal.css @@ -28,7 +28,7 @@ dialog { box-shadow: 0 1.5rem 4rem -0.5rem rgba(0, 0, 0, 0.4); > .close-modal { - @apply block absolute top-4 right-4; + @apply block absolute top-4 right-4 z-3; } > .content { @@ -36,7 +36,7 @@ dialog { /* modal header */ header { - @apply px-6 py-6; + @apply relative flex items-center px-6 h-20 z-2; h2 { @apply pr-12; @@ -57,6 +57,13 @@ dialog { footer { @apply px-6 py-4 border-t-md border-gray-400 flex justify-between; } + + /* event modal with a map */ + &.with-map { + header { + background: linear-gradient(180deg,rgba(255, 255, 255, 0.67) 0%, rgba(255, 255, 255, 0) 100%); + } + } } } diff --git a/resources/js/app.js b/resources/js/app.js index 9d862f9..6692998 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -28,6 +28,40 @@ document.addEventListener('htmx:configRequest', (evt) => { if (token) evt.detail.headers['X-CSRF-TOKEN'] = token }) +/** + * global auth expiry redirect (fetch/axios) + */ +const AUTH_REDIRECT_STATUSES = new Set([401, 419]); +const redirectToLogin = () => { + if (window.location.pathname !== '/login') { + window.location.assign('/login'); + } +}; + +if (window.fetch) { + const originalFetch = window.fetch.bind(window); + window.fetch = async (...args) => { + const response = await originalFetch(...args); + if (response && AUTH_REDIRECT_STATUSES.has(response.status)) { + redirectToLogin(); + } + return response; + }; +} + +if (window.axios) { + window.axios.interceptors.response.use( + (response) => response, + (error) => { + const status = error?.response?.status; + if (AUTH_REDIRECT_STATUSES.has(status)) { + redirectToLogin(); + } + return Promise.reject(error); + } + ); +} + /** * calendar ui * progressive enhancement on html form with no js diff --git a/resources/views/components/calendar/four/four.blade.php b/resources/views/components/calendar/four/four.blade.php index a3f1924..4fe2743 100644 --- a/resources/views/components/calendar/four/four.blade.php +++ b/resources/views/components/calendar/four/four.blade.php @@ -23,9 +23,26 @@ >
@foreach ($hgroup as $h) + @php + $dayParams = [ + 'view' => 'day', + 'date' => $h['date'], + 'density' => $density['step'], + 'daytime_hours' => (int) ($daytime_hours['enabled'] ?? 0), + ]; + @endphp
$h['is_today'] ?? false])> {{ $h['dow'] }} - {{ $h['day'] }} + + {{ $h['day'] }} +
@endforeach
diff --git a/resources/views/components/calendar/time/event.blade.php b/resources/views/components/calendar/time/event.blade.php index fcfdd59..7236692 100644 --- a/resources/views/components/calendar/time/event.blade.php +++ b/resources/views/components/calendar/time/event.blade.php @@ -14,6 +14,11 @@ --event-col: {{ $event['start_col'] }}; --event-bg: {{ $event['color'] }}; --event-fg: {{ $event['color_fg'] }}; + --event-overlap-count: {{ $event['overlap_count'] ?? 1 }}; + --event-overlap-index: {{ $event['overlap_index'] ?? 0 }}; + --event-overlap-width: {{ $event['overlap_width'] ?? 100 }}%; + --event-overlap-offset: {{ $event['overlap_offset'] ?? 0 }}%; + --event-z: {{ $event['overlap_z'] ?? 1 }}; "> @php $showParams = [$event['calendar_slug'], $event['id']]; diff --git a/resources/views/components/calendar/week/week.blade.php b/resources/views/components/calendar/week/week.blade.php index db6b37b..7170c52 100644 --- a/resources/views/components/calendar/week/week.blade.php +++ b/resources/views/components/calendar/week/week.blade.php @@ -25,9 +25,26 @@ >
@foreach ($hgroup as $h) + @php + $dayParams = [ + 'view' => 'day', + 'date' => $h['date'], + 'density' => $density['step'], + 'daytime_hours' => (int) ($daytime_hours['enabled'] ?? 0), + ]; + @endphp
$h['is_today'] ?? false])> {{ $h['dow'] }} - {{ $h['day'] }} + + {{ $h['day'] }} +
@endforeach
diff --git a/resources/views/components/modal/body.blade.php b/resources/views/components/modal/body.blade.php index 11b28be..f57add4 100644 --- a/resources/views/components/modal/body.blade.php +++ b/resources/views/components/modal/body.blade.php @@ -1,3 +1,3 @@ -
+
class(['flex flex-col px-8 pb-6']) }}> {{ $slot }}
diff --git a/resources/views/components/modal/content.blade.php b/resources/views/components/modal/content.blade.php index 74e1e62..bfcf818 100644 --- a/resources/views/components/modal/content.blade.php +++ b/resources/views/components/modal/content.blade.php @@ -3,6 +3,6 @@ -
+
class('content') }}> {{ $slot }}
diff --git a/resources/views/components/modal/title.blade.php b/resources/views/components/modal/title.blade.php index dbf1f32..c40c360 100644 --- a/resources/views/components/modal/title.blade.php +++ b/resources/views/components/modal/title.blade.php @@ -2,8 +2,6 @@ 'border' => false, ]) -
$border])> -

- {{ $slot }} -

+
class(['header--with-border' => $border]) }}> + {{ $slot }}
diff --git a/resources/views/event/partials/details.blade.php b/resources/views/event/partials/details.blade.php index 97ebd99..59c8fbe 100644 --- a/resources/views/event/partials/details.blade.php +++ b/resources/views/event/partials/details.blade.php @@ -2,8 +2,7 @@ $meta = $event->meta; $title = $meta->title ?? '(no title)'; $allDay = (bool) ($meta->all_day ?? false); - $calendarName = $calendar->displayname ?? __('common.calendar'); - $calendarColor = $calendar->meta_color ?? $calendar->calendarcolor ?? default_calendar_color(); + $calendarName = $calendarName ?? $calendar->displayname ?? __('common.calendar'); $rrule = $meta?->extra['rrule'] ?? null; $tzid = $meta?->extra['tzid'] ?? $tz; $locationLabel = $meta?->location_label ?? ''; @@ -16,16 +15,23 @@ $venue?->postal, ]))); $addressLine3 = $venue?->country; + $map = $map ?? ['enabled' => false, 'needs_key' => false, 'url' => null]; @endphp - - -
- - {{ $title }} -
+ + + +

{{ $title }}

+ @if ($map['enabled']) +
+ +
+
+
+
+ @endif

{{ __('calendar.event.when') }}

@@ -74,9 +80,6 @@ @endif
@endif -
- {{ __('calendar.event.map_coming') }} -
@else

{{ __('calendar.event.no_location') }}

@endif