Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions app/Livewire/FossilMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,25 @@ public function onFiltersApplied(

$this->dispatch('occurrences-loaded', occurrences: $occurrences);
} catch (ApiException $e) {
$this->loadError = $e->getMessage();
$this->dispatch('occurrences-error', message: $e->getMessage());
$this->loadError = 'The search could not be completed. Please try again or adjust your filters.';
$this->dispatch('occurrences-error', message: $this->loadError);
}
}

/**
* Triggered when OccurrenceFilters resets all filter state.
* Clears map markers and restores the initial empty-state overlay.
*/
#[On('filters-reset')]
public function onFiltersReset(): void
{
$this->filtersApplied = false;
$this->resultCount = 0;
$this->resultTotal = 0;
$this->loadError = null;
$this->dispatch('occurrences-loaded', occurrences: []);
}

public function render(): View
{
return view('livewire.fossil-map', [
Expand Down
52 changes: 43 additions & 9 deletions app/Livewire/OccurrenceFilters.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ class OccurrenceFilters extends Component
*/
public bool $customTaxon = false;

/**
* When true, shows min/max Ma inputs instead of the named interval select.
*/
public bool $customInterval = false;

/** Validation message shown near the Apply button when filters are incomplete. */
public ?string $validationError = null;

/**
* All PBDB envtype values available for filtering.
*
Expand Down Expand Up @@ -152,6 +160,7 @@ public static function countryOptions(): array
*/
public function updated(string $name): void
{
$this->validationError = null;
$this->dispatch('filtersChanged');
}

Expand All @@ -176,6 +185,7 @@ public function loadPreset(string $id): void
$this->envTypes = $preset->envTypes;
$this->countryCodes = $preset->countryCodes ?? '';
$this->customTaxon = false;
$this->customInterval = $preset->minMa !== null || $preset->maxMa !== null;

$this->applyFilters();
}
Expand All @@ -198,6 +208,25 @@ public function disableCustomTaxon(): void
$this->baseName = '';
}

/**
* Switch the time period field to custom age range inputs and clear the interval.
*/
public function enableCustomInterval(): void
{
$this->customInterval = true;
$this->interval = '';
}

/**
* Switch the time period field back to the named interval select and reset age range.
*/
public function disableCustomInterval(): void
{
$this->customInterval = false;
$this->minMa = 0;
$this->maxMa = 540;
}

/**
* Construct an OccurrenceQuery from the current filter state.
*/
Expand All @@ -224,6 +253,14 @@ public function buildQuery(): OccurrenceQuery
*/
public function applyFilters(): void
{
if (! $this->hasFilters()) {
$this->validationError = 'Please select an organism or time period to search.';

return;
}

$this->validationError = null;

$this->dispatch(
'apply-filters',
baseName: $this->baseName !== '' ? $this->baseName : null,
Expand Down Expand Up @@ -257,6 +294,8 @@ public function resetFilters(): void
$this->latMin = null;
$this->latMax = null;
$this->customTaxon = false;
$this->customInterval = false;
$this->validationError = null;
$this->dispatch('filters-reset');
}

Expand Down Expand Up @@ -284,21 +323,16 @@ public function setBoundingBox(float $lngMin, float $lngMax, float $latMin, floa
}

/**
* Returns true when at least one filter differs from its default.
* Returns true when a primary filter (organism or time period) is set.
* Environment, country, and ID quality are refiners and cannot stand alone —
* PBDB requires at least one taxon or temporal parameter.
*/
public function hasFilters(): bool
{
return $this->baseName !== ''
|| $this->interval !== ''
|| $this->minMa > 0
|| $this->maxMa < 540
|| $this->envTypes !== []
|| $this->countryCodes !== ''
|| $this->idQual !== 'any'
|| $this->lngMin !== null
|| $this->lngMax !== null
|| $this->latMin !== null
|| $this->latMax !== null;
|| $this->maxMa < 540;
}

public function render(): View
Expand Down
38 changes: 0 additions & 38 deletions resources/js/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import 'leaflet/dist/leaflet.css';
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
import 'leaflet.markercluster';
import 'leaflet-draw/dist/leaflet.draw.css';
import 'leaflet-draw';
import 'leaflet.heat';
// Vite bundles assets differently to webpack — provide explicit URLs for
// Leaflet's default marker icons so they resolve correctly at runtime.
Expand Down Expand Up @@ -50,7 +48,6 @@ function fossilMap() {
map: null,
markerClusterGroup: null,
heatLayer: null,
drawControl: null,
tileLayers: {},
currentOccurrences: [],
heatmapMode: false,
Expand All @@ -72,41 +69,6 @@ function fossilMap() {
// Marker cluster group
this.markerClusterGroup = L.markerClusterGroup();
this.map.addLayer(this.markerClusterGroup);

// Feature group to hold drawn shapes
const drawnItems = new L.FeatureGroup();
this.map.addLayer(drawnItems);

// Draw control — rectangle only
this.drawControl = new L.Control.Draw({
draw: {
rectangle: true,
polyline: false,
polygon: false,
circle: false,
marker: false,
circlemarker: false,
},
edit: { featureGroup: drawnItems },
});
this.map.addControl(this.drawControl);

// Push drawn rectangle bounds into the OccurrenceFilters Livewire
// component via a global Livewire event — OccurrenceFilters listens
// for 'bbox-set' with #[On('bbox-set')].
this.map.on(L.Draw.Event.CREATED, (event) => {
drawnItems.clearLayers();
drawnItems.addLayer(event.layer);

const bounds = event.layer.getBounds();
window.Livewire.dispatch('bbox-set', {
lngMin: bounds.getWest(),
lngMax: bounds.getEast(),
latMin: bounds.getSouth(),
latMax: bounds.getNorth(),
});
});

},

/**
Expand Down
28 changes: 28 additions & 0 deletions resources/views/components/form/mode-toggle.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@props([
'isCustom' => false,
'defaultLabel' => 'Select',
'customLabel' => 'Custom',
'onDefault' => '',
'onCustom' => '',
])

<div class="mt-1.5 flex overflow-hidden rounded-md border border-border text-xs">
<button
type="button"
wire:click="{{ $onDefault }}"
@class([
'flex-1 px-2.5 py-1.5 font-medium transition-colors',
'bg-accent text-on-accent' => ! $isCustom,
'bg-surface text-muted hover:bg-surface-hover' => $isCustom,
])
>{{ $defaultLabel }}</button>
<button
type="button"
wire:click="{{ $onCustom }}"
@class([
'flex-1 border-l border-border px-2.5 py-1.5 font-medium transition-colors',
'bg-accent text-on-accent' => $isCustom,
'bg-surface text-muted hover:bg-surface-hover' => ! $isCustom,
])
>{{ $customLabel }}</button>
</div>
2 changes: 1 addition & 1 deletion resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-bg text-text antialiased">
<nav class="border-b border-border bg-surface" aria-label="Main navigation">
<nav class="relative z-[1100] border-b border-border bg-surface" aria-label="Main navigation">
<div class="flex h-16 items-center justify-between px-4 sm:px-6">
<a href="/" class="text-lg font-semibold tracking-tight text-text">
Eonmap
Expand Down
31 changes: 30 additions & 1 deletion resources/views/livewire/fossil-map.blade.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="grid grid-cols-[18rem_1fr] h-[calc(100vh-4rem)]">

{{-- ─── Filter Panel ──────────────────────────────────────────────── --}}
<aside class="flex flex-col bg-surface border-r border-border overflow-hidden" aria-label="Filters">
<aside class="relative flex flex-col bg-surface border-r border-border overflow-hidden" aria-label="Filters">
<livewire:occurrence-filters />

{{-- Error state (set by FossilMap when the API call fails) --}}
Expand All @@ -10,6 +10,20 @@
{{ $loadError }}
</div>
@endif

{{-- Loading overlay — covers the filter panel while the API call is in flight --}}
<div
wire:loading
class="absolute inset-0 z-10 bg-surface/80 backdrop-blur-sm"
>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-3">
<svg class="h-7 w-7 animate-spin text-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
</svg>
<p class="text-sm font-medium text-muted">Searching&hellip;</p>
</div>
</div>
</aside>

{{-- ─── Map Container ─────────────────────────────────────────────── --}}
Expand Down Expand Up @@ -41,6 +55,17 @@ class="absolute inset-0 flex items-center justify-center pointer-events-none z-[
</div>
</div>

{{-- Zero results overlay — shown when a search completes with no matches --}}
<div
x-show="$wire.filtersApplied && $wire.resultCount === 0 && !$wire.loadError"
class="absolute inset-0 flex items-center justify-center pointer-events-none z-[500]"
>
<div class="bg-surface/90 backdrop-blur-sm rounded-xl border border-border px-8 py-6 text-center shadow-lg">
<p class="text-lg font-semibold text-text">No occurrences found</p>
<p class="mt-1 text-sm text-muted">Try a different organism, time period, or remove some filters.</p>
</div>
</div>

{{-- Result count indicator — shown after filters are applied --}}
<div
x-show="$wire.filtersApplied && $wire.resultCount > 0"
Expand All @@ -52,6 +77,10 @@ class="absolute bottom-8 right-3 z-[1000]"
? 'Showing ' + $wire.resultCount.toLocaleString() + ' of ' + $wire.resultTotal.toLocaleString() + ' occurrences'
: $wire.resultCount.toLocaleString() + ' ' + ($wire.resultCount === 1 ? 'occurrence' : 'occurrences')
"></span>
<p
x-show="$wire.resultCount >= 500"
class="mt-0.5 text-muted/70"
>This application limits results to 500 occurrences at a time.</p>
</div>
</div>

Expand Down
Loading
Loading