mirror of https://github.com/kcal-app/kcal.git
Complete AlpineJS-based ingredient picket
This commit is contained in:
parent
cb7483ae06
commit
f2864b76dc
|
|
@ -2,18 +2,22 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Food;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class IngredientPickerController extends Controller
|
class IngredientPickerController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Search for ingredients.
|
* Search for ingredients.
|
||||||
*/
|
*/
|
||||||
public function search(): JsonResponse
|
public function search(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
return response()->json([
|
$results = [];
|
||||||
['id' => 1, 'name' => 'Flour'],
|
$term = $request->query->get('term');
|
||||||
['id' => 2, 'name' => 'Eggs'],
|
if (!empty($term)) {
|
||||||
]);
|
$results = Food::search($term);
|
||||||
|
}
|
||||||
|
return response()->json($results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Food;
|
|
||||||
use App\Models\FoodAmount;
|
use App\Models\FoodAmount;
|
||||||
use App\Models\Recipe;
|
use App\Models\Recipe;
|
||||||
use App\Models\RecipeStep;
|
use App\Models\RecipeStep;
|
||||||
|
|
@ -73,7 +72,7 @@ class RecipeController extends Controller
|
||||||
{
|
{
|
||||||
return view('recipes.edit')
|
return view('recipes.edit')
|
||||||
->with('recipe', $recipe)
|
->with('recipe', $recipe)
|
||||||
->with('food_units', new Collection([
|
->with('ingredients_units', new Collection([
|
||||||
['value' => 'tsp', 'label' => 'tsp.'],
|
['value' => 'tsp', 'label' => 'tsp.'],
|
||||||
['value' => 'tbsp', 'label' => 'tbsp.'],
|
['value' => 'tbsp', 'label' => 'tbsp.'],
|
||||||
['value' => 'cup', 'label' => 'cup'],
|
['value' => 'cup', 'label' => 'cup'],
|
||||||
|
|
@ -99,14 +98,14 @@ class RecipeController extends Controller
|
||||||
'description' => 'nullable|string',
|
'description' => 'nullable|string',
|
||||||
'source' => 'nullable|string',
|
'source' => 'nullable|string',
|
||||||
'servings' => 'required|numeric',
|
'servings' => 'required|numeric',
|
||||||
'foods_amount' => ['required', 'array', new ArrayNotEmpty],
|
'ingredients_amount' => ['required', 'array', new ArrayNotEmpty],
|
||||||
'foods_amount.*' => ['required_with:foods.*', 'nullable', new StringIsDecimalOrFraction],
|
'ingredients_amount.*' => ['required_with:ingredients.*', 'nullable', new StringIsDecimalOrFraction],
|
||||||
'foods_unit' => ['required', 'array'],
|
'ingredients_unit' => ['required', 'array'],
|
||||||
'foods_unit.*' => 'nullable|string',
|
'ingredients_unit.*' => 'nullable|string',
|
||||||
'foods_detail' => ['required', 'array'],
|
'ingredients_detail' => ['required', 'array'],
|
||||||
'foods_detail.*' => 'nullable|string',
|
'ingredients_detail.*' => 'nullable|string',
|
||||||
'foods' => ['required', 'array', new ArrayNotEmpty],
|
'ingredients' => ['required', 'array', new ArrayNotEmpty],
|
||||||
'foods.*' => 'required_with:foods_amount.*|nullable|exists:App\Models\Food,id',
|
'ingredients.*' => 'required_with:ingredients_amount.*|nullable|exists:App\Models\Food,id',
|
||||||
'steps' => ['required', 'array', new ArrayNotEmpty],
|
'steps' => ['required', 'array', new ArrayNotEmpty],
|
||||||
'steps.*' => 'nullable|string',
|
'steps.*' => 'nullable|string',
|
||||||
]);
|
]);
|
||||||
|
|
@ -127,15 +126,15 @@ class RecipeController extends Controller
|
||||||
$food_amounts = [];
|
$food_amounts = [];
|
||||||
$weight = 0;
|
$weight = 0;
|
||||||
// TODO: Handle removals.
|
// TODO: Handle removals.
|
||||||
foreach (array_filter($input['foods_amount']) as $key => $amount) {
|
foreach (array_filter($input['ingredients_amount']) as $key => $amount) {
|
||||||
$food_amounts[$key] = $recipe->foodAmounts[$key] ?? new FoodAmount();
|
$food_amounts[$key] = $recipe->foodAmounts[$key] ?? new FoodAmount();
|
||||||
$food_amounts[$key]->fill([
|
$food_amounts[$key]->fill([
|
||||||
'amount' => Number::floatFromString($amount),
|
'amount' => Number::floatFromString($amount),
|
||||||
'unit' => $input['foods_unit'][$key],
|
'unit' => $input['ingredients_unit'][$key],
|
||||||
'detail' => $input['foods_detail'][$key],
|
'detail' => $input['ingredients_detail'][$key],
|
||||||
'weight' => $weight++,
|
'weight' => $weight++,
|
||||||
]);
|
]);
|
||||||
$food_amounts[$key]->food()->associate($input['foods'][$key]);
|
$food_amounts[$key]->food()->associate($input['ingredients'][$key]);
|
||||||
}
|
}
|
||||||
$recipe->foodAmounts()->saveMany($food_amounts);
|
$recipe->foodAmounts()->saveMany($food_amounts);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Traits\Journalable;
|
use App\Models\Traits\Journalable;
|
||||||
use App\Models\Traits\Sluggable;
|
use App\Models\Traits\Sluggable;
|
||||||
|
use App\Support\Number;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
@ -84,7 +85,7 @@ class Food extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be cast.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'calories' => 'float',
|
'calories' => 'float',
|
||||||
|
|
@ -97,6 +98,18 @@ class Food extends Model
|
||||||
'sodium' => 'float',
|
'sodium' => 'float',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected $appends = ['serving_size_formatted'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the serving size as a fractional.
|
||||||
|
*/
|
||||||
|
public function getServingSizeFormattedAttribute(): string {
|
||||||
|
return Number::fractionStringFromFloat($this->serving_size);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the food amounts using this food.
|
* Get the food amounts using this food.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,42 @@
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<x-inputs.input type="hidden"
|
<x-inputs.input type="hidden"
|
||||||
name="ingredients[{{ $index }}]"
|
name="ingredients[]"
|
||||||
value="{{ $defaultId ?? '' }}"
|
value="{{ $defaultId ?? '' }}"
|
||||||
x-ref="ingredients{{ $index }}"/>
|
x-ref="ingredients"/>
|
||||||
<x-inputs.input type="text"
|
<x-inputs.input type="text"
|
||||||
name="ingredients_name[{{ $index }}]"
|
name="ingredients_name[]"
|
||||||
value="{{ $defaultName ?? '' }}"
|
value="{{ $defaultName ?? '' }}"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
x-on:input.debounce.400ms="if ($event.target.value != '') {
|
x-on:input.debounce.400ms="if ($event.target.value != '') {
|
||||||
fetch('{{ route('ingredient-picker.search') }}')
|
fetch('{{ route('ingredient-picker.search') }}?term=' + $event.target.value)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => { results = data; searching = true; }); }"
|
.then(data => { results = data; searching = true; }); }"
|
||||||
x-on:focusout.debounce.200ms="searching = false;"
|
x-on:focusout.debounce.200ms="searching = false;"
|
||||||
x-ref="ingredients_name{{ $index }}" />
|
x-ref="ingredients_name" />
|
||||||
</div>
|
</div>
|
||||||
<div x-show="searching" x-cloak>
|
<div x-show="searching" x-cloak>
|
||||||
<div class="absolute border-2 border-gray-500 border-b-0 bg-white"
|
<div class="absolute border-2 border-gray-500 border-b-0 bg-white"
|
||||||
x-on:click="selected = $event.target; if (selected.dataset.id) { $refs.ingredients_name{{ $index }}.value = selected.dataset.value; $refs.ingredients{{ $index }}.value = selected.dataset.id; searching = false; }">
|
x-on:click="selected = $event.target; if (selected.dataset.id) { $refs.ingredients_name.value = selected.dataset.value; $refs.ingredients.value = selected.dataset.id; searching = false; }">
|
||||||
<template x-for="result in results" :key="result.id">
|
<template x-for="result in results" :key="result.id">
|
||||||
<div class="p-1 border-b-2 border-gray-500 hover:bg-yellow-300 cursor-pointer"
|
<div class="p-1 border-b-2 border-gray-500 hover:bg-yellow-300 cursor-pointer"
|
||||||
x-bind:data-id="result.id"
|
x-bind:data-id="result.id"
|
||||||
x-bind:data-value="result.name">
|
x-bind:data-value="result.name">
|
||||||
<div class="pointer-events-none">
|
<div class="pointer-events-none">
|
||||||
<div x-text="result.name"></div>
|
<div x-text="result.name"></div>
|
||||||
|
<div class="text-sm text-gray-600" x-text="result.brand" x-show="result.brand"></div>
|
||||||
|
<div class="text-sm">
|
||||||
|
Serving size <span x-text="result.serving_size_formatted"></span>
|
||||||
|
<span x-text="result.serving_unit"></span>
|
||||||
|
(<span x-text="result.serving_weight"></span>g)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<div class="p-1 border-b-2 border-gray-500" x-cloak x-show="searching && results.length == 0">
|
||||||
|
No results found.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -64,46 +64,40 @@
|
||||||
<!-- Ingredients -->
|
<!-- Ingredients -->
|
||||||
<h3 class="pt-2 mb-2 font-extrabold">Ingredients</h3>
|
<h3 class="pt-2 mb-2 font-extrabold">Ingredients</h3>
|
||||||
<div x-data="{ ingredients: 0 }">
|
<div x-data="{ ingredients: 0 }">
|
||||||
{{-- @foreach($recipe->foodAmounts as $foodAmount)--}}
|
@foreach($recipe->foodAmounts as $foodAmount)
|
||||||
{{-- <div class="flex flex-row space-x-4 mb-4">--}}
|
<div class="flex flex-row space-x-4 mb-4">
|
||||||
{{-- <x-inputs.input type="text"--}}
|
<x-inputs.input type="text"
|
||||||
{{-- name="foods_amount[]"--}}
|
name="ingredients_amount[]"
|
||||||
{{-- size="5"--}}
|
size="5"
|
||||||
{{-- :value="old('foods_amount.' . $loop->index, \App\Support\Number::fractionStringFromFloat($foodAmount->amount))" />--}}
|
:value="old('ingredients_amount.' . $loop->index, \App\Support\Number::fractionStringFromFloat($foodAmount->amount))" />
|
||||||
{{-- <x-inputs.select name="foods_unit[]"--}}
|
<x-inputs.select name="ingredients_unit[]"
|
||||||
{{-- :options="$food_units"--}}
|
:options="$ingredients_units"
|
||||||
{{-- :selectedValue="old('foods_unit.' . $loop->index, $foodAmount->unit)">--}}
|
:selectedValue="old('ingredients_unit.' . $loop->index, $foodAmount->unit)">
|
||||||
{{-- <option value=""></option>--}}
|
<option value=""></option>
|
||||||
{{-- </x-inputs.select>--}}
|
</x-inputs.select>
|
||||||
{{-- <livewire:food-picker :index="$loop->index"--}}
|
<x-ingredient-picker :default-id="old('ingredients.' . $loop->index, $foodAmount->food->id)"
|
||||||
{{-- :default-id="old('foods.' . $loop->index, $foodAmount->food->id)"--}}
|
:default-name="old('ingredients_name.' . $loop->index, $foodAmount->food->name)"/>
|
||||||
{{-- :default-name="old('foods_name.' . $loop->index, $foodAmount->food->name)" />--}}
|
<x-inputs.input type="text"
|
||||||
{{-- <x-inputs.input type="text"--}}
|
class="block"
|
||||||
{{-- class="block"--}}
|
name="ingredients_detail[]"
|
||||||
{{-- name="foods_detail[]"--}}
|
:value="old('ingredients_detail.' . $loop->index, $foodAmount->detail)" />
|
||||||
{{-- :value="old('foods_detail.' . $loop->index, $foodAmount->detail)" />--}}
|
</div>
|
||||||
{{-- </div>--}}
|
@endforeach
|
||||||
{{-- @endforeach--}}
|
|
||||||
<template x-for="i in ingredients + 1">
|
<template x-for="i in ingredients + 1">
|
||||||
<x-ingredient-picker index="1" />
|
<div class="flex flex-row space-x-4 mb-4">
|
||||||
|
<x-inputs.input type="text"
|
||||||
|
name="ingredients_amount[]"
|
||||||
|
size="5" />
|
||||||
|
<x-inputs.select name="ingredients_unit[]"
|
||||||
|
:options="$ingredients_units" >
|
||||||
|
<option value=""></option>
|
||||||
|
</x-inputs.select>
|
||||||
|
<x-ingredient-picker/>
|
||||||
|
<x-inputs.input type="text"
|
||||||
|
class="block"
|
||||||
|
name="ingredients_detail[]" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
{{-- <template x-for="i in ingredients + 1">--}}
|
|
||||||
{{-- <div class="flex flex-row space-x-4 mb-4">--}}
|
|
||||||
{{-- <x-inputs.input type="text"--}}
|
|
||||||
{{-- name="foods_amount[]"--}}
|
|
||||||
{{-- size="5" />--}}
|
|
||||||
{{-- <x-inputs.select name="foods_unit[]"--}}
|
|
||||||
{{-- :options="$food_units" >--}}
|
|
||||||
{{-- <option value=""></option>--}}
|
|
||||||
{{-- </x-inputs.select>--}}
|
|
||||||
{{-- <!-- TODO: Get this working in the template. See wire:init or use wire:click? -->--}}
|
|
||||||
{{-- <livewire:food-picker index="1" />--}}
|
|
||||||
{{-- <x-inputs.input type="text"--}}
|
|
||||||
{{-- class="block"--}}
|
|
||||||
{{-- name="foods_detail[]" />--}}
|
|
||||||
{{-- </div>--}}
|
|
||||||
{{-- </template>--}}
|
|
||||||
<x-inputs.button type="button" class="ml-3" x-on:click="ingredients++;">
|
<x-inputs.button type="button" class="ml-3" x-on:click="ingredients++;">
|
||||||
Add Ingredient
|
Add Ingredient
|
||||||
</x-inputs.button>
|
</x-inputs.button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue