resetPage(); } /** * Updated hook for reactive properties */ public function updated($property): void { if (in_array($property, [ 'search', 'operatorId', 'region', 'country', 'parkType', 'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea', 'minRides', 'maxDistance', 'sortBy', 'sortDirection' ])) { $this->resetPage(); $this->invalidateCache('parks'); } } /** * Enable location services */ public function enableLocation(): void { $this->locationLoading = true; $this->dispatch('request-location'); } /** * Handle location received from JavaScript */ public function locationReceived(array $location): void { $this->userLocation = $location; $this->locationEnabled = true; $this->locationLoading = false; $this->resetPage(); $this->invalidateCache('parks'); } /** * Handle location error */ public function locationError(string $error): void { $this->locationLoading = false; $this->dispatch('location-error', message: $error); } /** * Clear all filters */ public function clearFilters(): void { $this->reset([ 'search', 'operatorId', 'region', 'country', 'parkType', 'openingYearFrom', 'openingYearTo', 'minArea', 'maxArea', 'minRides', 'maxDistance' ]); $this->userLocation = null; $this->locationEnabled = false; $this->resetPage(); $this->invalidateCache('parks'); } /** * Toggle filters visibility */ public function toggleFilters(): void { $this->showFilters = !$this->showFilters; } /** * Sort parks by given field */ public function sortBy(string $field): void { if ($this->sortBy === $field) { $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { $this->sortBy = $field; $this->sortDirection = 'asc'; } $this->resetPage(); $this->invalidateCache('parks'); } /** * Get parks with location-aware search and filtering */ public function getParksProperty() { return $this->remember('parks', function () { return $this->buildParksQuery() ->paginate(12, ['*'], 'page', $this->getPage()); }, 1200); // 20-minute location-aware caching } /** * Build the parks query with Django parity search and filtering */ protected function buildParksQuery(): Builder { $query = Park::query(); // Location-aware search functionality (Django parity) if (!empty($this->search)) { $query = $this->locationAwareSearch($query, $this->search, $this->userLocation); } // Apply advanced filters with geographic context $query = $this->applyFilters($query, [ 'operator_id' => $this->operatorId, 'region' => $this->region, 'country' => $this->country, 'park_type' => $this->parkType, 'opening_year_from' => $this->openingYearFrom, 'opening_year_to' => $this->openingYearTo, 'min_area' => $this->minArea, 'max_area' => $this->maxArea, 'min_rides' => $this->minRides, 'max_distance' => $this->maxDistance, ], $this->userLocation); // Apply sorting with location-aware options $query = $this->applySorting($query); // Eager load relationships for performance $query->with(['location', 'operator', 'photos', 'statistics']) ->withCount(['rides', 'reviews']); return $query; } /** * Location-aware search functionality matching Django implementation */ protected function locationAwareSearch(Builder $query, string $searchQuery, ?array $userLocation = null): Builder { $terms = explode(' ', trim($searchQuery)); foreach ($terms as $term) { $term = trim($term); if (empty($term)) continue; $query->where(function ($subQuery) use ($term) { $subQuery->where('name', 'ilike', "%{$term}%") ->orWhere('description', 'ilike', "%{$term}%") ->orWhere('park_type', 'ilike', "%{$term}%") ->orWhereHas('location', function($locQuery) use ($term) { $locQuery->where('city', 'ilike', "%{$term}%") ->orWhere('state', 'ilike', "%{$term}%") ->orWhere('country', 'ilike', "%{$term}%"); }) ->orWhereHas('operator', fn($opQuery) => $opQuery->where('name', 'ilike', "%{$term}%")); }); } // Add distance-based ordering for location-aware results if ($userLocation) { $query->selectRaw('parks.*, (6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) * cos(radians(locations.longitude) - radians(?)) + sin(radians(?)) * sin(radians(locations.latitude)))) AS distance', [$userLocation['lat'], $userLocation['lng'], $userLocation['lat']]) ->join('locations', 'parks.location_id', '=', 'locations.id') ->orderBy('distance'); } return $query; } /** * Apply advanced filters with geographic context */ protected function applyFilters(Builder $query, array $filters, ?array $userLocation = null): Builder { return $query ->when($filters['operator_id'] ?? null, fn($q, $operatorId) => $q->where('operator_id', $operatorId)) ->when($filters['region'] ?? null, fn($q, $region) => $q->whereHas('location', fn($locQ) => $locQ->where('state', $region))) ->when($filters['country'] ?? null, fn($q, $country) => $q->whereHas('location', fn($locQ) => $locQ->where('country', $country))) ->when($filters['park_type'] ?? null, fn($q, $type) => $q->where('park_type', $type)) ->when($filters['opening_year_from'] ?? null, fn($q, $year) => $q->where('opening_date', '>=', "{$year}-01-01")) ->when($filters['opening_year_to'] ?? null, fn($q, $year) => $q->where('opening_date', '<=', "{$year}-12-31")) ->when($filters['min_area'] ?? null, fn($q, $area) => $q->where('area_acres', '>=', $area)) ->when($filters['max_area'] ?? null, fn($q, $area) => $q->where('area_acres', '<=', $area)) ->when($filters['min_rides'] ?? null, fn($q, $count) => $q->whereHas('rides', fn($rideQ) => $rideQ->havingRaw('COUNT(*) >= ?', [$count]))) ->when($filters['max_distance'] ?? null && $userLocation, function($q) use ($filters, $userLocation) { $q->whereRaw('(6371 * acos(cos(radians(?)) * cos(radians(locations.latitude)) * cos(radians(locations.longitude) - radians(?)) + sin(radians(?)) * sin(radians(locations.latitude)))) <= ?', [$userLocation['lat'], $userLocation['lng'], $userLocation['lat'], $filters['max_distance']]); }); } /** * Apply sorting with location-aware options */ protected function applySorting(Builder $query): Builder { switch ($this->sortBy) { case 'distance': if ($this->userLocation) { // Distance sorting already applied in locationAwareSearch return $query; } // Fallback to name if no location return $query->orderBy('name', $this->sortDirection); case 'rides_count': return $query->orderBy('rides_count', $this->sortDirection); case 'reviews_count': return $query->orderBy('reviews_count', $this->sortDirection); case 'opening_date': return $query->orderBy('opening_date', $this->sortDirection); case 'area_acres': return $query->orderBy('area_acres', $this->sortDirection); case 'name': default: return $query->orderBy('name', $this->sortDirection); } } /** * Get available operators for filter dropdown */ public function getOperatorsProperty() { return $this->remember('operators', function () { return Operator::select('id', 'name') ->whereHas('parks') ->orderBy('name') ->get(); }, 3600); } /** * Get available regions for filter dropdown */ public function getRegionsProperty() { return $this->remember('regions', function () { return DB::table('locations') ->join('parks', 'locations.id', '=', 'parks.location_id') ->select('locations.state') ->distinct() ->whereNotNull('locations.state') ->orderBy('locations.state') ->pluck('state'); }, 3600); } /** * Get available countries for filter dropdown */ public function getCountriesProperty() { return $this->remember('countries', function () { return DB::table('locations') ->join('parks', 'locations.id', '=', 'parks.location_id') ->select('locations.country') ->distinct() ->whereNotNull('locations.country') ->orderBy('locations.country') ->pluck('country'); }, 3600); } /** * Get available park types for filter dropdown */ public function getParkTypesProperty() { return $this->remember('park_types', function () { return Park::select('park_type') ->distinct() ->whereNotNull('park_type') ->orderBy('park_type') ->pluck('park_type'); }, 3600); } /** * Get filter summary for display */ public function getActiveFiltersProperty(): array { $filters = []; if (!empty($this->search)) { $filters[] = "Search: {$this->search}"; } if (!empty($this->operatorId)) { $operator = $this->operators->find($this->operatorId); if ($operator) { $filters[] = "Operator: {$operator->name}"; } } if (!empty($this->region)) { $filters[] = "Region: {$this->region}"; } if (!empty($this->country)) { $filters[] = "Country: {$this->country}"; } if (!empty($this->parkType)) { $filters[] = "Type: {$this->parkType}"; } if (!empty($this->openingYearFrom) || !empty($this->openingYearTo)) { $yearRange = $this->openingYearFrom . ' - ' . $this->openingYearTo; $filters[] = "Years: {$yearRange}"; } if (!empty($this->minArea) || !empty($this->maxArea)) { $areaRange = $this->minArea . ' - ' . $this->maxArea . ' acres'; $filters[] = "Area: {$areaRange}"; } if (!empty($this->minRides)) { $filters[] = "Min Rides: {$this->minRides}"; } if (!empty($this->maxDistance) && $this->locationEnabled) { $filters[] = "Within: {$this->maxDistance} km"; } return $filters; } /** * Render the component */ public function render() { $this->isLoading = false; return view('livewire.parks-listing', [ 'parks' => $this->parks, 'operators' => $this->operators, 'regions' => $this->regions, 'countries' => $this->countries, 'parkTypes' => $this->parkTypes, 'activeFilters' => $this->activeFilters, ]); } /** * Get cache key for this component */ protected function getCacheKey(string $suffix = ''): string { $locationKey = $this->userLocation ? md5(json_encode($this->userLocation)) : 'no-location'; $filterKey = md5(serialize([ $this->search, $this->operatorId, $this->region, $this->country, $this->parkType, $this->openingYearFrom, $this->openingYearTo, $this->minArea, $this->maxArea, $this->minRides, $this->maxDistance, $this->sortBy, $this->sortDirection ])); return 'thrillwiki.' . class_basename(static::class) . '.' . $locationKey . '.' . $filterKey . '.' . $suffix; } /** * Remember data with caching */ protected function remember(string $key, $callback, int $ttl = 3600) { return Cache::remember($this->getCacheKey($key), $ttl, $callback); } /** * Invalidate component cache */ protected function invalidateCache(string $key = null): void { if ($key) { Cache::forget($this->getCacheKey($key)); } else { // Clear all cache for this component Cache::flush(); } } }