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;
|
||||
|
||||
use App\Models\Food;
|
||||
use App\Models\IngredientAmount;
|
||||
use App\Models\Recipe;
|
||||
use App\Models\RecipeSeparator;
|
||||
use App\Models\RecipeStep;
|
||||
use App\Rules\ArrayNotEmpty;
|
||||
use App\Rules\StringIsDecimalOrFraction;
|
||||
|
@ -84,11 +84,10 @@ class RecipeController extends Controller
|
|||
$ingredients = [];
|
||||
if ($old = old('ingredients')) {
|
||||
foreach ($old['id'] as $key => $ingredient_id) {
|
||||
if (empty($ingredient_id)) {
|
||||
continue;
|
||||
}
|
||||
$ingredients[] = [
|
||||
'original_key' => $old['original_key'][$key],
|
||||
'type' => 'ingredient',
|
||||
'key' => $old['key'][$key],
|
||||
'weight' => $old['weight'][$key],
|
||||
'amount' => $old['amount'][$key],
|
||||
'unit' => $old['unit'][$key],
|
||||
'ingredient_id' => $ingredient_id,
|
||||
|
@ -101,7 +100,9 @@ class RecipeController extends Controller
|
|||
else {
|
||||
foreach ($recipe->ingredientAmounts as $key => $ingredientAmount) {
|
||||
$ingredients[] = [
|
||||
'original_key' => $key,
|
||||
'type' => 'ingredient',
|
||||
'key' => $key,
|
||||
'weight' => $ingredientAmount->weight,
|
||||
'amount' => $ingredientAmount->amount_formatted,
|
||||
'unit' => $ingredientAmount->unit,
|
||||
'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 = [];
|
||||
if ($old = old('steps')) {
|
||||
foreach ($old['step'] as $key => $step) {
|
||||
|
@ -119,7 +142,7 @@ class RecipeController extends Controller
|
|||
continue;
|
||||
}
|
||||
$steps[] = [
|
||||
'original_key' => $old['original_key'][$key],
|
||||
'key' => $old['key'][$key],
|
||||
'step_default' => $step,
|
||||
];
|
||||
}
|
||||
|
@ -127,7 +150,7 @@ class RecipeController extends Controller
|
|||
else {
|
||||
foreach ($recipe->steps as $key => $step) {
|
||||
$steps[] = [
|
||||
'original_key' => $key,
|
||||
'key' => $key,
|
||||
'step_default' => $step->step,
|
||||
];
|
||||
}
|
||||
|
@ -142,7 +165,7 @@ class RecipeController extends Controller
|
|||
return view('recipes.edit')
|
||||
->with('recipe', $recipe)
|
||||
->with('recipe_tags', $recipe_tags)
|
||||
->with('ingredients', $ingredients)
|
||||
->with('ingredients_list', new Collection([...$ingredients, ...$separators]))
|
||||
->with('steps', $steps)
|
||||
->with('ingredients_units', Nutrients::$units);
|
||||
}
|
||||
|
@ -175,15 +198,24 @@ class RecipeController extends Controller
|
|||
'ingredients.unit' => ['required', 'array'],
|
||||
'ingredients.unit.*' => ['required_with:ingredients.id.*'],
|
||||
'ingredients.detail' => ['required', 'array'],
|
||||
'ingredients.detail.*' => 'nullable|string',
|
||||
'ingredients.detail.*' => ['nullable', 'string'],
|
||||
'ingredients.id' => ['required', 'array', new ArrayNotEmpty],
|
||||
'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable',
|
||||
'ingredients.type' => ['required', 'array', new ArrayNotEmpty],
|
||||
'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.*' => 'nullable|string',
|
||||
'steps.original_key' => 'nullable|array',
|
||||
'steps.step.*' => ['nullable', 'string'],
|
||||
'steps.key' => ['nullable', 'array'],
|
||||
]);
|
||||
|
||||
// Validate that no ingredients are recursive.
|
||||
|
@ -208,53 +240,9 @@ class RecipeController extends Controller
|
|||
try {
|
||||
DB::transaction(function () use ($recipe, $input) {
|
||||
$recipe->saveOrFail();
|
||||
|
||||
// Delete any removed ingredients.
|
||||
$removed = array_diff($recipe->ingredientAmounts->keys()->all(), $input['ingredients']['original_key']);
|
||||
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);
|
||||
$this->updateIngredients($recipe, $input);
|
||||
$this->updateIngredientSeparators($recipe, $input);
|
||||
$this->updateSteps($recipe, $input);
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
@ -289,6 +277,114 @@ class RecipeController extends Controller
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -72,6 +72,7 @@ use Spatie\Tags\HasTags;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|Food withAnyTagsOfAnyType($tags)
|
||||
* @mixin \Eloquent
|
||||
* @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
|
||||
{
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Support\Nutrients;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
|
@ -37,8 +36,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
*/
|
||||
final class Goal extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Supported options for thr frequency attribute.
|
||||
*/
|
||||
|
|
|
@ -43,6 +43,7 @@ use Illuminate\Support\Pluralizer;
|
|||
* @mixin \Eloquent
|
||||
* @property-read string|null $unit_formatted
|
||||
* @property-read string $nutrients_summary
|
||||
* @method static \Database\Factories\IngredientAmountFactory factory(...$parameters)
|
||||
*/
|
||||
final class IngredientAmount extends Model
|
||||
{
|
||||
|
|
|
@ -48,6 +48,7 @@ use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
|||
* @mixin \Eloquent
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\IngredientAmount[] $ingredients
|
||||
* @property-read int|null $ingredients_count
|
||||
* @method static \Database\Factories\JournalEntryFactory factory(...$parameters)
|
||||
*/
|
||||
final class JournalEntry extends Model
|
||||
{
|
||||
|
|
|
@ -10,6 +10,7 @@ use ElasticScoutDriverPlus\QueryDsl;
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Scout\Searchable;
|
||||
use Spatie\Image\Manipulations;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
|
@ -69,6 +70,11 @@ use Spatie\Tags\HasTags;
|
|||
* @method static \Illuminate\Database\Eloquent\Builder|Recipe whereDescriptionDelta($value)
|
||||
* @property-read \Spatie\MediaLibrary\MediaCollections\Models\Collections\MediaCollection|Media[] $media
|
||||
* @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
|
||||
{
|
||||
|
@ -158,6 +164,16 @@ final class Recipe extends Model implements HasMedia
|
|||
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.
|
||||
*/
|
||||
|
@ -165,6 +181,19 @@ final class Recipe extends Model implements HasMedia
|
|||
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.
|
||||
*
|
||||
|
|
|
@ -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 whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
* @method static \Database\Factories\RecipeStepFactory factory(...$parameters)
|
||||
*/
|
||||
final class RecipeStep extends Model
|
||||
{
|
||||
|
@ -40,7 +41,7 @@ final class RecipeStep extends Model
|
|||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be cast.
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected $casts = [
|
||||
'number' => 'int',
|
||||
|
|
|
@ -37,6 +37,7 @@ use Illuminate\Support\Facades\Auth;
|
|||
* @mixin \Eloquent
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\Goal[] $goals
|
||||
* @property-read int|null $goals_count
|
||||
* @method static \Database\Factories\UserFactory factory(...$parameters)
|
||||
*/
|
||||
final class User extends Authenticatable
|
||||
{
|
||||
|
|
|
@ -42,6 +42,8 @@ class ArrayFormat
|
|||
*
|
||||
* @return array
|
||||
* The flipped array.
|
||||
*
|
||||
* @todo Return Collection instead of array.
|
||||
*/
|
||||
public static function flipTwoDimensionalKeys(array $array): array {
|
||||
$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": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@shopify/draggable": "^1.0.0-beta.8",
|
||||
"@shopify/draggable": "^1.0.0-beta.12",
|
||||
"alpine-magic-helpers": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.2.1",
|
||||
"@tailwindcss/typography": "^0.4.0",
|
||||
"alpinejs": "^2.8.1",
|
||||
"alpinejs": "^2.8.2",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"axios": "^0.21.1",
|
||||
"cross-env": "^7.0",
|
||||
|
@ -1138,9 +1138,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@shopify/draggable": {
|
||||
"version": "1.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-9IeBPQM93Ad4qFKUopwuTClzoST/1OId4MaSd/8FB5ScCL2tl25UaOGNR8E2hjiL7xK4LN5+I1Ews6amS7YAiA=="
|
||||
"version": "1.0.0-beta.12",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.12.tgz",
|
||||
"integrity": "sha512-Un/Dn61sv2er9yjDXLGWMauCOWBb0BMbm0yzmmrD+oUX2/x50yhNJASTsCRdndUCpWlqYfZH8jEfaOgTPsKc/g=="
|
||||
},
|
||||
"node_modules/@tailwindcss/forms": {
|
||||
"version": "0.2.1",
|
||||
|
@ -2044,9 +2044,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/alpinejs": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.1.tgz",
|
||||
"integrity": "sha512-ETJ/k0fbiBeP+OSd5Fhj70c+zb+YRzcVbyh5DVeLT3FBWMUeUvjOSWLi53IVLPSehaT2SKmB7w08WGF2jYTqNA=="
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.2.tgz",
|
||||
"integrity": "sha512-5yOUtckn4CBp0qsHpo2qgjZyZit84uXvHbB7NJ27sn4FA6UlFl2i9PGUAdTXkcbFvvxDJBM+zpOD8RuNYFvQAw=="
|
||||
},
|
||||
"node_modules/ansi-colors": {
|
||||
"version": "4.1.1",
|
||||
|
@ -13684,9 +13684,9 @@
|
|||
}
|
||||
},
|
||||
"@shopify/draggable": {
|
||||
"version": "1.0.0-beta.8",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.8.tgz",
|
||||
"integrity": "sha512-9IeBPQM93Ad4qFKUopwuTClzoST/1OId4MaSd/8FB5ScCL2tl25UaOGNR8E2hjiL7xK4LN5+I1Ews6amS7YAiA=="
|
||||
"version": "1.0.0-beta.12",
|
||||
"resolved": "https://registry.npmjs.org/@shopify/draggable/-/draggable-1.0.0-beta.12.tgz",
|
||||
"integrity": "sha512-Un/Dn61sv2er9yjDXLGWMauCOWBb0BMbm0yzmmrD+oUX2/x50yhNJASTsCRdndUCpWlqYfZH8jEfaOgTPsKc/g=="
|
||||
},
|
||||
"@tailwindcss/forms": {
|
||||
"version": "0.2.1",
|
||||
|
@ -14494,9 +14494,9 @@
|
|||
}
|
||||
},
|
||||
"alpinejs": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.1.tgz",
|
||||
"integrity": "sha512-ETJ/k0fbiBeP+OSd5Fhj70c+zb+YRzcVbyh5DVeLT3FBWMUeUvjOSWLi53IVLPSehaT2SKmB7w08WGF2jYTqNA=="
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-2.8.2.tgz",
|
||||
"integrity": "sha512-5yOUtckn4CBp0qsHpo2qgjZyZit84uXvHbB7NJ27sn4FA6UlFl2i9PGUAdTXkcbFvvxDJBM+zpOD8RuNYFvQAw=="
|
||||
},
|
||||
"ansi-colors": {
|
||||
"version": "4.1.1",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.2.1",
|
||||
"@tailwindcss/typography": "^0.4.0",
|
||||
"alpinejs": "^2.8.1",
|
||||
"alpinejs": "^2.8.2",
|
||||
"autoprefixer": "^10.2.5",
|
||||
"axios": "^0.21.1",
|
||||
"cross-env": "^7.0",
|
||||
|
@ -25,7 +25,7 @@
|
|||
"vue-template-compiler": "^2.6.12"
|
||||
},
|
||||
"dependencies": {
|
||||
"@shopify/draggable": "^1.0.0-beta.8",
|
||||
"@shopify/draggable": "^1.0.0-beta.12",
|
||||
"alpine-magic-helpers": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4743,7 +4743,7 @@
|
|||
// 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?
|
||||
|
||||
this.listenForNewElementsToInitialize();
|
||||
|
@ -4832,15 +4832,15 @@
|
|||
});
|
||||
}
|
||||
|
||||
initializeElements(rootEl, extraVars = () => {}, shouldRegisterListeners = true) {
|
||||
initializeElements(rootEl, extraVars = () => {}, componentForClone = false) {
|
||||
this.walkAndSkipNestedComponents(rootEl, el => {
|
||||
// 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_inserted_me !== undefined) return false;
|
||||
this.initializeElement(el, extraVars, shouldRegisterListeners);
|
||||
this.initializeElement(el, extraVars, componentForClone ? false : true);
|
||||
}, el => {
|
||||
el.__x = new Component(el);
|
||||
if (!componentForClone) el.__x = new Component(el);
|
||||
});
|
||||
this.executeAndClearRemainingShowDirectiveStack();
|
||||
this.executeAndClearNextTickStack(rootEl);
|
||||
|
@ -5070,7 +5070,7 @@
|
|||
}
|
||||
|
||||
const Alpine = {
|
||||
version: "2.8.1",
|
||||
version: "2.8.2",
|
||||
pauseMutationObserver: false,
|
||||
magicProperties: {},
|
||||
onComponentInitializeds: [],
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,7 @@
|
|||
<x-slot name="header">
|
||||
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1>
|
||||
</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
|
||||
@csrf
|
||||
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
|
||||
|
@ -126,19 +126,29 @@
|
|||
<!-- Ingredients -->
|
||||
<h3 class="mt-6 mb-2 font-extrabold text-lg">Ingredients</h3>
|
||||
<div x-data class="ingredients space-y-4">
|
||||
@forelse($ingredients as $ingredient)
|
||||
@include('recipes.partials.ingredient-input', $ingredient)
|
||||
@forelse($ingredients_list->sortBy('weight') as $item)
|
||||
@if($item['type'] === 'ingredient')
|
||||
@include('recipes.partials.ingredient-input', $item)
|
||||
@elseif($item['type'] === 'separator')
|
||||
@include('recipes.partials.separator-input', $item)
|
||||
@endif
|
||||
@empty
|
||||
@include('recipes.partials.ingredient-input')
|
||||
@endforelse
|
||||
<div class="entry-template hidden">
|
||||
@include('recipes.partials.ingredient-input')
|
||||
<div class="templates hidden">
|
||||
<div class="ingredient-template">
|
||||
@include('recipes.partials.ingredient-input')
|
||||
</div>
|
||||
<div class="separator-template">
|
||||
@include('recipes.partials.separator-input')
|
||||
</div>
|
||||
</div>
|
||||
<x-inputs.icon-button type="button" color="green" x-on:click="addEntryNode($el);">
|
||||
<svg class="h-10 w-10 pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
</x-inputs.icon-button>
|
||||
<x-inputs.button type="button" color="green" x-on:click="addNodeFromTemplate($el, 'ingredient');">
|
||||
Add Ingredient
|
||||
</x-inputs.button>
|
||||
<x-inputs.button type="button" color="blue" x-on:click="addNodeFromTemplate($el, 'separator');">
|
||||
Add Separator
|
||||
</x-inputs.button>
|
||||
</div>
|
||||
|
||||
<!-- Steps -->
|
||||
|
@ -149,18 +159,18 @@
|
|||
@empty
|
||||
@include('recipes.partials.step-input')
|
||||
@endforelse
|
||||
<div class="entry-template hidden">
|
||||
@include('recipes.partials.step-input')
|
||||
<div class="templates hidden">
|
||||
<div class="step-template">
|
||||
@include('recipes.partials.step-input')
|
||||
</div>
|
||||
</div>
|
||||
<x-inputs.icon-button type="button" color="green" x-on:click="addEntryNode($el);">
|
||||
<svg class="h-10 w-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<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" />
|
||||
</svg>
|
||||
</x-inputs.icon-button>
|
||||
<x-inputs.button type="button" color="green" x-on:click="addNodeFromTemplate($el, 'step');">
|
||||
Add Step
|
||||
</x-inputs.button>
|
||||
</div>
|
||||
|
||||
<div x-data class="flex items-center justify-end mt-4">
|
||||
<x-inputs.button x-on:click="prepareForm();" class="ml-3">
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<x-inputs.button x-on:click="prepareForm($el);" class="ml-3">
|
||||
{{ ($recipe->exists ? 'Save' : 'Add') }}
|
||||
</x-inputs.button>
|
||||
</div>
|
||||
|
@ -202,9 +212,9 @@
|
|||
description.setContents(JSON.parse(document.querySelector('input[name="description_delta"]').value));
|
||||
} catch (e) {}
|
||||
|
||||
// Activate ingredient draggables.
|
||||
new Draggable.Sortable(document.querySelector('.ingredients'), {
|
||||
draggable: '.ingredient',
|
||||
// Activate ingredient sortable.
|
||||
const ingredientsSortable = new Draggable.Sortable(document.querySelector('.ingredients'), {
|
||||
draggable: '.draggable',
|
||||
handle: '.draggable-handle',
|
||||
mirror: {
|
||||
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.
|
||||
new Draggable.Sortable(document.querySelector('.steps'), {
|
||||
draggable: '.step',
|
||||
draggable: '.draggable',
|
||||
handle: '.draggable-handle',
|
||||
mirror: {
|
||||
appendTo: '.steps',
|
||||
|
@ -224,29 +243,39 @@
|
|||
</script>
|
||||
<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) => {
|
||||
// Create clone of template entry.
|
||||
let template = $el.querySelector(':scope .entry-template');
|
||||
let newEntry = template.cloneNode(true).firstElementChild;
|
||||
let addNodeFromTemplate = ($el, type) => {
|
||||
// Create clone of relevant template.
|
||||
const templates = $el.querySelector(`:scope .templates`);
|
||||
const template = templates.querySelector(`:scope .${type}-template`);
|
||||
const newNode = template.cloneNode(true).firstElementChild;
|
||||
|
||||
// Insert new entry before add button.
|
||||
$el.insertBefore(newEntry, template);
|
||||
// Set weight based on previous sibling.
|
||||
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.
|
||||
*
|
||||
* @param {object} $el Form element.
|
||||
*/
|
||||
let prepareForm = () => {
|
||||
let prepareForm = ($el) => {
|
||||
// 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.
|
||||
document.querySelector('input[name="description_delta"]').value = JSON.stringify(description.getContents());
|
||||
document.querySelector('input[name="description"]').value = description.root.innerHTML
|
||||
$el.querySelector('input[name="description_delta"]').value = JSON.stringify(description.getContents());
|
||||
$el.querySelector('input[name="description"]').value = description.root.innerHTML
|
||||
// Remove extraneous spaces from rendered result.
|
||||
.replaceAll('<p><br></p>', '');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<div class="ingredient">
|
||||
<x-inputs.input type="hidden" name="ingredients[original_key][]" :value="$original_key ?? null" />
|
||||
<div class="ingredient draggable">
|
||||
<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 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">
|
||||
|
|
|
@ -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">
|
||||
<x-inputs.input type="hidden" name="steps[original_key][]" :value="$original_key ?? null" />
|
||||
<div class="step draggable">
|
||||
<x-inputs.input type="hidden" name="steps[key][]" :value="$key ?? null" />
|
||||
<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">
|
||||
<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>
|
||||
<div class="prose prose-lg">
|
||||
<ul class="space-y-2">
|
||||
@foreach($recipe->ingredientAmounts as $ia)
|
||||
<li>
|
||||
<span>
|
||||
{{ \App\Support\Number::fractionStringFromFloat($ia->amount) }}
|
||||
@if($ia->unitFormatted){{ $ia->unitFormatted }}@endif
|
||||
@if($ia->ingredient->type === \App\Models\Recipe::class)
|
||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
href="{{ route('recipes.show', $ia->ingredient) }}">
|
||||
{{ $ia->ingredient->name }}
|
||||
</a>
|
||||
@else
|
||||
{{ $ia->ingredient->name }}@if($ia->ingredient->detail), {{ $ia->ingredient->detail }}@endif
|
||||
@endif
|
||||
@if($ia->detail)<span class="text-gray-500">{{ $ia->detail }}</span>@endif
|
||||
<div x-show="showNutrientsSummary" class="text-sm text-gray-500">{{ $ia->nutrients_summary }}</div>
|
||||
</span>
|
||||
</li>
|
||||
@foreach($recipe->ingredientsList->sortBy('weight') as $item)
|
||||
@if($item::class === \App\Models\IngredientAmount::class)
|
||||
<li>
|
||||
<span>
|
||||
{{ \App\Support\Number::fractionStringFromFloat($item->amount) }}
|
||||
@if($item->unitFormatted){{ $item->unitFormatted }}@endif
|
||||
@if($item->ingredient->type === \App\Models\Recipe::class)
|
||||
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
||||
href="{{ route('recipes.show', $item->ingredient) }}">
|
||||
{{ $item->ingredient->name }}
|
||||
</a>
|
||||
@else
|
||||
{{ $item->ingredient->name }}@if($item->ingredient->detail), {{ $item->ingredient->detail }}@endif
|
||||
@endif
|
||||
@if($item->detail)<span class="text-gray-500">{{ $item->detail }}</span>@endif
|
||||
<div x-show="showNutrientsSummary" class="text-sm text-gray-500">{{ $item->nutrients_summary }}</div>
|
||||
</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
|
||||
</ul>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue