mirror of https://github.com/kcal-app/kcal.git
Add search/load support to recipes
This commit is contained in:
parent
2a6549c6d2
commit
7526138ad2
|
@ -26,8 +26,7 @@ class RecipeController extends Controller
|
|||
*/
|
||||
public function index(): View
|
||||
{
|
||||
return view('recipes.index')
|
||||
->with('recipes', Recipe::all()->sortBy('name'));
|
||||
return view('recipes.index');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -40,8 +40,15 @@ class RecipeAdapter extends AbstractAdapter
|
|||
*/
|
||||
protected function filter($query, Collection $filters)
|
||||
{
|
||||
if ($term = $filters->get('search')) {
|
||||
$query->where('recipes.name', 'like', "%{$term}%")
|
||||
->orWhere('recipes.description', 'like', "%{$term}%")
|
||||
->orWhere('recipes.source', 'like', "%{$term}%");
|
||||
}
|
||||
else {
|
||||
$this->filterWithScopes($query, $filters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingredient amount relationships.
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
<div x-data="searchView()" x-init="loadMore()">
|
||||
<x-inputs.input type="text"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
autocomplete="off"
|
||||
class="w-full mb-4"
|
||||
@input.debounce.400ms="search($event)" />
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
{{ $results }}
|
||||
</div>
|
||||
<x-inputs.button
|
||||
class="text-xl mt-4"
|
||||
color="blue"
|
||||
type="button"
|
||||
x-show="morePages"
|
||||
x-cloak
|
||||
@click.prevent="loadMore()">
|
||||
Load more
|
||||
</x-inputs.button>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
let searchView = () => {
|
||||
return {
|
||||
results: [],
|
||||
number: 1,
|
||||
size: 12,
|
||||
morePages: false,
|
||||
searchTerm: '{{ $defaultSearch ?? null }}',
|
||||
reset() {
|
||||
this.results = [];
|
||||
this.number = 1;
|
||||
this.searchTerm = null;
|
||||
this.morePages = false;
|
||||
},
|
||||
loadMore() {
|
||||
let url = `{{ $route }}?page[number]=${this.number}&page[size]=${this.size}`;
|
||||
if (this.searchTerm) {
|
||||
url += `&filter[search]=${this.searchTerm}`;
|
||||
}
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.results = [...this.results, ...data.data.map(result => result.attributes)];
|
||||
if (this.number < data.meta.page['last-page']) {
|
||||
this.morePages = true;
|
||||
} else {
|
||||
this.number++;
|
||||
}
|
||||
});
|
||||
},
|
||||
search(e) {
|
||||
this.reset();
|
||||
if (e.target.value !== '') {
|
||||
this.searchTerm = e.target.value;
|
||||
this.loadMore();
|
||||
}
|
||||
else {
|
||||
this.loadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
|
@ -9,15 +9,9 @@
|
|||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
<div x-data="foods()" x-init="loadMore()">
|
||||
<x-inputs.input type="text"
|
||||
name="search"
|
||||
placeholder="Search..."
|
||||
autocomplete="off"
|
||||
class="w-full mb-4"
|
||||
@input.debounce.400ms="search($event)" />
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<template x-for="food in foods" :key="food">
|
||||
<x-search-view :route="route('api:v1:foods.index')">
|
||||
<x-slot name="results">
|
||||
<template x-for="food in results" :key="food">
|
||||
<div class="p-2 font-light rounded-t border-2 border-gray-400">
|
||||
<a class="h-6 w-6 text-gray-500 hover:text-gray-700 hover:border-gray-300 float-right text-sm"
|
||||
x-bind:href="food.editUrl">
|
||||
|
@ -52,66 +46,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<x-inputs.button
|
||||
class="text-xl mt-4"
|
||||
color="blue"
|
||||
type="button"
|
||||
x-show="morePages"
|
||||
@click.prevent="loadMore()">
|
||||
Load more
|
||||
</x-inputs.button>
|
||||
</x-slot>
|
||||
</x-search-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
let foods = () => {
|
||||
return {
|
||||
foods: [],
|
||||
number: 1,
|
||||
size: 12,
|
||||
morePages: true,
|
||||
searchTerm: '{{ $defaultSearch ?? null }}',
|
||||
reset() {
|
||||
this.foods = [];
|
||||
this.number = 1;
|
||||
this.searchTerm = null;
|
||||
this.morePages = true;
|
||||
},
|
||||
loadMore() {
|
||||
let url = `{{ route('api:v1:foods.index') }}?page[number]=${this.number}&page[size]=${this.size}`;
|
||||
if (this.searchTerm) {
|
||||
url += `&filter[search]=${this.searchTerm}`;
|
||||
}
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.foods = [...this.foods, ...data.data.map(food => food.attributes)];
|
||||
if (this.number >= data.meta.page['last-page']) {
|
||||
this.morePages = false;
|
||||
} else {
|
||||
this.number++;
|
||||
}
|
||||
});
|
||||
},
|
||||
search(e) {
|
||||
this.reset();
|
||||
if (e.target.value !== '') {
|
||||
this.searchTerm = e.target.value;
|
||||
this.loadMore();
|
||||
}
|
||||
else {
|
||||
this.loadMore();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
@endonce
|
||||
</x-app-layout>
|
||||
|
|
|
@ -9,11 +9,12 @@
|
|||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6 bg-white border-b border-gray-200">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
@foreach ($recipes as $recipe)
|
||||
<x-search-view :route="route('api:v1:recipes.index')">
|
||||
<x-slot name="results">
|
||||
<template x-for="recipe in results" :key="recipe">
|
||||
<div class="p-2 font-light rounded-lg border-2 border-gray-200">
|
||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300 float-right text-sm"
|
||||
href="{{ route('recipes.edit', $recipe) }}">
|
||||
x-bind:href="recipe.editUrl">
|
||||
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
|
||||
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" />
|
||||
|
@ -21,28 +22,29 @@
|
|||
</a>
|
||||
<div class="pb-2 flex justify-between items-baseline">
|
||||
<div class="text-2xl">
|
||||
<a href="{{ route('recipes.show', $recipe) }}"
|
||||
class="text-gray-600 hover:text-gray-800">{{ $recipe->name }}</a>
|
||||
<a x-bind:href="recipe.showUrl"
|
||||
class="text-gray-600 hover:text-gray-800" x-text="recipe.name"></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 text-sm border-t-8 border-black pt-2">
|
||||
<div class="col-span-2 text-xs text-right">Amount per serving</div>
|
||||
<div class="font-extrabold text-lg border-b-4 border-black">Calories</div>
|
||||
<div class="font-extrabold text-right text-lg border-b-4 border-black">{{ $recipe->caloriesPerServing() }}</div>
|
||||
<div class="font-extrabold text-right text-lg border-b-4 border-black" x-text="`${recipe.caloriesPerServing}g`"></div>
|
||||
<div class="font-bold border-b border-gray-300">Fat</div>
|
||||
<div class="text-right border-b border-gray-300">{{ $recipe->fatPerServing() < 1 ? $recipe->fatPerServing() * 1000 . "m" : $recipe->fatPerServing() }}g</div>
|
||||
<div class="text-right border-b border-gray-300" x-text="`${recipe.fatPerServing}g`"></div>
|
||||
<div class="font-bold border-b border-gray-300">Cholesterol</div>
|
||||
<div class="text-right border-b border-gray-300">{{ $recipe->cholesterolPerServing() < 1 ? $recipe->cholesterolPerServing() * 1000 . "m" : $recipe->cholesterolPerServing() }}g</div>
|
||||
<div class="text-right border-b border-gray-300" x-text="`${Math.round(recipe.cholesterolPerServing)}mg`"></div>
|
||||
<div class="font-bold border-b border-gray-300">Sodium</div>
|
||||
<div class="text-right border-b border-gray-300">{{ $recipe->sodiumPerServing() < 1 ? $recipe->sodiumPerServing() * 1000 . "m" : $recipe->sodiumPerServing() }}g</div>
|
||||
<div class="text-right border-b border-gray-300" x-text="`${Math.round(recipe.sodiumPerServing)}mg`"></div>
|
||||
<div class="font-bold border-b border-gray-300">Carbohydrates</div>
|
||||
<div class="text-right border-b border-gray-300">{{ $recipe->carbohydratesPerServing() < 1 ? $recipe->carbohydratesPerServing() * 1000 . "m" : $recipe->carbohydratesPerServing() }}g</div>
|
||||
<div class="text-right border-b border-gray-300" x-text="`${recipe.carbohydratesPerServing}g`"></div>
|
||||
<div class="font-bold">Protein</div>
|
||||
<div class="text-right">{{ $recipe->proteinPerServing() < 1 ? $recipe->proteinPerServing() * 1000 . "m" : $recipe->proteinPerServing() }}g</div>
|
||||
<div class="text-right" x-text="`${recipe.proteinPerServing}g`"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</template>
|
||||
</x-slot>
|
||||
</x-search-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue