Complete AlpineJS-based ingredient picket

This commit is contained in:
Christopher C. Wells 2021-01-20 20:39:14 -08:00 committed by Christopher Charbonneau Wells
parent cb7483ae06
commit f2864b76dc
5 changed files with 83 additions and 64 deletions

View File

@ -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);
} }
} }

View File

@ -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);

View File

@ -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.
*/ */

View File

@ -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>

View File

@ -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>