mirror of https://github.com/kcal-app/kcal.git
Add ingredient separators (#10)
* Add weight handling to ingredient sortable * Add frontend logic for recipe ingredients "separator" * Add "recipe separator" model * Update ingredient handlers on recipe save * Combine ingredients and separators handler in recipe edit * Handle recipe ingredient separators in recipe show * Fix handling of old recipe form data
This commit is contained in:
parent
15e016692c
commit
0407899496
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Models\Food;
|
|
||||||
use App\Models\IngredientAmount;
|
use App\Models\IngredientAmount;
|
||||||
use App\Models\Recipe;
|
use App\Models\Recipe;
|
||||||
|
use App\Models\RecipeSeparator;
|
||||||
use App\Models\RecipeStep;
|
use App\Models\RecipeStep;
|
||||||
use App\Rules\ArrayNotEmpty;
|
use App\Rules\ArrayNotEmpty;
|
||||||
use App\Rules\StringIsDecimalOrFraction;
|
use App\Rules\StringIsDecimalOrFraction;
|
||||||
|
@ -84,11 +84,10 @@ class RecipeController extends Controller
|
||||||
$ingredients = [];
|
$ingredients = [];
|
||||||
if ($old = old('ingredients')) {
|
if ($old = old('ingredients')) {
|
||||||
foreach ($old['id'] as $key => $ingredient_id) {
|
foreach ($old['id'] as $key => $ingredient_id) {
|
||||||
if (empty($ingredient_id)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$ingredients[] = [
|
$ingredients[] = [
|
||||||
'original_key' => $old['original_key'][$key],
|
'type' => 'ingredient',
|
||||||
|
'key' => $old['key'][$key],
|
||||||
|
'weight' => $old['weight'][$key],
|
||||||
'amount' => $old['amount'][$key],
|
'amount' => $old['amount'][$key],
|
||||||
'unit' => $old['unit'][$key],
|
'unit' => $old['unit'][$key],
|
||||||
'ingredient_id' => $ingredient_id,
|
'ingredient_id' => $ingredient_id,
|
||||||
|
@ -101,7 +100,9 @@ class RecipeController extends Controller
|
||||||
else {
|
else {
|
||||||
foreach ($recipe->ingredientAmounts as $key => $ingredientAmount) {
|
foreach ($recipe->ingredientAmounts as $key => $ingredientAmount) {
|
||||||
$ingredients[] = [
|
$ingredients[] = [
|
||||||
'original_key' => $key,
|
'type' => 'ingredient',
|
||||||
|
'key' => $key,
|
||||||
|
'weight' => $ingredientAmount->weight,
|
||||||
'amount' => $ingredientAmount->amount_formatted,
|
'amount' => $ingredientAmount->amount_formatted,
|
||||||
'unit' => $ingredientAmount->unit,
|
'unit' => $ingredientAmount->unit,
|
||||||
'ingredient_id' => $ingredientAmount->ingredient_id,
|
'ingredient_id' => $ingredientAmount->ingredient_id,
|
||||||
|
@ -112,6 +113,28 @@ class RecipeController extends Controller
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$separators = [];
|
||||||
|
if ($old = old('separators')) {
|
||||||
|
foreach ($old['key'] as $index => $key) {
|
||||||
|
$separators[] = [
|
||||||
|
'type' => 'separator',
|
||||||
|
'key' => $old['key'][$index],
|
||||||
|
'weight' => $old['weight'][$index],
|
||||||
|
'text' => $old['text'][$index],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach ($recipe->ingredientSeparators as $key => $ingredientSeparator) {
|
||||||
|
$separators[] = [
|
||||||
|
'type' => 'separator',
|
||||||
|
'key' => $key,
|
||||||
|
'weight' => $ingredientSeparator->weight,
|
||||||
|
'text' => $ingredientSeparator->text,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$steps = [];
|
$steps = [];
|
||||||
if ($old = old('steps')) {
|
if ($old = old('steps')) {
|
||||||
foreach ($old['step'] as $key => $step) {
|
foreach ($old['step'] as $key => $step) {
|
||||||
|
@ -119,7 +142,7 @@ class RecipeController extends Controller
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$steps[] = [
|
$steps[] = [
|
||||||
'original_key' => $old['original_key'][$key],
|
'key' => $old['key'][$key],
|
||||||
'step_default' => $step,
|
'step_default' => $step,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -127,7 +150,7 @@ class RecipeController extends Controller
|
||||||
else {
|
else {
|
||||||
foreach ($recipe->steps as $key => $step) {
|
foreach ($recipe->steps as $key => $step) {
|
||||||
$steps[] = [
|
$steps[] = [
|
||||||
'original_key' => $key,
|
'key' => $key,
|
||||||
'step_default' => $step->step,
|
'step_default' => $step->step,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -142,7 +165,7 @@ class RecipeController extends Controller
|
||||||
return view('recipes.edit')
|
return view('recipes.edit')
|
||||||
->with('recipe', $recipe)
|
->with('recipe', $recipe)
|
||||||
->with('recipe_tags', $recipe_tags)
|
->with('recipe_tags', $recipe_tags)
|
||||||
->with('ingredients', $ingredients)
|
->with('ingredients_list', new Collection([...$ingredients, ...$separators]))
|
||||||
->with('steps', $steps)
|
->with('steps', $steps)
|
||||||
->with('ingredients_units', Nutrients::$units);
|
->with('ingredients_units', Nutrients::$units);
|
||||||
}
|
}
|
||||||
|
@ -175,15 +198,24 @@ class RecipeController extends Controller
|
||||||
'ingredients.unit' => ['required', 'array'],
|
'ingredients.unit' => ['required', 'array'],
|
||||||
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
||||||
'ingredients.detail' => ['required', 'array'],
|
'ingredients.detail' => ['required', 'array'],
|
||||||
'ingredients.detail.*' => 'nullable|string',
|
'ingredients.detail.*' => ['nullable', 'string'],
|
||||||
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
|
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
|
||||||
'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable',
|
'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable',
|
||||||
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
|
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
|
||||||
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
|
'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()],
|
||||||
'ingredients.original_key' => 'nullable|array',
|
'ingredients.key' => ['required', 'array', new ArrayNotEmpty],
|
||||||
|
'ingredients.key.*' => ['nullable', 'int'],
|
||||||
|
'ingredients.weight' => ['required', 'array', new ArrayNotEmpty],
|
||||||
|
'ingredients.weight.*' => ['required', 'int'],
|
||||||
|
'separators.key' => ['nullable', 'array'],
|
||||||
|
'separators.key.*' => ['nullable', 'int'],
|
||||||
|
'separators.weight' => ['nullable', 'array'],
|
||||||
|
'separators.weight.*' => ['required', 'int'],
|
||||||
|
'separators.text' => ['nullable', 'array'],
|
||||||
|
'separators.text.*' => ['nullable', 'string'],
|
||||||
'steps.step' => ['required', 'array', new ArrayNotEmpty],
|
'steps.step' => ['required', 'array', new ArrayNotEmpty],
|
||||||
'steps.step.*' => 'nullable|string',
|
'steps.step.*' => ['nullable', 'string'],
|
||||||
'steps.original_key' => 'nullable|array',
|
'steps.key' => ['nullable', 'array'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Validate that no ingredients are recursive.
|
// Validate that no ingredients are recursive.
|
||||||
|
@ -208,53 +240,9 @@ class RecipeController extends Controller
|
||||||
try {
|
try {
|
||||||
DB::transaction(function () use ($recipe, $input) {
|
DB::transaction(function () use ($recipe, $input) {
|
||||||
$recipe->saveOrFail();
|
$recipe->saveOrFail();
|
||||||
|
$this->updateIngredients($recipe, $input);
|
||||||
// Delete any removed ingredients.
|
$this->updateIngredientSeparators($recipe, $input);
|
||||||
$removed = array_diff($recipe->ingredientAmounts->keys()->all(), $input['ingredients']['original_key']);
|
$this->updateSteps($recipe, $input);
|
||||||
foreach ($removed as $removed_key) {
|
|
||||||
$recipe->ingredientAmounts[$removed_key]->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add/update current ingredients.
|
|
||||||
$ingredient_amounts = [];
|
|
||||||
$weight = 0;
|
|
||||||
foreach (array_filter($input['ingredients']['id']) as $key => $ingredient_id) {
|
|
||||||
if (!is_null($input['ingredients']['original_key'][$key])) {
|
|
||||||
$ingredient_amounts[$key] = $recipe->ingredientAmounts[$input['ingredients']['original_key'][$key]];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$ingredient_amounts[$key] = new IngredientAmount();
|
|
||||||
}
|
|
||||||
$ingredient_amounts[$key]->fill([
|
|
||||||
'amount' => Number::floatFromString($input['ingredients']['amount'][$key]),
|
|
||||||
'unit' => $input['ingredients']['unit'][$key],
|
|
||||||
'detail' => $input['ingredients']['detail'][$key],
|
|
||||||
'weight' => $weight++,
|
|
||||||
]);
|
|
||||||
$ingredient_amounts[$key]->ingredient()
|
|
||||||
->associate($input['ingredients']['type'][$key]::where('id', $ingredient_id)->first());
|
|
||||||
}
|
|
||||||
$recipe->ingredientAmounts()->saveMany($ingredient_amounts);
|
|
||||||
|
|
||||||
$steps = [];
|
|
||||||
$number = 1;
|
|
||||||
|
|
||||||
// Delete any removed steps.
|
|
||||||
$removed = array_diff($recipe->steps->keys()->all(), $input['steps']['original_key']);
|
|
||||||
foreach ($removed as $removed_key) {
|
|
||||||
$recipe->steps[$removed_key]->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (array_filter($input['steps']['step']) as $key => $step) {
|
|
||||||
if (!is_null($input['steps']['original_key'][$key])) {
|
|
||||||
$steps[$key] = $recipe->steps[$input['steps']['original_key'][$key]];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$steps[$key] = new RecipeStep();
|
|
||||||
}
|
|
||||||
$steps[$key]->fill(['number' => $number++, 'step' => $step]);
|
|
||||||
}
|
|
||||||
$recipe->steps()->saveMany($steps);
|
|
||||||
});
|
});
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
DB::rollBack();
|
DB::rollBack();
|
||||||
|
@ -289,6 +277,114 @@ class RecipeController extends Controller
|
||||||
return redirect()->route('recipes.show', $recipe);
|
return redirect()->route('recipes.show', $recipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates recipe ingredients data based on input.
|
||||||
|
*
|
||||||
|
* @param \App\Models\Recipe $recipe
|
||||||
|
* @param array $input
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function updateIngredients(Recipe $recipe, array $input): void {
|
||||||
|
// Delete any removed ingredients.
|
||||||
|
$removed = array_diff($recipe->ingredientAmounts->keys()->all(), $input['ingredients']['key']);
|
||||||
|
foreach ($removed as $removed_key) {
|
||||||
|
$recipe->ingredientAmounts[$removed_key]->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update current ingredients.
|
||||||
|
$ingredient_amounts = [];
|
||||||
|
foreach (array_filter($input['ingredients']['id']) as $key => $ingredient_id) {
|
||||||
|
if (!is_null($input['ingredients']['key'][$key])) {
|
||||||
|
$ingredient_amounts[$key] = $recipe->ingredientAmounts[$input['ingredients']['key'][$key]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$ingredient_amounts[$key] = new IngredientAmount();
|
||||||
|
}
|
||||||
|
$ingredient_amounts[$key]->fill([
|
||||||
|
'amount' => Number::floatFromString($input['ingredients']['amount'][$key]),
|
||||||
|
'unit' => $input['ingredients']['unit'][$key],
|
||||||
|
'detail' => $input['ingredients']['detail'][$key],
|
||||||
|
'weight' => (int) $input['ingredients']['weight'][$key],
|
||||||
|
]);
|
||||||
|
$ingredient_amounts[$key]->ingredient()
|
||||||
|
->associate($input['ingredients']['type'][$key]::where('id', $ingredient_id)->first());
|
||||||
|
}
|
||||||
|
$recipe->ingredientAmounts()->saveMany($ingredient_amounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates recipe steps data based on input.
|
||||||
|
*
|
||||||
|
* @param \App\Models\Recipe $recipe
|
||||||
|
* @param array $input
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function updateSteps(Recipe $recipe, array $input): void {
|
||||||
|
$steps = [];
|
||||||
|
$number = 1;
|
||||||
|
|
||||||
|
// Delete any removed steps.
|
||||||
|
$removed = array_diff($recipe->steps->keys()->all(), $input['steps']['key']);
|
||||||
|
foreach ($removed as $removed_key) {
|
||||||
|
$recipe->steps[$removed_key]->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_filter($input['steps']['step']) as $key => $step) {
|
||||||
|
if (!is_null($input['steps']['key'][$key])) {
|
||||||
|
$steps[$key] = $recipe->steps[$input['steps']['key'][$key]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$steps[$key] = new RecipeStep();
|
||||||
|
}
|
||||||
|
$steps[$key]->fill(['number' => $number++, 'step' => $step]);
|
||||||
|
}
|
||||||
|
$recipe->steps()->saveMany($steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates recipe ingredient separators data based on input.
|
||||||
|
*
|
||||||
|
* @param \App\Models\Recipe $recipe
|
||||||
|
* @param array $input
|
||||||
|
*
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
private function updateIngredientSeparators(Recipe $recipe, array $input): void {
|
||||||
|
// Take no action of remove all separators
|
||||||
|
if (!isset($input['separators']) || empty($input['separators'])) {
|
||||||
|
if ($recipe->ingredientSeparators->isNotEmpty()) {
|
||||||
|
$recipe->ingredientSeparators()->delete();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete any removed separators.
|
||||||
|
$removed = array_diff($recipe->ingredientSeparators->keys()->all(), $input['separators']['key']);
|
||||||
|
foreach ($removed as $removed_key) {
|
||||||
|
$recipe->ingredientSeparators[$removed_key]->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add/update current separators.
|
||||||
|
$ingredient_separators = [];
|
||||||
|
foreach ($input['separators']['key'] as $index => $key) {
|
||||||
|
if (!is_null($key)) {
|
||||||
|
$ingredient_separators[$index] = $recipe->ingredientSeparators[$key];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$ingredient_separators[$index] = new RecipeSeparator();
|
||||||
|
}
|
||||||
|
$ingredient_separators[$index]->fill([
|
||||||
|
'container' => 'ingredients',
|
||||||
|
'text' => $input['separators']['text'][$index],
|
||||||
|
'weight' => (int) $input['separators']['weight'][$index],
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
$recipe->ingredientSeparators()->saveMany($ingredient_separators);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm removal of specified resource.
|
* Confirm removal of specified resource.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -72,6 +72,7 @@ use Spatie\Tags\HasTags;
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Food withAnyTagsOfAnyType($tags)
|
* @method static \Illuminate\Database\Eloquent\Builder|Food withAnyTagsOfAnyType($tags)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Food withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug)
|
* @method static \Illuminate\Database\Eloquent\Builder|Food withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug)
|
||||||
|
* @method static \Database\Factories\FoodFactory factory(...$parameters)
|
||||||
*/
|
*/
|
||||||
final class Food extends Model
|
final class Food extends Model
|
||||||
{
|
{
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Support\Nutrients;
|
use App\Support\Nutrients;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
@ -37,8 +36,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
*/
|
*/
|
||||||
final class Goal extends Model
|
final class Goal extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supported options for thr frequency attribute.
|
* Supported options for thr frequency attribute.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -43,6 +43,7 @@ use Illuminate\Support\Pluralizer;
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
* @property-read string|null $unit_formatted
|
* @property-read string|null $unit_formatted
|
||||||
* @property-read string $nutrients_summary
|
* @property-read string $nutrients_summary
|
||||||
|
* @method static \Database\Factories\IngredientAmountFactory factory(...$parameters)
|
||||||
*/
|
*/
|
||||||
final class IngredientAmount extends Model
|
final class IngredientAmount extends Model
|
||||||
{
|
{
|
||||||
|
|
|
@ -48,6 +48,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\IngredientAmount[] $ingredients
|
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\IngredientAmount[] $ingredients
|
||||||
* @property-read int|null $ingredients_count
|
* @property-read int|null $ingredients_count
|
||||||
|
* @method static \Database\Factories\JournalEntryFactory factory(...$parameters)
|
||||||
*/
|
*/
|
||||||
final class JournalEntry extends Model
|
final class JournalEntry extends Model
|
||||||
{
|
{
|
||||||
|
|
|
@ -10,6 +10,7 @@ use ElasticScoutDriverPlus\QueryDsl;
|
||||||
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;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Laravel\Scout\Searchable;
|
use Laravel\Scout\Searchable;
|
||||||
use Spatie\Image\Manipulations;
|
use Spatie\Image\Manipulations;
|
||||||
use Spatie\MediaLibrary\HasMedia;
|
use Spatie\MediaLibrary\HasMedia;
|
||||||
|
@ -69,6 +70,11 @@ use Spatie\Tags\HasTags;
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|Recipe whereDescriptionDelta($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|Recipe whereDescriptionDelta($value)
|
||||||
* @property-read \Spatie\MediaLibrary\MediaCollections\Models\Collections\MediaCollection|Media[] $media
|
* @property-read \Spatie\MediaLibrary\MediaCollections\Models\Collections\MediaCollection|Media[] $media
|
||||||
* @property-read int|null $media_count
|
* @property-read int|null $media_count
|
||||||
|
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\RecipeSeparator[] $ingredientSeparators
|
||||||
|
* @property-read int|null $ingredient_separators_count
|
||||||
|
* @method static \Database\Factories\RecipeFactory factory(...$parameters)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|Recipe whereTimeCook($value)
|
||||||
|
* @property-read Collection $ingredients_list
|
||||||
*/
|
*/
|
||||||
final class Recipe extends Model implements HasMedia
|
final class Recipe extends Model implements HasMedia
|
||||||
{
|
{
|
||||||
|
@ -158,6 +164,16 @@ final class Recipe extends Model implements HasMedia
|
||||||
return round($this->weight / $this->servings, 2);
|
return round($this->weight / $this->servings, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ingredients list (ingredient amounts and separators).
|
||||||
|
*/
|
||||||
|
public function getIngredientsListAttribute(): Collection {
|
||||||
|
return new Collection([
|
||||||
|
...$this->ingredientAmounts,
|
||||||
|
...$this->ingredientSeparators,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the steps for this Recipe.
|
* Get the steps for this Recipe.
|
||||||
*/
|
*/
|
||||||
|
@ -165,6 +181,19 @@ final class Recipe extends Model implements HasMedia
|
||||||
return $this->hasMany(RecipeStep::class)->orderBy('number');
|
return $this->hasMany(RecipeStep::class)->orderBy('number');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get "separators" for the ingredients.
|
||||||
|
*
|
||||||
|
* Separators are used to adding headings or simple separations to the
|
||||||
|
* ingredients _list_ for a recipe. Their position is defined by weights
|
||||||
|
* compatible with ingredient weights.
|
||||||
|
*/
|
||||||
|
public function ingredientSeparators(): HasMany {
|
||||||
|
return $this->hasMany(RecipeSeparator::class)
|
||||||
|
->where('container', 'ingredients')
|
||||||
|
->orderBy('weight');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add nutrient calculations handling to overloading.
|
* Add nutrient calculations handling to overloading.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App\Models\RecipeSeparator
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int $recipe_id
|
||||||
|
* @property string $container
|
||||||
|
* @property int $weight
|
||||||
|
* @property string $text
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \App\Models\Recipe $recipe
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator whereContainer($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator whereCreatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator whereRecipeId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator whereText($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator whereUpdatedAt($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeSeparator whereWeight($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
final class RecipeSeparator extends Model
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'container',
|
||||||
|
'weight',
|
||||||
|
'text',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected $casts = [
|
||||||
|
'weight' => 'int',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Recipe this step belongs to.
|
||||||
|
*/
|
||||||
|
public function recipe(): BelongsTo {
|
||||||
|
return $this->belongsTo(Recipe::class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|RecipeStep whereStep($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeStep whereStep($value)
|
||||||
* @method static \Illuminate\Database\Eloquent\Builder|RecipeStep whereUpdatedAt($value)
|
* @method static \Illuminate\Database\Eloquent\Builder|RecipeStep whereUpdatedAt($value)
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
|
* @method static \Database\Factories\RecipeStepFactory factory(...$parameters)
|
||||||
*/
|
*/
|
||||||
final class RecipeStep extends Model
|
final class RecipeStep extends Model
|
||||||
{
|
{
|
||||||
|
@ -40,7 +41,7 @@ final class RecipeStep extends Model
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that should be cast.
|
* @inheritdoc
|
||||||
*/
|
*/
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'number' => 'int',
|
'number' => 'int',
|
||||||
|
|
|
@ -37,6 +37,7 @@ use Illuminate\Support\Facades\Auth;
|
||||||
* @mixin \Eloquent
|
* @mixin \Eloquent
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Goal[] $goals
|
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Goal[] $goals
|
||||||
* @property-read int|null $goals_count
|
* @property-read int|null $goals_count
|
||||||
|
* @method static \Database\Factories\UserFactory factory(...$parameters)
|
||||||
*/
|
*/
|
||||||
final class User extends Authenticatable
|
final class User extends Authenticatable
|
||||||
{
|
{
|
||||||
|
|
|
@ -42,6 +42,8 @@ class ArrayFormat
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
* The flipped array.
|
* The flipped array.
|
||||||
|
*
|
||||||
|
* @todo Return Collection instead of array.
|
||||||
*/
|
*/
|
||||||
public static function flipTwoDimensionalKeys(array $array): array {
|
public static function flipTwoDimensionalKeys(array $array): array {
|
||||||
$flipped = [];
|
$flipped = [];
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Recipe;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateRecipeSeparatorsTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('recipe_separators', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignIdFor(Recipe::class)->index()->constrained()->cascadeOnUpdate()->cascadeOnDelete();
|
||||||
|
$table->string('container')->index();
|
||||||
|
$table->unsignedInteger('weight')->index();
|
||||||
|
$table->longText('text')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('recipe_separators');
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,13 +5,13 @@
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shopify/draggable": "^1.0.0-beta.8",
|
"@shopify/draggable": "^1.0.0-beta.12",
|
||||||
"alpine-magic-helpers": "^1.1.0"
|
"alpine-magic-helpers": "^1.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.2.1",
|
"@tailwindcss/forms": "^0.2.1",
|
||||||
"@tailwindcss/typography": "^0.4.0",
|
"@tailwindcss/typography": "^0.4.0",
|
||||||
"alpinejs": "^2.8.1",
|
"alpinejs": "^2.8.2",
|
||||||
"autoprefixer": "^10.2.5",
|
"autoprefixer": "^10.2.5",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cross-env": "^7.0",
|
"cross-env": "^7.0",
|
||||||
|
@ -1138,9 +1138,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shopify/draggable": {
|
"node_modules/@shopify/draggable": {
|
||||||
"version": "1.0.0-beta.8",
|
"version": "1.0.0-beta.12",
|
||||||
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.8.tgz",
|
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.12.tgz",
|
||||||
"integrity": "sha512-9IeBPQM93Ad4qFKUopwuTClzoST/1OId4MaSd/8FB5ScCL2tl25UaOGNR8E2hjiL7xK4LN5+I1Ews6amS7YAiA=="
|
"integrity": "sha512-Un/Dn61sv2er9yjDXLGWMauCOWBb0BMbm0yzmmrD+oUX2/x50yhNJASTsCRdndUCpWlqYfZH8jEfaOgTPsKc/g=="
|
||||||
},
|
},
|
||||||
"node_modules/@tailwindcss/forms": {
|
"node_modules/@tailwindcss/forms": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
|
@ -2044,9 +2044,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/alpinejs": {
|
"node_modules/alpinejs": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.2.tgz",
|
||||||
"integrity": "sha512-ETJ/k0fbiBeP+OSd5Fhj70c+zb+YRzcVbyh5DVeLT3FBWMUeUvjOSWLi53IVLPSehaT2SKmB7w08WGF2jYTqNA=="
|
"integrity": "sha512-5yOUtckn4CBp0qsHpo2qgjZyZit84uXvHbB7NJ27sn4FA6UlFl2i9PGUAdTXkcbFvvxDJBM+zpOD8RuNYFvQAw=="
|
||||||
},
|
},
|
||||||
"node_modules/ansi-colors": {
|
"node_modules/ansi-colors": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
@ -13684,9 +13684,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@shopify/draggable": {
|
"@shopify/draggable": {
|
||||||
"version": "1.0.0-beta.8",
|
"version": "1.0.0-beta.12",
|
||||||
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.8.tgz",
|
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.12.tgz",
|
||||||
"integrity": "sha512-9IeBPQM93Ad4qFKUopwuTClzoST/1OId4MaSd/8FB5ScCL2tl25UaOGNR8E2hjiL7xK4LN5+I1Ews6amS7YAiA=="
|
"integrity": "sha512-Un/Dn61sv2er9yjDXLGWMauCOWBb0BMbm0yzmmrD+oUX2/x50yhNJASTsCRdndUCpWlqYfZH8jEfaOgTPsKc/g=="
|
||||||
},
|
},
|
||||||
"@tailwindcss/forms": {
|
"@tailwindcss/forms": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
|
@ -14494,9 +14494,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"alpinejs": {
|
"alpinejs": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.2.tgz",
|
||||||
"integrity": "sha512-ETJ/k0fbiBeP+OSd5Fhj70c+zb+YRzcVbyh5DVeLT3FBWMUeUvjOSWLi53IVLPSehaT2SKmB7w08WGF2jYTqNA=="
|
"integrity": "sha512-5yOUtckn4CBp0qsHpo2qgjZyZit84uXvHbB7NJ27sn4FA6UlFl2i9PGUAdTXkcbFvvxDJBM+zpOD8RuNYFvQAw=="
|
||||||
},
|
},
|
||||||
"ansi-colors": {
|
"ansi-colors": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.2.1",
|
"@tailwindcss/forms": "^0.2.1",
|
||||||
"@tailwindcss/typography": "^0.4.0",
|
"@tailwindcss/typography": "^0.4.0",
|
||||||
"alpinejs": "^2.8.1",
|
"alpinejs": "^2.8.2",
|
||||||
"autoprefixer": "^10.2.5",
|
"autoprefixer": "^10.2.5",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cross-env": "^7.0",
|
"cross-env": "^7.0",
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
"vue-template-compiler": "^2.6.12"
|
"vue-template-compiler": "^2.6.12"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shopify/draggable": "^1.0.0-beta.8",
|
"@shopify/draggable": "^1.0.0-beta.12",
|
||||||
"alpine-magic-helpers": "^1.1.0"
|
"alpine-magic-helpers": "^1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4743,7 +4743,7 @@
|
||||||
// event listeners are registered (the mutation observer will take care of them)
|
// event listeners are registered (the mutation observer will take care of them)
|
||||||
|
|
||||||
|
|
||||||
this.initializeElements(this.$el, () => {}, componentForClone ? false : true); // Use mutation observer to detect new elements being added within this component at run-time.
|
this.initializeElements(this.$el, () => {}, componentForClone); // Use mutation observer to detect new elements being added within this component at run-time.
|
||||||
// Alpine's just so darn flexible amirite?
|
// Alpine's just so darn flexible amirite?
|
||||||
|
|
||||||
this.listenForNewElementsToInitialize();
|
this.listenForNewElementsToInitialize();
|
||||||
|
@ -4832,15 +4832,15 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeElements(rootEl, extraVars = () => {}, shouldRegisterListeners = true) {
|
initializeElements(rootEl, extraVars = () => {}, componentForClone = false) {
|
||||||
this.walkAndSkipNestedComponents(rootEl, el => {
|
this.walkAndSkipNestedComponents(rootEl, el => {
|
||||||
// Don't touch spawns from for loop
|
// Don't touch spawns from for loop
|
||||||
if (el.__x_for_key !== undefined) return false; // Don't touch spawns from if directives
|
if (el.__x_for_key !== undefined) return false; // Don't touch spawns from if directives
|
||||||
|
|
||||||
if (el.__x_inserted_me !== undefined) return false;
|
if (el.__x_inserted_me !== undefined) return false;
|
||||||
this.initializeElement(el, extraVars, shouldRegisterListeners);
|
this.initializeElement(el, extraVars, componentForClone ? false : true);
|
||||||
}, el => {
|
}, el => {
|
||||||
el.__x = new Component(el);
|
if (!componentForClone) el.__x = new Component(el);
|
||||||
});
|
});
|
||||||
this.executeAndClearRemainingShowDirectiveStack();
|
this.executeAndClearRemainingShowDirectiveStack();
|
||||||
this.executeAndClearNextTickStack(rootEl);
|
this.executeAndClearNextTickStack(rootEl);
|
||||||
|
@ -5070,7 +5070,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const Alpine = {
|
const Alpine = {
|
||||||
version: "2.8.1",
|
version: "2.8.2",
|
||||||
pauseMutationObserver: false,
|
pauseMutationObserver: false,
|
||||||
magicProperties: {},
|
magicProperties: {},
|
||||||
onComponentInitializeds: [],
|
onComponentInitializeds: [],
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
||||||
<x-slot name="header">
|
<x-slot name="header">
|
||||||
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1>
|
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
<form method="POST" enctype="multipart/form-data" action="{{ ($recipe->exists ? route('recipes.update', $recipe) : route('recipes.store')) }}">
|
<form x-data method="POST" enctype="multipart/form-data" action="{{ ($recipe->exists ? route('recipes.update', $recipe) : route('recipes.store')) }}">
|
||||||
@if ($recipe->exists)@method('put')@endif
|
@if ($recipe->exists)@method('put')@endif
|
||||||
@csrf
|
@csrf
|
||||||
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
|
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
|
||||||
|
@ -126,19 +126,29 @@
|
||||||
<!-- Ingredients -->
|
<!-- Ingredients -->
|
||||||
<h3 class="mt-6 mb-2 font-extrabold text-lg">Ingredients</h3>
|
<h3 class="mt-6 mb-2 font-extrabold text-lg">Ingredients</h3>
|
||||||
<div x-data class="ingredients space-y-4">
|
<div x-data class="ingredients space-y-4">
|
||||||
@forelse($ingredients as $ingredient)
|
@forelse($ingredients_list->sortBy('weight') as $item)
|
||||||
@include('recipes.partials.ingredient-input', $ingredient)
|
@if($item['type'] === 'ingredient')
|
||||||
|
@include('recipes.partials.ingredient-input', $item)
|
||||||
|
@elseif($item['type'] === 'separator')
|
||||||
|
@include('recipes.partials.separator-input', $item)
|
||||||
|
@endif
|
||||||
@empty
|
@empty
|
||||||
@include('recipes.partials.ingredient-input')
|
@include('recipes.partials.ingredient-input')
|
||||||
@endforelse
|
@endforelse
|
||||||
<div class="entry-template hidden">
|
<div class="templates hidden">
|
||||||
@include('recipes.partials.ingredient-input')
|
<div class="ingredient-template">
|
||||||
|
@include('recipes.partials.ingredient-input')
|
||||||
|
</div>
|
||||||
|
<div class="separator-template">
|
||||||
|
@include('recipes.partials.separator-input')
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<x-inputs.icon-button type="button" color="green" x-on:click="addEntryNode($el);">
|
<x-inputs.button type="button" color="green" x-on:click="addNodeFromTemplate($el, 'ingredient');">
|
||||||
<svg class="h-10 w-10 pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
Add Ingredient
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
|
</x-inputs.button>
|
||||||
</svg>
|
<x-inputs.button type="button" color="blue" x-on:click="addNodeFromTemplate($el, 'separator');">
|
||||||
</x-inputs.icon-button>
|
Add Separator
|
||||||
|
</x-inputs.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Steps -->
|
<!-- Steps -->
|
||||||
|
@ -149,18 +159,18 @@
|
||||||
@empty
|
@empty
|
||||||
@include('recipes.partials.step-input')
|
@include('recipes.partials.step-input')
|
||||||
@endforelse
|
@endforelse
|
||||||
<div class="entry-template hidden">
|
<div class="templates hidden">
|
||||||
@include('recipes.partials.step-input')
|
<div class="step-template">
|
||||||
|
@include('recipes.partials.step-input')
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<x-inputs.icon-button type="button" color="green" x-on:click="addEntryNode($el);">
|
<x-inputs.button type="button" color="green" x-on:click="addNodeFromTemplate($el, 'step');">
|
||||||
<svg class="h-10 w-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
Add Step
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
|
</x-inputs.button>
|
||||||
</svg>
|
|
||||||
</x-inputs.icon-button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div x-data class="flex items-center justify-end mt-4">
|
<div class="flex items-center justify-end mt-4">
|
||||||
<x-inputs.button x-on:click="prepareForm();" class="ml-3">
|
<x-inputs.button x-on:click="prepareForm($el);" class="ml-3">
|
||||||
{{ ($recipe->exists ? 'Save' : 'Add') }}
|
{{ ($recipe->exists ? 'Save' : 'Add') }}
|
||||||
</x-inputs.button>
|
</x-inputs.button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -202,9 +212,9 @@
|
||||||
description.setContents(JSON.parse(document.querySelector('input[name="description_delta"]').value));
|
description.setContents(JSON.parse(document.querySelector('input[name="description_delta"]').value));
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
// Activate ingredient draggables.
|
// Activate ingredient sortable.
|
||||||
new Draggable.Sortable(document.querySelector('.ingredients'), {
|
const ingredientsSortable = new Draggable.Sortable(document.querySelector('.ingredients'), {
|
||||||
draggable: '.ingredient',
|
draggable: '.draggable',
|
||||||
handle: '.draggable-handle',
|
handle: '.draggable-handle',
|
||||||
mirror: {
|
mirror: {
|
||||||
appendTo: '.ingredients',
|
appendTo: '.ingredients',
|
||||||
|
@ -212,9 +222,18 @@
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Recalculate weight (order) of all ingredients.
|
||||||
|
ingredientsSortable.on('drag:stopped', (e) => {
|
||||||
|
Array.from(e.sourceContainer.children)
|
||||||
|
.filter(el => el.classList.contains('draggable'))
|
||||||
|
.forEach((el, index) => {
|
||||||
|
el.querySelector('input[name$="[weight][]"]').value = index;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
// Activate step draggables.
|
// Activate step draggables.
|
||||||
new Draggable.Sortable(document.querySelector('.steps'), {
|
new Draggable.Sortable(document.querySelector('.steps'), {
|
||||||
draggable: '.step',
|
draggable: '.draggable',
|
||||||
handle: '.draggable-handle',
|
handle: '.draggable-handle',
|
||||||
mirror: {
|
mirror: {
|
||||||
appendTo: '.steps',
|
appendTo: '.steps',
|
||||||
|
@ -224,29 +243,39 @@
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
/**
|
/**
|
||||||
* Adds a set of entry form fields from the template.
|
* Adds a node to ingredients or steps based on a template.
|
||||||
*
|
*
|
||||||
* @param {object} $el Entry lines parent element.
|
* @param {object} $el Parent element.
|
||||||
|
* @param {string} type Template type -- "ingredient", "separator", or "step".
|
||||||
*/
|
*/
|
||||||
let addEntryNode = ($el) => {
|
let addNodeFromTemplate = ($el, type) => {
|
||||||
// Create clone of template entry.
|
// Create clone of relevant template.
|
||||||
let template = $el.querySelector(':scope .entry-template');
|
const templates = $el.querySelector(`:scope .templates`);
|
||||||
let newEntry = template.cloneNode(true).firstElementChild;
|
const template = templates.querySelector(`:scope .${type}-template`);
|
||||||
|
const newNode = template.cloneNode(true).firstElementChild;
|
||||||
|
|
||||||
// Insert new entry before add button.
|
// Set weight based on previous sibling.
|
||||||
$el.insertBefore(newEntry, template);
|
const lastWeight = templates.previousElementSibling.querySelector('input[name$="[weight][]"]');
|
||||||
|
if (lastWeight && lastWeight.value) {
|
||||||
|
newNode.querySelector('input[name$="[weight][]"]').value = Number.parseInt(lastWeight.value) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert new node before templates.
|
||||||
|
$el.insertBefore(newNode, templates);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepare form values for submit.
|
* Prepare form values for submit.
|
||||||
|
*
|
||||||
|
* @param {object} $el Form element.
|
||||||
*/
|
*/
|
||||||
let prepareForm = () => {
|
let prepareForm = ($el) => {
|
||||||
// Remove any hidden templates before form submit.
|
// Remove any hidden templates before form submit.
|
||||||
document.querySelectorAll(':scope .entry-template').forEach(e => e.remove());
|
$el.querySelectorAll(':scope .templates').forEach(e => e.remove());
|
||||||
|
|
||||||
// Add description values to hidden fields.
|
// Add description values to hidden fields.
|
||||||
document.querySelector('input[name="description_delta"]').value = JSON.stringify(description.getContents());
|
$el.querySelector('input[name="description_delta"]').value = JSON.stringify(description.getContents());
|
||||||
document.querySelector('input[name="description"]').value = description.root.innerHTML
|
$el.querySelector('input[name="description"]').value = description.root.innerHTML
|
||||||
// Remove extraneous spaces from rendered result.
|
// Remove extraneous spaces from rendered result.
|
||||||
.replaceAll('<p><br></p>', '');
|
.replaceAll('<p><br></p>', '');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<div class="ingredient">
|
<div class="ingredient draggable">
|
||||||
<x-inputs.input type="hidden" name="ingredients[original_key][]" :value="$original_key ?? null" />
|
<x-inputs.input type="hidden" name="ingredients[key][]" :value="$key ?? null" />
|
||||||
|
<x-inputs.input type="hidden" name="ingredients[weight][]" :value="$weight ?? null" />
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0 w-full">
|
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0 w-full">
|
||||||
<div class="draggable-handle self-center text-gray-500 bg-gray-100 w-full md:w-auto p-2 cursor-move">
|
<div class="draggable-handle self-center text-gray-500 bg-gray-100 w-full md:w-auto p-2 cursor-move">
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="separator draggable">
|
||||||
|
<x-inputs.input type="hidden" name="separators[key][]" :value="$key ?? null" />
|
||||||
|
<x-inputs.input type="hidden" name="separators[weight][]" :value="$weight ?? null" />
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0 w-full">
|
||||||
|
<div class="draggable-handle self-center text-gray-500 bg-gray-100 w-full md:w-auto p-2 cursor-move">
|
||||||
|
<svg class="h-6 w-6 mx-auto" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<x-inputs.label for="source" value="Separator text" class="hidden" />
|
||||||
|
|
||||||
|
<x-inputs.input name="separators[text][]"
|
||||||
|
type="text"
|
||||||
|
placeholder="Separator text (optional)"
|
||||||
|
class="block w-full"
|
||||||
|
:value="$text ?? null" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-none">
|
||||||
|
<x-inputs.icon-button type="button"
|
||||||
|
color="red"
|
||||||
|
x-on:click="$event.target.parentNode.parentNode.parentNode.remove();">
|
||||||
|
<svg class="h-8 w-8 pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</x-inputs.icon-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="step">
|
<div class="step draggable">
|
||||||
<x-inputs.input type="hidden" name="steps[original_key][]" :value="$original_key ?? null" />
|
<x-inputs.input type="hidden" name="steps[key][]" :value="$key ?? null" />
|
||||||
<div class="flex flex-row mb-4 space-x-4">
|
<div class="flex flex-row mb-4 space-x-4">
|
||||||
<div class="draggable-handle self-center text-gray-500 bg-gray-100 p-2 cursor-move">
|
<div class="draggable-handle self-center text-gray-500 bg-gray-100 p-2 cursor-move">
|
||||||
<svg class="h-10 w-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-10 w-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
|
|
@ -52,23 +52,34 @@
|
||||||
</h1>
|
</h1>
|
||||||
<div class="prose prose-lg">
|
<div class="prose prose-lg">
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
@foreach($recipe->ingredientAmounts as $ia)
|
@foreach($recipe->ingredientsList->sortBy('weight') as $item)
|
||||||
<li>
|
@if($item::class === \App\Models\IngredientAmount::class)
|
||||||
<span>
|
<li>
|
||||||
{{ \App\Support\Number::fractionStringFromFloat($ia->amount) }}
|
<span>
|
||||||
@if($ia->unitFormatted){{ $ia->unitFormatted }}@endif
|
{{ \App\Support\Number::fractionStringFromFloat($item->amount) }}
|
||||||
@if($ia->ingredient->type === \App\Models\Recipe::class)
|
@if($item->unitFormatted){{ $item->unitFormatted }}@endif
|
||||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
@if($item->ingredient->type === \App\Models\Recipe::class)
|
||||||
href="{{ route('recipes.show', $ia->ingredient) }}">
|
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||||
{{ $ia->ingredient->name }}
|
href="{{ route('recipes.show', $item->ingredient) }}">
|
||||||
</a>
|
{{ $item->ingredient->name }}
|
||||||
@else
|
</a>
|
||||||
{{ $ia->ingredient->name }}@if($ia->ingredient->detail), {{ $ia->ingredient->detail }}@endif
|
@else
|
||||||
@endif
|
{{ $item->ingredient->name }}@if($item->ingredient->detail), {{ $item->ingredient->detail }}@endif
|
||||||
@if($ia->detail)<span class="text-gray-500">{{ $ia->detail }}</span>@endif
|
@endif
|
||||||
<div x-show="showNutrientsSummary" class="text-sm text-gray-500">{{ $ia->nutrients_summary }}</div>
|
@if($item->detail)<span class="text-gray-500">{{ $item->detail }}</span>@endif
|
||||||
</span>
|
<div x-show="showNutrientsSummary" class="text-sm text-gray-500">{{ $item->nutrients_summary }}</div>
|
||||||
</li>
|
</span>
|
||||||
|
</li>
|
||||||
|
@elseif($item::class === \App\Models\RecipeSeparator::class)
|
||||||
|
</ul></div>
|
||||||
|
@if($item->text)
|
||||||
|
<h2 class="mt-3 font-bold">{{ $item->text }}</h2>
|
||||||
|
@else
|
||||||
|
<hr class="mt-3 lg:w-1/2" />
|
||||||
|
@endif
|
||||||
|
<div class="prose prose-lg">
|
||||||
|
<ul class="space-y-2">
|
||||||
|
@endif
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue