Event modals now change the url for proper back handling; improvements to geocoding existing locations with no lat and lon; normalizes time based view footer
This commit is contained in:
parent
ef658d1c04
commit
baeb291db5
@ -24,6 +24,10 @@ GEOCODER_COUNTRY=USA
|
|||||||
GEOCODER_CATEGORIES=POI,Address
|
GEOCODER_CATEGORIES=POI,Address
|
||||||
ARCGIS_API_KEY=
|
ARCGIS_API_KEY=
|
||||||
ARCGIS_STORE_RESULTS=true # set to false to not store results
|
ARCGIS_STORE_RESULTS=true # set to false to not store results
|
||||||
|
ARCGIS_DEBUG=false
|
||||||
|
ARCGIS_BASEMAP_STYLE=arcgis/community
|
||||||
|
ARCGIS_BASEMAP_ZOOM=16
|
||||||
|
GEOCODE_AFTER_MIGRATE=false
|
||||||
|
|
||||||
PHP_CLI_SERVER_WORKERS=4
|
PHP_CLI_SERVER_WORKERS=4
|
||||||
|
|
||||||
|
|||||||
@ -34,14 +34,97 @@ class GeocodeEventLocations implements ShouldQueue
|
|||||||
public function handle(Geocoder $geocoder): void
|
public function handle(Geocoder $geocoder): void
|
||||||
{
|
{
|
||||||
// working counters
|
// working counters
|
||||||
$processed = 0;
|
$stats = [
|
||||||
$created = 0;
|
'locations' => [
|
||||||
$updated = 0;
|
'processed' => 0,
|
||||||
$skipped = 0;
|
'updated' => 0,
|
||||||
$failed = 0;
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
],
|
||||||
|
'events' => [
|
||||||
|
'processed' => 0,
|
||||||
|
'created' => 0,
|
||||||
|
'updated' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$handled = 0;
|
||||||
|
|
||||||
Log::info('GeocodeEventLocations: start', ['limit' => $this->limit]);
|
Log::info('GeocodeEventLocations: start', ['limit' => $this->limit]);
|
||||||
|
|
||||||
|
// first, geocode any location rows missing coordinates
|
||||||
|
$locations = Location::query()
|
||||||
|
->whereNotNull('raw_address')
|
||||||
|
->where('raw_address', '<>', '')
|
||||||
|
->where(function ($q) {
|
||||||
|
$q->whereNull('lat')->orWhereNull('lon');
|
||||||
|
})
|
||||||
|
->orderBy('id');
|
||||||
|
|
||||||
|
$stop = false;
|
||||||
|
|
||||||
|
$locations->chunkById(200, function ($chunk) use ($geocoder, &$stats, &$handled, &$stop) {
|
||||||
|
foreach ($chunk as $loc) {
|
||||||
|
if ($stop) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->limit !== null && $handled >= $this->limit) {
|
||||||
|
$stop = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handled++;
|
||||||
|
$stats['locations']['processed']++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$norm = $geocoder->forward($loc->raw_address);
|
||||||
|
|
||||||
|
if (!$norm || !is_numeric($norm['lat'] ?? null) || !is_numeric($norm['lon'] ?? null)) {
|
||||||
|
$stats['locations']['skipped']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changed = false;
|
||||||
|
if ($loc->lat === null && is_numeric($norm['lat'])) {
|
||||||
|
$loc->lat = $norm['lat'];
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
if ($loc->lon === null && is_numeric($norm['lon'])) {
|
||||||
|
$loc->lon = $norm['lon'];
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['street', 'city', 'state', 'postal', 'country'] as $field) {
|
||||||
|
if (empty($loc->{$field}) && !empty($norm[$field])) {
|
||||||
|
$loc->{$field} = $norm[$field];
|
||||||
|
$changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($changed) {
|
||||||
|
$loc->save();
|
||||||
|
$stats['locations']['updated']++;
|
||||||
|
} else {
|
||||||
|
$stats['locations']['skipped']++;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$stats['locations']['failed']++;
|
||||||
|
Log::warning('GeocodeEventLocations: location failed', [
|
||||||
|
'location_id' => $loc->id,
|
||||||
|
'raw_address' => $loc->raw_address,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 'id');
|
||||||
|
|
||||||
|
if ($stop) {
|
||||||
|
Log::info('GeocodeEventLocations: done', $stats);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// events that have a non-empty location string but no linked location row yet
|
// events that have a non-empty location string but no linked location row yet
|
||||||
$todo = EventMeta::query()
|
$todo = EventMeta::query()
|
||||||
->whereNull('location_id')
|
->whereNull('location_id')
|
||||||
@ -49,46 +132,63 @@ class GeocodeEventLocations implements ShouldQueue
|
|||||||
->where('location', '<>', '')
|
->where('location', '<>', '')
|
||||||
->orderBy('event_id'); // important for chunkById
|
->orderBy('event_id'); // important for chunkById
|
||||||
|
|
||||||
$stop = false;
|
|
||||||
|
|
||||||
// log total to process (before limit)
|
// log total to process (before limit)
|
||||||
$total = (clone $todo)->count();
|
$total = (clone $todo)->count();
|
||||||
Log::info('[geo] starting GeocodeEventLocations', ['total' => $total, 'limit' => $this->limit]);
|
Log::info('[geo] starting GeocodeEventLocations', ['total' => $total, 'limit' => $this->limit]);
|
||||||
|
|
||||||
// chunk through event_meta rows
|
// chunk through event_meta rows
|
||||||
$todo->chunkById(200, function ($chunk) use ($geocoder, &$processed, &$created, &$updated, &$skipped, &$failed, &$stop) {
|
$todo->chunkById(200, function ($chunk) use ($geocoder, &$stats, &$handled, &$stop) {
|
||||||
foreach ($chunk as $meta) {
|
foreach ($chunk as $meta) {
|
||||||
if ($stop) {
|
if ($stop) {
|
||||||
return false; // stop further chunking
|
return false; // stop further chunking
|
||||||
}
|
}
|
||||||
|
|
||||||
// respect limit if provided
|
// respect limit if provided
|
||||||
if ($this->limit !== null && $processed >= $this->limit) {
|
if ($this->limit !== null && $handled >= $this->limit) {
|
||||||
$stop = true;
|
$stop = true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// geocode the free-form location string; if it looks like a label,
|
// geocode the free-form location string; prefer an existing location match
|
||||||
// fall back to the normalized location record's raw address
|
|
||||||
$query = $meta->location;
|
$query = $meta->location;
|
||||||
$norm = $geocoder->forward($query);
|
$location = Location::where('display_name', $meta->location)
|
||||||
|
->orWhere('raw_address', $meta->location)
|
||||||
|
->first();
|
||||||
|
|
||||||
if (!$norm) {
|
if (!$location) {
|
||||||
$location = Location::where('display_name', $meta->location)
|
// soft match on prefix when there is exactly one candidate
|
||||||
->orWhere('raw_address', $meta->location)
|
$matches = Location::where('display_name', 'like', $meta->location . '%')
|
||||||
->first();
|
->limit(2)
|
||||||
|
->get();
|
||||||
if ($location?->raw_address) {
|
if ($matches->count() === 1) {
|
||||||
$query = $location->raw_address;
|
$location = $matches->first();
|
||||||
$norm = $geocoder->forward($query);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($location) {
|
||||||
|
// if we already have coords, just link and move on
|
||||||
|
if (is_numeric($location->lat) && is_numeric($location->lon)) {
|
||||||
|
$meta->location_id = $location->id;
|
||||||
|
$meta->save();
|
||||||
|
$handled++;
|
||||||
|
$stats['events']['processed']++;
|
||||||
|
$stats['events']['updated']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($location->raw_address) {
|
||||||
|
$query = $location->raw_address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$norm = $geocoder->forward($query);
|
||||||
|
|
||||||
// skip obvious non-address labels or unresolved queries
|
// skip obvious non-address labels or unresolved queries
|
||||||
if (!$norm || (!$norm['lat'] && !$norm['street'])) {
|
if (!$norm || (!$norm['lat'] && !$norm['street'])) {
|
||||||
$skipped++;
|
$stats['events']['skipped']++;
|
||||||
$processed++;
|
$handled++;
|
||||||
|
$stats['events']['processed']++;
|
||||||
Log::info('GeocodeEventLocations: skipped', [
|
Log::info('GeocodeEventLocations: skipped', [
|
||||||
'event_id' => $meta->event_id,
|
'event_id' => $meta->event_id,
|
||||||
'location' => $meta->location,
|
'location' => $meta->location,
|
||||||
@ -133,22 +233,24 @@ class GeocodeEventLocations implements ShouldQueue
|
|||||||
$existing->lon = $norm['lon'];
|
$existing->lon = $norm['lon'];
|
||||||
$existing->raw_address ??= $norm['raw_address'];
|
$existing->raw_address ??= $norm['raw_address'];
|
||||||
$existing->save();
|
$existing->save();
|
||||||
$updated++;
|
$stats['events']['updated']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($loc->wasRecentlyCreated) {
|
if ($loc->wasRecentlyCreated) {
|
||||||
$created++;
|
$stats['events']['created']++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// link event_meta → locations
|
// link event_meta → locations
|
||||||
$meta->location_id = $loc->id;
|
$meta->location_id = $loc->id;
|
||||||
$meta->save();
|
$meta->save();
|
||||||
|
|
||||||
$processed++;
|
$handled++;
|
||||||
|
$stats['events']['processed']++;
|
||||||
|
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$failed++;
|
$stats['events']['failed']++;
|
||||||
$processed++;
|
$handled++;
|
||||||
|
$stats['events']['processed']++;
|
||||||
Log::warning('GeocodeEventLocations: failed', [
|
Log::warning('GeocodeEventLocations: failed', [
|
||||||
'event_id' => $meta->event_id,
|
'event_id' => $meta->event_id,
|
||||||
'location' => $meta->location,
|
'location' => $meta->location,
|
||||||
@ -158,6 +260,6 @@ class GeocodeEventLocations implements ShouldQueue
|
|||||||
}
|
}
|
||||||
}, 'event_id');
|
}, 'event_id');
|
||||||
|
|
||||||
Log::info('GeocodeEventLocations: done', compact('processed', 'created', 'updated', 'skipped', 'failed'));
|
Log::info('GeocodeEventLocations: done', $stats);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,7 @@ class Location extends Model
|
|||||||
$changed = true;
|
$changed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ($changed) {
|
if ($changed) {
|
||||||
$existing->save();
|
$existing->save();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,10 @@ namespace App\Providers;
|
|||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Illuminate\Support\Facades\Blade;
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Database\Events\MigrationsEnded;
|
||||||
|
use App\Jobs\GeocodeEventLocations;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@ -22,5 +26,26 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
{
|
{
|
||||||
// Calendar form
|
// Calendar form
|
||||||
Blade::component('calendars._form', 'calendar-form');
|
Blade::component('calendars._form', 'calendar-form');
|
||||||
|
|
||||||
|
if (app()->runningInConsole()) {
|
||||||
|
Event::listen(MigrationsEnded::class, function () {
|
||||||
|
if (!config('services.geocoding.after_migrate')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config('services.geocoding.arcgis.api_key')) {
|
||||||
|
Log::warning('Skipping geocode after migrate: missing ArcGIS API key');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
GeocodeEventLocations::runNow();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::warning('Geocode after migrate failed', [
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,6 +39,7 @@ return [
|
|||||||
"provider" => env("GEOCODER", "arcgis"),
|
"provider" => env("GEOCODER", "arcgis"),
|
||||||
"timeout" => (int) env("GEOCODER_TIMEOUT", 20),
|
"timeout" => (int) env("GEOCODER_TIMEOUT", 20),
|
||||||
"user_agent" => env("GEOCODER_USER_AGENT", "Kithkin/LocalDev"),
|
"user_agent" => env("GEOCODER_USER_AGENT", "Kithkin/LocalDev"),
|
||||||
|
"after_migrate" => (bool) env("GEOCODE_AFTER_MIGRATE", false),
|
||||||
"arcgis" => [
|
"arcgis" => [
|
||||||
"api_key" => env("ARCGIS_API_KEY"),
|
"api_key" => env("ARCGIS_API_KEY"),
|
||||||
"store" => (bool) env("ARCGIS_STORE_RESULTS", true),
|
"store" => (bool) env("ARCGIS_STORE_RESULTS", true),
|
||||||
|
|||||||
@ -78,7 +78,7 @@ button,
|
|||||||
> label,
|
> label,
|
||||||
> button {
|
> button {
|
||||||
@apply relative flex items-center justify-center h-full pl-3.5 pr-3 cursor-pointer;
|
@apply relative flex items-center justify-center h-full pl-3.5 pr-3 cursor-pointer;
|
||||||
@apply border-md border-primary border-l-0 font-medium rounded-none whitespace-nowrap;
|
@apply border-md border-primary border-l-0 text-base font-medium rounded-none whitespace-nowrap;
|
||||||
transition: outline 125ms ease-in-out;
|
transition: outline 125ms ease-in-out;
|
||||||
box-shadow: var(--shadows);
|
box-shadow: var(--shadows);
|
||||||
--shadows: none;
|
--shadows: none;
|
||||||
|
|||||||
@ -93,21 +93,6 @@
|
|||||||
&[data-event-visible="9"] {
|
&[data-event-visible="9"] {
|
||||||
.event:nth-child(n+10) { @apply hidden; }
|
.event:nth-child(n+10) { @apply hidden; }
|
||||||
}
|
}
|
||||||
&.is-expanded {
|
|
||||||
position: relative;
|
|
||||||
height: min-content;
|
|
||||||
padding-bottom: 1px;
|
|
||||||
z-index: 3;
|
|
||||||
border: 1.5px solid black;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
scale: 1.05;
|
|
||||||
width: 120%;
|
|
||||||
margin-left: -10%;
|
|
||||||
|
|
||||||
div.more-events {
|
|
||||||
@apply relative h-8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* events */
|
/* events */
|
||||||
.event {
|
.event {
|
||||||
@ -135,6 +120,28 @@
|
|||||||
@apply hidden;
|
@apply hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* expanded days with truncated events */
|
||||||
|
&.is-expanded {
|
||||||
|
position: relative;
|
||||||
|
height: min-content;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
z-index: 3;
|
||||||
|
border: 1.5px solid black;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
scale: 1.05;
|
||||||
|
width: 120%;
|
||||||
|
margin-left: -10%;
|
||||||
|
margin-bottom: -100%; /* needed to break out of row in webkit */
|
||||||
|
|
||||||
|
div.more-events {
|
||||||
|
@apply relative h-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ const SELECTORS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
* htmx/global
|
* htmx/global
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -33,8 +34,10 @@ document.addEventListener('htmx:configRequest', (evt) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
* global auth expiry redirect (fetch/axios)
|
* global auth expiry redirect (fetch/axios)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const AUTH_REDIRECT_STATUSES = new Set([401, 419]);
|
const AUTH_REDIRECT_STATUSES = new Set([401, 419]);
|
||||||
const redirectToLogin = () => {
|
const redirectToLogin = () => {
|
||||||
if (window.location.pathname !== '/login') {
|
if (window.location.pathname !== '/login') {
|
||||||
@ -67,9 +70,11 @@ if (window.axios) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* calendar ui
|
*
|
||||||
* progressive enhancement on html form with no js
|
* calendar ui improvements
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// progressive enhancement on html form with no JS
|
||||||
document.addEventListener('change', (event) => {
|
document.addEventListener('change', (event) => {
|
||||||
const target = event.target;
|
const target = event.target;
|
||||||
|
|
||||||
@ -90,9 +95,24 @@ document.addEventListener('change', (event) => {
|
|||||||
form.requestSubmit();
|
form.requestSubmit();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// close event modal on back/forward navigation
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
if (!document.querySelector('article#calendar')) return;
|
||||||
|
|
||||||
|
const dialog = document.querySelector('dialog');
|
||||||
|
if (!dialog?.open) return;
|
||||||
|
|
||||||
|
const modal = dialog.querySelector('#modal');
|
||||||
|
if (!modal?.querySelector('[data-modal-kind="event"]')) return;
|
||||||
|
|
||||||
|
dialog.close();
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
* calendar sidebar expand toggle
|
* calendar sidebar expand toggle
|
||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('click', (event) => {
|
document.addEventListener('click', (event) => {
|
||||||
const toggle = event.target.closest(SELECTORS.calendarExpandToggle);
|
const toggle = event.target.closest(SELECTORS.calendarExpandToggle);
|
||||||
if (!toggle) return;
|
if (!toggle) return;
|
||||||
@ -107,9 +127,11 @@ document.addEventListener('click', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
* color picker component
|
* color picker component
|
||||||
* native <input type="color"> + hex + random palette)
|
* native <input type="color"> + hex + random palette)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function initColorPickers(root = document) {
|
function initColorPickers(root = document) {
|
||||||
const isHex = (v) => /^#?[0-9a-fA-F]{6}$/.test((v || '').trim());
|
const isHex = (v) => /^#?[0-9a-fA-F]{6}$/.test((v || '').trim());
|
||||||
|
|
||||||
@ -198,8 +220,10 @@ function initColorPickers(root = document) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
* month view overflow handling (progressive enhancement)
|
* month view overflow handling (progressive enhancement)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function initMonthOverflow(root = document) {
|
function initMonthOverflow(root = document) {
|
||||||
const days = root.querySelectorAll(SELECTORS.monthDay);
|
const days = root.querySelectorAll(SELECTORS.monthDay);
|
||||||
days.forEach((day) => updateMonthOverflow(day));
|
days.forEach((day) => updateMonthOverflow(day));
|
||||||
@ -311,7 +335,28 @@ function updateMonthOverflow(dayEl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// show more events in a month calendar day when some are hidden
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
const button = event.target.closest(SELECTORS.monthDayMore);
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const dayEl = button.closest(SELECTORS.monthDay);
|
||||||
|
if (!dayEl) return;
|
||||||
|
|
||||||
|
dayEl.classList.toggle('is-expanded');
|
||||||
|
updateMonthOverflow(dayEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// month day resizer
|
||||||
|
let monthResizeTimer;
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
if (!document.querySelector(SELECTORS.monthDay)) return;
|
||||||
|
window.clearTimeout(monthResizeTimer);
|
||||||
|
monthResizeTimer = window.setTimeout(() => initMonthOverflow(), 100);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
*
|
||||||
* initialization
|
* initialization
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -328,21 +373,3 @@ document.addEventListener('htmx:afterSwap', (e) => {
|
|||||||
initColorPickers(e.target);
|
initColorPickers(e.target);
|
||||||
initMonthOverflow(e.target);
|
initMonthOverflow(e.target);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (event) => {
|
|
||||||
const button = event.target.closest(SELECTORS.monthDayMore);
|
|
||||||
if (!button) return;
|
|
||||||
|
|
||||||
const dayEl = button.closest(SELECTORS.monthDay);
|
|
||||||
if (!dayEl) return;
|
|
||||||
|
|
||||||
dayEl.classList.toggle('is-expanded');
|
|
||||||
updateMonthOverflow(dayEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
let monthResizeTimer;
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
if (!document.querySelector(SELECTORS.monthDay)) return;
|
|
||||||
window.clearTimeout(monthResizeTimer);
|
|
||||||
monthResizeTimer = window.setTimeout(() => initMonthOverflow(), 100);
|
|
||||||
});
|
|
||||||
|
|||||||
@ -95,11 +95,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
<menu>
|
<menu>
|
||||||
<li>
|
|
||||||
<a class="button button--icon" href="{{ route('calendar.settings') }}">
|
|
||||||
<x-icon-settings />
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<form id="calendar-nav"
|
<form id="calendar-nav"
|
||||||
action="{{ route('calendar.index') }}"
|
action="{{ route('calendar.index') }}"
|
||||||
@ -177,9 +172,13 @@
|
|||||||
</x-button.group-input>
|
</x-button.group-input>
|
||||||
<noscript><button type="submit" class="button">Apply</button></noscript>
|
<noscript><button type="submit" class="button">Apply</button></noscript>
|
||||||
</form>
|
</form>
|
||||||
<li>
|
</li>
|
||||||
<a class="button button--primary" href="{{ route('calendar.create') }}">
|
<li class="gap-0 flex flex-row">
|
||||||
<x-icon-plus-circle /> Create
|
<a class="button button--icon" href="{{ route('calendar.settings') }}">
|
||||||
|
<x-icon-settings />
|
||||||
|
</a>
|
||||||
|
<a class="button button--icon" href="{{ route('calendar.create') }}">
|
||||||
|
<x-icon-plus-circle />
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</menu>
|
</menu>
|
||||||
@ -216,6 +215,7 @@
|
|||||||
:active="$active"
|
:active="$active"
|
||||||
:density="$density"
|
:density="$density"
|
||||||
:daytime_hours="$daytime_hours"
|
:daytime_hours="$daytime_hours"
|
||||||
|
:timezone="$timezone"
|
||||||
:now="$now"
|
:now="$now"
|
||||||
/>
|
/>
|
||||||
@break
|
@break
|
||||||
@ -231,6 +231,7 @@
|
|||||||
:active="$active"
|
:active="$active"
|
||||||
:density="$density"
|
:density="$density"
|
||||||
:daytime_hours="$daytime_hours"
|
:daytime_hours="$daytime_hours"
|
||||||
|
:timezone="$timezone"
|
||||||
:now="$now"
|
:now="$now"
|
||||||
/>
|
/>
|
||||||
@break
|
@break
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
'density' => '30',
|
'density' => '30',
|
||||||
'now' => [],
|
'now' => [],
|
||||||
'daytime_hours' => [],
|
'daytime_hours' => [],
|
||||||
|
'timezone' => 'UTC',
|
||||||
])
|
])
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@ -40,7 +41,12 @@
|
|||||||
@endif
|
@endif
|
||||||
</ol>
|
</ol>
|
||||||
<footer>
|
<footer>
|
||||||
<x-calendar.time.daytime-hours view="day" :density="$density" :daytime_hours="$daytime_hours" />
|
<div class="left">
|
||||||
<x-calendar.time.density view="day" :density="$density" :daytime_hours="$daytime_hours" />
|
<a href="{{ route('account.locale') }}" class="timezone">{{ $timezone }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<x-calendar.time.daytime-hours view="day" :density="$density" :daytime_hours="$daytime_hours" />
|
||||||
|
<x-calendar.time.density view="day" :density="$density" :daytime_hours="$daytime_hours" />
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
'density' => '30',
|
'density' => '30',
|
||||||
'now' => [],
|
'now' => [],
|
||||||
'daytime_hours' => [],
|
'daytime_hours' => [],
|
||||||
|
'timezone' => 'UTC',
|
||||||
])
|
])
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@ -62,7 +63,12 @@
|
|||||||
@endif
|
@endif
|
||||||
</ol>
|
</ol>
|
||||||
<footer>
|
<footer>
|
||||||
<x-calendar.time.daytime-hours view="four" :density="$density" :daytime_hours="$daytime_hours" />
|
<div class="left">
|
||||||
<x-calendar.time.density view="four" :density="$density" :daytime_hours="$daytime_hours" />
|
<a href="{{ route('account.locale') }}" class="timezone">{{ $timezone }}</a>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<x-calendar.time.daytime-hours view="four" :density="$density" :daytime_hours="$daytime_hours" />
|
||||||
|
<x-calendar.time.density view="four" :density="$density" :daytime_hours="$daytime_hours" />
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
href="{{ route('calendar.event.show', $showParams) }}"
|
href="{{ route('calendar.event.show', $showParams) }}"
|
||||||
hx-get="{{ route('calendar.event.show', $showParams) }}"
|
hx-get="{{ route('calendar.event.show', $showParams) }}"
|
||||||
hx-target="#modal"
|
hx-target="#modal"
|
||||||
hx-push-url="false"
|
hx-push-url="true"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
style="--event-color: {{ $color }}"
|
style="--event-color: {{ $color }}"
|
||||||
data-calendar="{{ $event['calendar_slug'] }}"
|
data-calendar="{{ $event['calendar_slug'] }}"
|
||||||
|
|||||||
@ -26,13 +26,13 @@
|
|||||||
$showParams['occurrence'] = $event['occurrence'];
|
$showParams['occurrence'] = $event['occurrence'];
|
||||||
}
|
}
|
||||||
@endphp
|
@endphp
|
||||||
<a class="event{{ $event['visible'] ? '' : ' hidden' }}"
|
<a class="event{{ $event['visible'] ? '' : ' hidden' }}"
|
||||||
href="{{ route('calendar.event.show', $showParams) }}"
|
href="{{ route('calendar.event.show', $showParams) }}"
|
||||||
hx-get="{{ route('calendar.event.show', $showParams) }}"
|
hx-get="{{ route('calendar.event.show', $showParams) }}"
|
||||||
hx-target="#modal"
|
hx-target="#modal"
|
||||||
hx-push-url="false"
|
hx-push-url="true"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
data-calendar="{{ $event['calendar_slug'] }}"
|
data-calendar="{{ $event['calendar_slug'] }}"
|
||||||
>
|
>
|
||||||
<span>{{ $event['title'] }}</span>
|
<span>{{ $event['title'] }}</span>
|
||||||
<time datetime="{{ $event['start'] }}">{{ $event['start_ui'] }} - {{ $event['end_ui'] }}</time>
|
<time datetime="{{ $event['start'] }}">{{ $event['start_ui'] }} - {{ $event['end_ui'] }}</time>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
<dialog
|
<dialog
|
||||||
hx-on:click="if(event.target === this) this.close()"
|
hx-on:click="if(event.target === this) this.close()"
|
||||||
hx-on:close="document.getElementById('modal').innerHTML=''"
|
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-on::after-swap="this.closest('dialog')?.showModal()"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
$map = $map ?? ['enabled' => false, 'needs_key' => false, 'url' => null];
|
$map = $map ?? ['enabled' => false, 'needs_key' => false, 'url' => null];
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<x-modal.content :class="$map['enabled'] ? 'with-map' : null">
|
<x-modal.content data-modal-kind="event" :class="$map['enabled'] ? 'with-map' : null">
|
||||||
<x-modal.title class="gap-4">
|
<x-modal.title class="gap-4">
|
||||||
<span class="inline-block h-4 w-4 rounded-full" style="background: {{ $event->color }};"></span>
|
<span class="inline-block h-4 w-4 rounded-full" style="background: {{ $event->color }};"></span>
|
||||||
<h2>{{ $title }}</h2>
|
<h2>{{ $title }}</h2>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user