@extends('layouts.public') @section('content') @php /** @var array $restaurants @var array $cuisines @var array $tags @var array $filters @var string $view @var string $diet @var bool $openNow */ $tags = $tags ?? []; $view = in_array($view ?? 'grid', ['list', 'map'], true) ? $view : 'grid'; $diet = $diet ?? ''; $openNow = $openNow ?? false; $priceMax = (float) ($filters['price_max'] ?? 0); // Geo (F2/S6/S7/S8), all resolved by the controller. $origin is null when nothing // located the visitor — a radius measured from nowhere is not a filter, so the whole // distance control stays off the page rather than sitting there doing nothing. $radius = (float) ($radius ?? 0); $radiusMin = (float) ($radiusMin ?? 1); $radiusMax = (float) ($radiusMax ?? 25); $origin = $origin ?? null; // True when a radius was asked for and nothing could be measured from — see the note by the slider. $radiusIgnored = (bool) ($radiusIgnored ?? false); $mapAvailable = (bool) ($mapAvailable ?? false); $mapPoints = $mapPoints ?? []; $mapCapped = (bool) ($mapCapped ?? false); $mapMaxPins = (int) ($mapMaxPins ?? 500); $mapCluster = (bool) ($mapCluster ?? true); $geoBrowser = (bool) ($geoBrowser ?? false); $distUnit = \App\Services\Distance::unitLabel(); // The pin only travels in the query string when the browser is the thing that found it. $pinLat = $geoBrowser && is_numeric(request()->query('lat')) ? (string) request()->query('lat') : ''; $pinLng = $geoBrowser && is_numeric(request()->query('lng')) ? (string) request()->query('lng') : ''; // $restaurants holds only the current page; the total (across all pages) drives the counts. $total = (int) ($pager['total'] ?? count($restaurants)); // Preserve current query params (except view) when building toggle links. $baseQuery = request()->query(); unset($baseQuery['view']); $gridHref = route('restaurants') . '?' . http_build_query(array_merge($baseQuery, ['view' => 'grid'])); $listHref = route('restaurants') . '?' . http_build_query(array_merge($baseQuery, ['view' => 'list'])); $mapHref = route('restaurants') . '?' . http_build_query(array_merge($baseQuery, ['view' => 'map'])); $dietLabels = [ '004-leaf' => t_raw('common.diet_vegetarian_options'), '006-chili' => t_raw('common.diet_spicy_dishes'), '005-chef' => t_raw('common.diet_chefs_picks'), '008-protein' => t_raw('common.diet_egg_dishes'), '009-lemon' => t_raw('common.diet_zesty_dishes'), ]; // ----- Active-filter chips: build a removable chip per applied filter. Each chip // links back to this same page with just that one parameter dropped, so a buyer // can clear filters individually instead of only "Reset all". $cuisineLabels = []; foreach ($cuisines as $c) { $cuisineLabels[$c['slug']] = $c['name']; } $tagLabels = []; foreach ($tags as $t) { $tagLabels[$t['slug']] = $t['name']; } $dietChipLabels = ['veg' => t_raw('restaurant.diet_veg'), 'vegan' => t_raw('restaurant.diet_vegan'), 'protein' => t_raw('restaurant.diet_protein'), 'light' => t_raw('restaurant.diet_light')]; $sortChipLabels = ['rating' => t_raw('restaurant.sort_top_rated'), 'delivery' => t_raw('restaurant.sort_fastest_delivery'), 'price' => t_raw('restaurant.sort_lowest_min_order'), 'newest' => t_raw('restaurant.sort_newest')]; // A chip's link is the CURRENT query string minus one parameter — a plain link, so // removing a filter needs no JavaScript. Two parameters are deliberately handled // differently from the rest: // `view` is kept. It is how the visitor is looking at the results, not something // they filtered by, and dropping a chip must not throw them out of the map. // `page` is dropped. A narrower or wider result set has a different last page, and // page 7 of a 3-page result set is an empty screen that reads as "no results". $chipQuery = request()->query(); unset($chipQuery['page']); // The view that actually rendered, not the one that was asked for. An install with no // map configured downgrades `view=map` to the grid, and a chip must not hand back a // parameter the page just refused. if ($view === 'grid') { unset($chipQuery['view']); } else { $chipQuery['view'] = $view; } $chips = []; $chipFor = static function (string $param, string $label) use ($chipQuery, $__env): array { $q = $chipQuery; unset($q[$param]); return ['label' => $label, 'href' => route('restaurants') . ($q ? '?' . http_build_query($q) : '')]; }; if (($filters['q'] ?? '') !== '') { $chips[] = $chipFor('q', t_raw('restaurant.chip_search', [':q' => $filters['q']])); } if (($filters['city'] ?? '') !== '') { $chips[] = $chipFor('city', t_raw('restaurant.chip_city', [':city' => $filters['city']])); } if (($filters['cuisine'] ?? '') !== '') { $chips[] = $chipFor('cuisine', t_raw('restaurant.chip_cuisine', [':name' => $cuisineLabels[$filters['cuisine']] ?? $filters['cuisine']])); } if (($filters['tag'] ?? '') !== '') { $chips[] = $chipFor('tag', t_raw('restaurant.chip_tag', [':name' => $tagLabels[$filters['tag']] ?? $filters['tag']])); } if ($openNow) { $chips[] = $chipFor('open', t_raw('restaurant.open_now')); } if ($diet !== '') { $chips[] = $chipFor('diet', $dietChipLabels[$diet] ?? ucfirst($diet)); } if ($priceMax > 0) { $chips[] = $chipFor('price_max', t_raw('restaurant.chip_price_up_to', [':price' => money($priceMax)])); } if (($filters['min_rating'] ?? 0) > 0) { $chips[] = $chipFor('min_rating', t_raw('restaurant.chip_min_rating', [':rating' => number_format((float) $filters['min_rating'], 1)])); } // The radius chip carries the CLAMPED figure, not the one that was asked for, so the // chip and the result set always say the same thing. if ($radius > 0 && $origin !== null) { $chips[] = $chipFor('radius', t_raw('restaurant.chip_radius', [':distance' => rtrim(rtrim(number_format($radius, 1), '0'), '.') . ' ' . $distUnit])); } if (($filters['sort'] ?? '') !== '' && isset($sortChipLabels[$filters['sort']])) { $chips[] = $chipFor('sort', $sortChipLabels[$filters['sort']]); } // Clear all keeps only the view, for the same reason a chip does. $clearAllHref = route('restaurants') . ($view !== 'grid' ? '?view=' . $view : ''); @endphp

{{ t_raw('restaurant.browse') }}

@php /* The page's messages belong UNDER its title band, inside the page the reader is looking at — not floating above it. getFlashes() consumes, so the layout's own call further out finds nothing and does not draw it twice. */ @endphp
@partial('flash')
@php // data-filter-auto (not .qm-order-filters, which is the dashboard's // horizontal layout) opts this card into the shared auto-apply behaviour // in js/qm-table-tools.js: every control re-runs the query on change and // the search fires after 3 characters, so there is no Apply step. @endphp

{{ t_raw('restaurant.filters') }}

{{ t_raw('common.reset') }}
@php // Distance (F2/S7). The pin the browser handed over rides along as two // hidden fields so changing any OTHER filter does not lose it — and it is // only ever written in `gps` mode, because in `ip` mode the server already // knows and in `off` mode nobody is asking. They sit outside the arranged // controls below because they are not a control: nothing is drawn, and // dragging the slider must not be able to move them out of the form. @endphp @php // One closure per control, keyed by the query parameter it writes, drawn in the // order the operator arranged on Admin → Settings → General. A control that has // nothing to offer still decides that for itself: the distance slider needs a // located visitor, the tag select needs tags. $filterControls = [ 'q' => function () use ($filters, $__env): void { @endphp
@php }, 'city' => function () use ($filters, $__env): void { @endphp
@php }, 'radius' => function () use ($origin, $geoBrowser, $radius, $radiusMin, $radiusMax, $distUnit, $radiusIgnored, $__env): void { if ($origin === null && !$geoBrowser) { return; } @endphp
@php // The slider rests at 0 = any distance, exactly as the rating slider // rests at 0 = any rating. The operator's floor is enforced by the // controller, not by this input's min, so a value dragged below it // (or typed into the URL) lands on the floor rather than under it. @endphp @if ($geoBrowser) @endif @php /* A radius the server could not apply — nothing has located the visitor yet — said so nowhere at all: the slider moved, the URL carried `radius=`, the label updated, the chip was suppressed and the result count did not budge. One line, in the control that caused it. */ @endphp @if (!empty($radiusIgnored))

{{ t_raw('restaurant.radius_needs_location') }}

@endif
@php }, 'cuisine' => function () use ($filters, $cuisines, $__env): void { @endphp
@php }, 'tag' => function () use ($filters, $tags, $__env): void { if (!$tags) { return; } @endphp
@php }, 'open' => function () use ($openNow, $__env): void { @endphp
{{ t_raw('restaurant.availability_label') }}
@php }, 'diet' => function () use ($diet, $__env): void { @endphp
{{ t_raw('restaurant.dietary_label') }}
@php // FOUR CHIPS, THREE RADIOS. `light` is a first-class diet value everywhere // else — the controller whitelists it, the model tests it against // Restaurant::LIGHT_MEAL_KCAL, and the chip label above already names it — // but the radio that produces it was never drawn, so ?diet=light was // reachable only by typing the URL. @endphp
@php }, 'price_max' => function () use ($priceMax, $__env): void { @endphp
@php }, 'sort' => function () use ($filters, $__env): void { @endphp
@php }, 'min_rating' => function () use ($filters, $__env): void { @endphp
@php }, ]; foreach (\App\Services\ListingFilter::order() as $filterId) { if (isset($filterControls[$filterId])) { $filterControls[$filterId](); } } @endphp @php // Fallback only: hidden once qm-table-tools.js marks the form .is-auto, // but still submits normally for visitors without JavaScript. @endphp
@php // Paid listing ad: only on a tag- or cuisine-filtered page, matching that slug. $adSlug = ($filters['tag'] ?? '') ?: ($filters['cuisine'] ?? ''); $listAd = $adSlug ? active_ad('listing', $adSlug) : null; @endphp @if ($listAd) @partial('cards/ad', ['ad' => $listAd, 'variant' => 'banner']) @endif

{!! t_raw($total === 1 ? 'restaurant.found_count_one' : 'restaurant.found_count_many', [':n' => '' . (int) $total . '']) !!}

@php // The map button exists only where there IS a map: an install with maps // switched off, or a provider chosen without its key, gets two buttons // rather than a third that leads to a blank rectangle. @endphp @if ($mapAvailable) @endif
@if ($chips)
{{ t_raw('restaurant.chips_label') }} @foreach ($chips as $chip) {{ $chip['label'] }} {{ t_raw('restaurant.remove_filter_sr') }} @endforeach {{ t_raw('restaurant.clear_all_chip') }}
@endif @if ($restaurants->isNotEmpty()) @if ($view === 'map') @php // One map, the product's only one: QMMap (Leaflet) with the provider the // admin chose. The whole map is described by these attributes and drawn by // js/restaurant-map.js, so there is no PHP inside a