Skip to content
Open
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
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ yarn-error.log
composer.lock
package-lock.json
yarn.lock
config/lunar
public/build
meilisearch/
.phpunit.cache/test-results
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ docker-compose up

The demo store will be available to `http://localhost` in your browser.

### Searching

Out the box, the starter kit is configured to use Meilisearch. You will need to ensure you have this set up, you should also check the following config files to familiarise yourself how search is configured to work.

- `config/lunar/search.php`
- `config/scout.php`

To customise what is indexed, you should look at the `Search/ProductIndexer` class.

#### Log into Lunar panel

Once the project is prepared, the Lunar panel will start and available to `http://localhost/lunar`.
Expand Down
25 changes: 18 additions & 7 deletions app/Livewire/CollectionPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

namespace App\Livewire;

use App\Livewire\Traits\WithSearch;
use App\Traits\FetchesUrls;
use Illuminate\Support\Collection;
use Illuminate\View\View;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
use Lunar\Models\Collection as CollectionModel;
use Lunar\Search\Contracts\SearchManagerContract;

class CollectionPage extends Component
{
use FetchesUrls;
use WithPagination;
use WithSearch;

public function mount(string $slug): void
{
Expand All @@ -24,21 +29,27 @@ public function mount(string $slug): void
]
);



if (! $this->url) {
abort(404);
}

$this->filters = [
'collection_ids' => [$this->collection->id]
];
}

/**
* Computed property to return the collection.
*/
public function getCollectionProperty(): mixed
#[Computed]
public function collection(): mixed
{
return $this->url->element;
}

public function render(): View
public function render(): \Illuminate\Contracts\View\View
{
return view('livewire.collection-page');
return view('livewire.search-page', [
'isCollection' => true,
]);
}
}
30 changes: 13 additions & 17 deletions app/Livewire/SearchPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,30 @@

namespace App\Livewire;

use App\Livewire\Traits\SearchesProducts;
use App\Livewire\Traits\WithSearch;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\View\View;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Modelable;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
use Lunar\Models\Product;
use Lunar\Search\Contracts\SearchManagerContract;
use Lunar\Search\Data\SearchResults;
use Lunar\Search\Facades\Search;

class SearchPage extends Component
{
use WithPagination;
use WithSearch;

/**
* {@inheritDoc}
*/
protected $queryString = [
'term',
];

/**
* The search term.
*/
public ?string $term = null;

/**
* Return the search results.
*/
public function getResultsProperty(): LengthAwarePaginator
public function mount(Request $request)
{
return Product::search($this->term)->paginate(50);
$this->query = (string) $request->get('query', '');
}

public function render(): View
Expand Down
113 changes: 113 additions & 0 deletions app/Livewire/Traits/WithSearch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

namespace App\Livewire\Traits;

use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Modelable;
use Livewire\Attributes\Url;
use Lunar\Search\Contracts\SearchManagerContract;
use Lunar\Search\Data\SearchResults;
use Lunar\Search\Facades\Search;

trait WithSearch
{
#[Modelable]
#[Url]
public ?array $facets = [];

public array $filters = [];

#[Modelable]
#[Url]
public int $perPage = 50;

#[Modelable]
#[Url]
public string $sort = '';

#[Modelable]
#[Url]
public string $query = '';

public function mountWithSearch(Request $request): void
{
$this->sort = (string) $request->get('sort', '');
}

#[Computed]
public function searchInstance(): SearchManagerContract
{
$search = Search::model(\Lunar\Models\Product::class);

if ($this->facets && count($this->facets)) {
$facets = [];

foreach ($this->facets as $facet) {
[$field, $facetValue] = explode(':', urldecode($facet));

if (empty($facets[$field])) {
$facets[$field] = [];
}

$facets[$field][] = $facetValue;
}

$search->setFacets($facets);
}

$search->filter($this->filters);

return $search;
}



#[Computed]
public function results(): SearchResults
{

$sorting = $this->sort ?: '';

return $this->searchInstance->perPage($this->perPage)->sort($sorting)->query($this->query)->get();
}

#[Computed]
public function displayFacets(): Collection
{
return collect($this->results->facets)->reject(
fn ($facet) => !count($facet->values)
)->values();
}

public function clearFacets(): void
{
$this->facets = [];
}

public function updatedQuery(): void
{
$this->resetPage();
}

public function updatedFacets(): void
{
$this->resetPage();
}

public function updatedPerPage(): void
{
$this->resetPage();
$this->instance->perPage($this->perPage);
}

public function removeFacet(string $facet): void
{
$facets = $this->facets;
unset($facets[
array_search($facet, $facets)
]);
$this->facets = collect($facets)->values()->toArray();
}
}
121 changes: 121 additions & 0 deletions app/Search/ProductIndexer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

namespace App\Search;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;

class ProductIndexer extends \Lunar\Search\ProductIndexer
{
public function makeAllSearchableUsing(Builder $query): Builder
{
return $query->with([
'thumbnail',
'variants',
'productType',
'brand',
'prices',
]);
}

public function toSearchableArray(Model $model): array
{
$priceModels = $model->prices;

$basePrice = $priceModels->first(function ($price) {
return $price->min_quantity == 1;
});

$minPrice = $priceModels->sortBy('price')->first();

$options = $model->productOptions()->with([
'values' => function ($query) use ($model) {
$query->whereHas('variants', function ($relation) use ($model) {
$relation->where('product_id', $model->id);
});
},
])->get()->mapWithKeys(function ($option) {
return [
$option->handle => $option->values->map(function ($value) {
return $value->translate('name');
})
];
})->toArray();

return [
'id' => (string) $model->id,
'name' => $model->attr('name'),
'description' => $model->attr('description'),
'brand' => $model->brand?->name,
'thumbnail' => $model->thumbnail?->getUrl('small'),
'slug' => $model->defaultUrl?->slug,
'price' => [
'inc_tax' => [
'value' => (int) $basePrice->priceIncTax()->value,
'formatted' => $basePrice->priceIncTax()->formatted,
],
'ex_tax' => [
'value' => (int) $basePrice->priceExTax()->value,
'formatted' => $basePrice->priceExTax()->formatted,
],
],
'min_price' => $minPrice?->priceIncTax()?->value ?: 0,
...$options,
...$this->getCollections($model)
];
}

private function getCollections(Model $product): array
{
$trail = [];

$categories = [];
$categoryIds = [];

foreach ($product->collections as $i => $collection) {
$levels = [$collection->translateAttribute('name')];
$levelIds = [$collection->id];
$node = $collection;

while ($node->parent) {
array_unshift($levels, $node->parent->translateAttribute('name'));
array_unshift($levelIds, $node->parent->id);
$node = $node->parent;
}

foreach ($levels as $level => $name) {
if (! isset($trail['lvl'.$level])) {
$trail['lvl'.$level] = [];
}

$key = $level - 1;

$breadcrumb = [$name];

while (isset($levels[$key])) {
array_unshift($breadcrumb, $levels[$key]);
$key--;
}

$trail['lvl'.$level][] = implode(' > ', $breadcrumb);
}

$categories = array_merge($levels, $categories);
$categoryIds = array_merge($levelIds, $categoryIds);
}

foreach ($trail as $key => $values) {
$trail[$key] = collect($values)
->unique()
->values()
->toArray();
}

return [
'hierarchical_collections' => $trail,
'collection_ids' => collect($categoryIds)->unique()->values()->toArray(),
'collections' => collect($categories)->unique()->values()->toArray(),
];
}
}
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"league/flysystem-aws-s3-v3": "^3.0",
"lunarphp/lunar": "^1.0@beta",
"lunarphp/stripe": "^1.0@beta",
"lunarphp/search": "^0.1@alpha",
"lunarphp/table-rate-shipping": "^1.0@beta",
"meilisearch/meilisearch-php": "^1.6",
"predis/predis": "^2.2"
Expand Down
Loading