diff --git a/.env.local.example b/.env.local.example index a8d3835..38797f4 100644 --- a/.env.local.example +++ b/.env.local.example @@ -3,7 +3,7 @@ APP_NAME=kcal APP_ENV=local APP_KEY= APP_DEBUG=true -APP_URL=http://kcal.test +APP_URL=http://127.0.0.1 APP_PORT=8080 APP_SERVICE=app APP_TIMEZONE=UTC @@ -12,7 +12,7 @@ LOG_CHANNEL=stack LOG_LEVEL=debug DB_CONNECTION=mysql -DB_HOST=mysql +DB_HOST=db DB_PORT=3306 DB_DATABASE=kcal DB_USERNAME=kcal diff --git a/app/Http/Controllers/FoodController.php b/app/Http/Controllers/FoodController.php index 176b604..20c53d2 100644 --- a/app/Http/Controllers/FoodController.php +++ b/app/Http/Controllers/FoodController.php @@ -2,13 +2,12 @@ namespace App\Http\Controllers; +use App\Http\Requests\UpdateFoodRequest; use App\Models\Food; -use App\Rules\StringIsDecimalOrFraction; use App\Support\Number; use App\Support\Nutrients; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -35,7 +34,7 @@ class FoodController extends Controller /** * Store a newly created resource in storage. */ - public function store(Request $request): RedirectResponse + public function store(UpdateFoodRequest $request): RedirectResponse { return $this->update($request, new Food()); } @@ -72,25 +71,10 @@ class FoodController extends Controller /** * Update the specified resource in storage. */ - public function update(Request $request, Food $food): RedirectResponse + public function update(UpdateFoodRequest $request, Food $food): RedirectResponse { - $attributes = $request->validate([ - 'name' => 'required|string', - 'detail' => 'nullable|string', - 'brand' => 'nullable|string', - 'source' => 'nullable|string', - 'notes' => 'nullable|string', - 'serving_size' => ['required', new StringIsDecimalOrFraction], - 'serving_unit' => 'nullable|string', - 'serving_unit_name' => 'nullable|string', - 'serving_weight' => 'required|numeric', - 'calories' => 'nullable|numeric', - 'fat' => 'nullable|numeric', - 'cholesterol' => 'nullable|numeric', - 'sodium' => 'nullable|numeric', - 'carbohydrates' => 'nullable|numeric', - 'protein' => 'nullable|numeric', - ]); + $attributes = $request->validated(); + $attributes['serving_size'] = Number::floatFromString($attributes['serving_size']); $attributes['name'] = Str::lower($attributes['name']); diff --git a/app/Http/Controllers/GoalController.php b/app/Http/Controllers/GoalController.php index fd6b0ad..15a7a6b 100644 --- a/app/Http/Controllers/GoalController.php +++ b/app/Http/Controllers/GoalController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Http\Requests\UpdateGoalRequest; use App\Models\Goal; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; @@ -40,7 +41,7 @@ class GoalController extends Controller /** * Store a newly created resource in storage. */ - public function store(Request $request): RedirectResponse + public function store(UpdateGoalRequest $request): RedirectResponse { return $this->update($request, new Goal()); } @@ -70,15 +71,9 @@ class GoalController extends Controller /** * Update the specified resource in storage. */ - public function update(Request $request, Goal $goal): RedirectResponse + public function update(UpdateGoalRequest $request, Goal $goal): RedirectResponse { - $attributes = $request->validate([ - 'from' => ['nullable', 'date'], - 'to' => ['nullable', 'date'], - 'frequency' => ['required', 'string'], - 'name' => ['required', 'string'], - 'goal' => ['required', 'numeric'], - ]); + $attributes = $request->validated(); $goal->fill($attributes)->user()->associate(Auth::user()); $goal->save(); session()->flash('message', "Goal updated!"); diff --git a/app/Http/Controllers/JournalEntryController.php b/app/Http/Controllers/JournalEntryController.php index 2f279dc..166e73b 100644 --- a/app/Http/Controllers/JournalEntryController.php +++ b/app/Http/Controllers/JournalEntryController.php @@ -5,13 +5,11 @@ namespace App\Http\Controllers; +use App\Http\Requests\StoreFromNutrientsJournalEntryRequest; +use App\Http\Requests\StoreJournalEntryRequest; use App\Models\Food; use App\Models\JournalEntry; use App\Models\Recipe; -use App\Rules\ArrayNotEmpty; -use App\Rules\InArray; -use App\Rules\StringIsDecimalOrFraction; -use App\Rules\UsesIngredientTrait; use App\Support\ArrayFormat; use App\Support\Number; use App\Support\Nutrients; @@ -85,6 +83,7 @@ class JournalEntryController extends Controller continue; } $ingredients[$key] = [ + 'key' => $key, 'date' => $old['date'][$key], 'meal' => $old['meal'][$key], 'amount' => $amount, @@ -130,28 +129,9 @@ class JournalEntryController extends Controller /** * Store a newly created resource in storage. */ - public function store(Request $request): RedirectResponse + public function store(StoreJournalEntryRequest $request): RedirectResponse { - $input = $request->validate([ - 'ingredients.date' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.date.*' => ['nullable', 'date', 'required_with:ingredients.id.*'], - 'ingredients.meal' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.meal.*' => [ - 'nullable', - 'string', - 'required_with:ingredients.id.*', - new InArray(JournalEntry::meals()->pluck('value')->toArray()) - ], - 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction], - 'ingredients.unit' => ['required', 'array'], - 'ingredients.unit.*' => ['required_with:ingredients.id.*'], - '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()], - 'group_entries' => ['nullable', 'boolean'], - ]); + $input = $request->validated(); $ingredients = ArrayFormat::flipTwoDimensionalKeys($input['ingredients']); @@ -284,23 +264,8 @@ class JournalEntryController extends Controller /** * Store an entry from nutrients. */ - public function storeFromNutrients(Request $request): RedirectResponse { - $attributes = $request->validate([ - 'date' => ['required', 'date'], - 'meal' => [ - 'required', - 'string', - new InArray(JournalEntry::meals()->pluck('value')->toArray()) - ], - 'summary' => ['required', 'string'], - 'calories' => ['nullable', 'required_without_all:fat,cholesterol,sodium,carbohydrates,protein', 'numeric'], - 'fat' => ['nullable', 'required_without_all:calories,cholesterol,sodium,carbohydrates,protein', 'numeric'], - 'cholesterol' => ['nullable', 'required_without_all:calories,fat,sodium,carbohydrates,protein', 'numeric'], - 'sodium' => ['nullable', 'required_without_all:calories,fat,cholesterol,carbohydrates,protein', 'numeric'], - 'carbohydrates' => ['nullable', 'required_without_all:calories,fat,cholesterol,sodium,protein', 'numeric'], - 'protein' => ['nullable', 'required_without_all:calories,fat,cholesterol,sodium,carbohydrates', 'numeric'], - ]); - + public function storeFromNutrients(StoreFromNutrientsJournalEntryRequest $request): RedirectResponse { + $attributes = $request->validated(); $entry = JournalEntry::make(array_filter($attributes)) ->user()->associate(Auth::user()); $entry->save(); diff --git a/app/Http/Controllers/RecipeController.php b/app/Http/Controllers/RecipeController.php index 30aefe3..a0dde29 100644 --- a/app/Http/Controllers/RecipeController.php +++ b/app/Http/Controllers/RecipeController.php @@ -2,19 +2,16 @@ namespace App\Http\Controllers; +use App\Http\Requests\UpdateRecipeRequest; 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; -use App\Rules\UsesIngredientTrait; use App\Support\Number; use App\Support\Nutrients; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; -use Illuminate\Http\Request; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; @@ -44,12 +41,9 @@ class RecipeController extends Controller /** * Store a newly created resource in storage. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\RedirectResponse - * * @throws \Throwable */ - public function store(Request $request): RedirectResponse + public function store(UpdateRecipeRequest $request): RedirectResponse { return $this->update($request, new Recipe()); } @@ -96,18 +90,18 @@ class RecipeController extends Controller 'ingredient_name' => $old['name'][$key], 'detail' => $old['detail'][$key], ]; - } - // Add supported units for the ingredient. - $ingredient = NULL; - if ($ingredients[$key]['ingredient_type'] === Food::class) { - $ingredient = Food::whereId($ingredients[$key]['ingredient_id'])->first(); - } - elseif ($ingredients[$key]['ingredient_type'] === Recipe::class) { - $ingredient = Recipe::whereId($ingredients[$key]['ingredient_id'])->first(); - } - if ($ingredient) { - $ingredients[$key]['units_supported'] = $ingredient->units_supported; + // Add supported units for the ingredient. + $ingredient = NULL; + if ($ingredients[$key]['ingredient_type'] === Food::class) { + $ingredient = Food::whereId($ingredients[$key]['ingredient_id'])->first(); + } + elseif ($ingredients[$key]['ingredient_type'] === Recipe::class) { + $ingredient = Recipe::whereId($ingredients[$key]['ingredient_id'])->first(); + } + if ($ingredient) { + $ingredients[$key]['units_supported'] = $ingredient->units_supported; + } } } else { @@ -187,50 +181,11 @@ class RecipeController extends Controller /** * Update the specified resource in storage. * - * @param \Illuminate\Http\Request $request - * @param \App\Models\Recipe $recipe - * - * @return \Illuminate\Http\RedirectResponse - * * @throws \Throwable */ - public function update(Request $request, Recipe $recipe): RedirectResponse + public function update(UpdateRecipeRequest $request, Recipe $recipe): RedirectResponse { - $input = $request->validate([ - 'name' => ['required', 'string'], - 'description' => ['nullable', 'string'], - 'description_delta' => ['nullable', 'string'], - 'image' => ['nullable', 'file', 'mimes:jpg,png,gif'], - 'remove_image' => ['nullable', 'boolean'], - 'servings' => ['required', 'numeric'], - 'time_prep' => ['nullable', 'numeric'], - 'time_cook' => ['nullable', 'numeric'], - 'weight' => ['nullable', 'numeric'], - 'source' => ['nullable', 'string'], - 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], - 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsDecimalOrFraction], - 'ingredients.unit' => ['required', 'array'], - 'ingredients.unit.*' => ['required_with:ingredients.id.*'], - 'ingredients.detail' => ['required', 'array'], - '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.key' => ['nullable', 'array'], - '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.key' => ['nullable', 'array'], - ]); + $input = $request->validated(); // Validate that no ingredients are recursive. // TODO: refactor as custom validator. @@ -246,6 +201,7 @@ class RecipeController extends Controller 'description_delta' => $input['description_delta'], 'servings' => (int) $input['servings'], 'weight' => $input['weight'], + 'volume' => Number::floatFromString($input['volume']), 'time_prep' => (int) $input['time_prep'], 'time_cook' => (int) $input['time_cook'], 'source' => $input['source'], @@ -285,7 +241,7 @@ class RecipeController extends Controller ->usingFileName("{$recipe->slug}.{$file->extension()}") ->toMediaCollection(); } - elseif (isset($input['remove_image']) && (bool) $input['remove_image']) { + elseif (isset($input['remove_image']) && $input['remove_image']) { $recipe->clearMediaCollection(); } diff --git a/app/Http/Requests/StoreFromNutrientsJournalEntryRequest.php b/app/Http/Requests/StoreFromNutrientsJournalEntryRequest.php new file mode 100644 index 0000000..8fe450a --- /dev/null +++ b/app/Http/Requests/StoreFromNutrientsJournalEntryRequest.php @@ -0,0 +1,36 @@ + ['required', 'date'], + 'meal' => [ + 'required', + 'string', + new InArray(JournalEntry::meals()->pluck('value')->toArray()) + ], + 'summary' => ['required', 'string'], + 'calories' => ['nullable', 'numeric', 'min:0', 'required_without_all:fat,cholesterol,sodium,carbohydrates,protein'], + 'fat' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,cholesterol,sodium,carbohydrates,protein'], + 'cholesterol' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,sodium,carbohydrates,protein'], + 'sodium' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,carbohydrates,protein'], + 'carbohydrates' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,sodium,protein'], + 'protein' => ['nullable', 'numeric', 'min:0', 'required_without_all:calories,fat,cholesterol,sodium,carbohydrates'], + ]; + } +} diff --git a/app/Http/Requests/StoreJournalEntryRequest.php b/app/Http/Requests/StoreJournalEntryRequest.php new file mode 100644 index 0000000..3d55538 --- /dev/null +++ b/app/Http/Requests/StoreJournalEntryRequest.php @@ -0,0 +1,57 @@ + ['required', 'array', new ArrayNotEmpty], + 'ingredients.date.*' => ['nullable', 'date', 'required_with:ingredients.id.*'], + 'ingredients.meal' => ['required', 'array', new ArrayNotEmpty], + 'ingredients.meal.*' => [ + 'nullable', + 'string', + 'required_with:ingredients.id.*', + new InArray(JournalEntry::meals()->pluck('value')->toArray()) + ], + 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], + 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsPositiveDecimalOrFraction], + 'ingredients.unit' => ['required', 'array'], + 'ingredients.unit.*' => ['required_with:ingredients.id.*'], + 'ingredients.id.*' => 'required_with:ingredients.amount.*|nullable', + 'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()], + 'group_entries' => ['nullable', 'boolean'], + ]; + } + + /** + * @inheritdoc + */ + public function attributes(): array + { + return [ + 'ingredients.amount' => 'amount', + 'ingredients.amount.*' => 'amount', + 'ingredients.date' => 'date', + 'ingredients.date.*' => 'date', + 'ingredients.id.*' => 'item', + 'ingredients.meal' => 'meal', + 'ingredients.meal.*' => 'meal', + 'ingredients.unit' => 'unit', + 'ingredients.unit.*' => 'unit', + ]; + } +} diff --git a/app/Http/Requests/UpdateFoodRequest.php b/app/Http/Requests/UpdateFoodRequest.php new file mode 100644 index 0000000..6ad2c14 --- /dev/null +++ b/app/Http/Requests/UpdateFoodRequest.php @@ -0,0 +1,35 @@ + ['required', 'string'], + 'detail' => ['nullable', 'string'], + 'brand' => ['nullable', 'string'], + 'source' => ['nullable', 'string'], + 'notes' => ['nullable', 'string'], + 'serving_size' => ['required', new StringIsPositiveDecimalOrFraction], + 'serving_unit' => ['nullable', 'string'], + 'serving_unit_name' => ['nullable', 'string'], + 'serving_weight' => ['required', 'numeric', 'min:0'], + 'calories' => ['nullable', 'numeric', 'min:0'], + 'fat' => ['nullable', 'numeric', 'min:0'], + 'cholesterol' => ['nullable', 'numeric', 'min:0'], + 'sodium' => ['nullable', 'numeric', 'min:0'], + 'carbohydrates' => ['nullable', 'numeric', 'min:0'], + 'protein' => ['nullable', 'numeric', 'min:0'], + ]; + } + +} diff --git a/app/Http/Requests/UpdateGoalRequest.php b/app/Http/Requests/UpdateGoalRequest.php new file mode 100644 index 0000000..8e8146d --- /dev/null +++ b/app/Http/Requests/UpdateGoalRequest.php @@ -0,0 +1,25 @@ + ['nullable', 'date'], + 'to' => ['nullable', 'date'], + 'frequency' => ['required', 'string'], + 'name' => ['required', 'string'], + 'goal' => ['required', 'numeric', 'min:0'], + ]; + } + +} diff --git a/app/Http/Requests/UpdateRecipeRequest.php b/app/Http/Requests/UpdateRecipeRequest.php new file mode 100644 index 0000000..49f3357 --- /dev/null +++ b/app/Http/Requests/UpdateRecipeRequest.php @@ -0,0 +1,80 @@ + ['required', 'string'], + 'description' => ['nullable', 'string'], + 'description_delta' => ['nullable', 'string'], + 'image' => ['nullable', 'file', 'mimes:jpg,png,gif'], + 'remove_image' => ['nullable', 'boolean'], + 'servings' => ['required', 'numeric'], + 'time_prep' => ['nullable', 'numeric'], + 'time_cook' => ['nullable', 'numeric'], + 'weight' => ['nullable', 'numeric', 'min:0'], + 'volume' => ['nullable', new StringIsPositiveDecimalOrFraction], + 'source' => ['nullable', 'string'], + 'ingredients.amount' => ['required', 'array', new ArrayNotEmpty], + 'ingredients.amount.*' => ['required_with:ingredients.id.*', 'nullable', new StringIsPositiveDecimalOrFraction], + 'ingredients.unit' => ['required', 'array'], + 'ingredients.unit.*' => ['required_with:ingredients.id.*'], + 'ingredients.detail' => ['required', 'array'], + 'ingredients.detail.*' => ['nullable', 'string'], + 'ingredients.id' => ['required', 'array', new ArrayNotEmpty], + 'ingredients.id.*' => ['required_with:ingredients.amount.*', 'required_with:ingredients.unit.*', 'nullable'], + 'ingredients.type' => ['required', 'array', new ArrayNotEmpty], + 'ingredients.type.*' => ['required_with:ingredients.id.*', 'nullable', new UsesIngredientTrait()], + 'ingredients.key' => ['nullable', 'array'], + '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.key' => ['nullable', 'array'], + ]; + } + + /** + * @inheritdoc + */ + public function messages(): array + { + return [ + 'ingredients.id.*.required_with' => 'Missing :attribute in Ingredients.', + 'ingredients.amount.*.required_with' => 'Missing :attribute in Ingredients.', + 'ingredients.unit.*.required_with' => 'Missing :attribute in Ingredients.', + ]; + } + + /** + * @inheritdoc + */ + public function attributes(): array + { + return [ + 'ingredients.id.*' => 'ingredient name', + 'ingredients.amount.*' => 'ingredient amount', + 'ingredients.unit.*' => 'ingredient unit', + ]; + } + +} diff --git a/app/JsonApi/Schemas/RecipeSchema.php b/app/JsonApi/Schemas/RecipeSchema.php index b45b80e..a57b27d 100644 --- a/app/JsonApi/Schemas/RecipeSchema.php +++ b/app/JsonApi/Schemas/RecipeSchema.php @@ -36,6 +36,8 @@ class RecipeSchema extends SchemaProvider 'servings' => $resource->servings, 'weight' => $resource->weight, 'serving_weight' => $resource->serving_weight, + 'volume' => $resource->volume, + 'volumeFormatted' => $resource->volume_formatted, 'units_supported' => $resource->units_supported->pluck('label'), 'caloriesPerServing' => $resource->caloriesPerServing(), 'carbohydratesPerServing' => $resource->carbohydratesPerServing(), diff --git a/app/Models/Goal.php b/app/Models/Goal.php index 2bd35a2..3c66e9e 100644 --- a/app/Models/Goal.php +++ b/app/Models/Goal.php @@ -6,6 +6,7 @@ use App\Support\Nutrients; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Str; /** * App\Models\Goal @@ -44,7 +45,10 @@ final class Goal extends Model * Supported options for thr frequency attribute. */ public static array $frequencyOptions = [ - 'daily' => ['value' => 'daily', 'label' => 'daily'], + 'daily' => [ + 'value' => 'daily', + 'label' => 'daily' + ], ]; /** @@ -94,7 +98,7 @@ final class Goal extends Model foreach (Nutrients::all() as $nutrient) { $options[$nutrient['value']] = [ 'value' => $nutrient['value'], - 'label' => $nutrient['label'], + 'label' => Str::ucfirst($nutrient['label']), 'unit' => $nutrient['unit'], ]; } diff --git a/app/Models/Recipe.php b/app/Models/Recipe.php index 309d2b5..b856e4e 100644 --- a/app/Models/Recipe.php +++ b/app/Models/Recipe.php @@ -7,6 +7,7 @@ use App\Models\Traits\Ingredient; use App\Models\Traits\Journalable; use App\Models\Traits\Sluggable; use App\Models\Traits\Taggable; +use App\Support\Number; use App\Support\Nutrients; use ElasticScoutDriverPlus\QueryDsl; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -79,6 +80,9 @@ use Spatie\MediaLibrary\MediaCollections\Models\Media; * @property-read \Illuminate\Database\Eloquent\Collection|\App\Models\RecipeSeparator[] $separators * @property-read int|null $separators_count * @property-read Collection $units_supported + * @property float|null $volume + * @property-read string|null $volume_formatted + * @method static \Illuminate\Database\Eloquent\Builder|Recipe whereVolume($value) */ final class Recipe extends Model implements HasMedia { @@ -104,6 +108,7 @@ final class Recipe extends Model implements HasMedia 'source', 'servings', 'weight', + 'volume', ]; /** @@ -114,6 +119,7 @@ final class Recipe extends Model implements HasMedia 'time_prep' => 'int', 'time_cook' => 'int', 'weight' => 'float', + 'volume' => 'float', ]; /** @@ -133,6 +139,7 @@ final class Recipe extends Model implements HasMedia */ protected $appends = [ 'serving_weight', + 'volume_formatted', 'time_total', 'units_supported' ]; @@ -169,6 +176,17 @@ final class Recipe extends Model implements HasMedia return round($this->weight / $this->servings); } + /** + * Get the volume as a formatted string (e.g. 0.5 = 1/2). + */ + public function getVolumeFormattedAttribute(): ?string { + $result = null; + if (!empty($this->volume)) { + $result = Number::rationalStringFromFloat($this->volume); + } + return $result; + } + /** * Get the ingredients list (ingredient amounts and separators). */ diff --git a/app/Models/Traits/Ingredient.php b/app/Models/Traits/Ingredient.php index a81c339..81f5c2c 100644 --- a/app/Models/Traits/Ingredient.php +++ b/app/Models/Traits/Ingredient.php @@ -65,6 +65,9 @@ trait Ingredient if (!empty($this->serving_weight)) { $supported = $supported->merge($units->where('type', 'weight')); } + if (isset($this->volume) && !empty($this->volume)) { + $supported = $supported->merge($units->where('type', 'volume')); + } return $supported->sortBy('label'); } } diff --git a/app/Rules/StringIsDecimalOrFraction.php b/app/Rules/StringIsPositiveDecimalOrFraction.php similarity index 76% rename from app/Rules/StringIsDecimalOrFraction.php rename to app/Rules/StringIsPositiveDecimalOrFraction.php index 9a0c70e..b875606 100644 --- a/app/Rules/StringIsDecimalOrFraction.php +++ b/app/Rules/StringIsPositiveDecimalOrFraction.php @@ -5,7 +5,7 @@ namespace App\Rules; use App\Support\Number; use Illuminate\Contracts\Validation\Rule; -class StringIsDecimalOrFraction implements Rule +class StringIsPositiveDecimalOrFraction implements Rule { /** * {@inheritdoc} @@ -14,7 +14,7 @@ class StringIsDecimalOrFraction implements Rule { try { $result = Number::floatFromString($value); - return $result != 0; + return $result > 0; } catch (\InvalidArgumentException $e) { // Allow to pass through, method will return false. @@ -27,6 +27,6 @@ class StringIsDecimalOrFraction implements Rule */ public function message(): string { - return 'The :attribute must be a decimal or fraction.'; + return 'The :attribute must be a positive decimal or fraction.'; } } diff --git a/app/Support/Nutrients.php b/app/Support/Nutrients.php index 4daa135..24ec687 100644 --- a/app/Support/Nutrients.php +++ b/app/Support/Nutrients.php @@ -175,6 +175,8 @@ class Nutrients /** * Calculate a nutrient amount for a recipe. + * + * Weight base unit is grams, volume base unit is cups. */ public static function calculateRecipeNutrientAmount( Recipe $recipe, @@ -182,18 +184,19 @@ class Nutrients float $amount, string $fromUnit ): float { - if ($fromUnit === 'oz') { - return $amount * self::$gramsPerOunce / $recipe->weight * $recipe->{"{$nutrient}Total"}(); - } - elseif ($fromUnit === 'serving') { + if ($fromUnit === 'serving') { + // Use "per serving" methods directly. return $recipe->{"{$nutrient}PerServing"}() * $amount; } - elseif ($fromUnit === 'gram') { - return $amount / $recipe->weight * $recipe->{"{$nutrient}Total"}(); - } - else { - throw new \DomainException("Unsupported recipe unit: {$fromUnit}"); - } + $multiplier = match ($fromUnit) { + 'oz' => $amount * self::$gramsPerOunce / $recipe->weight, + 'gram' => $amount / $recipe->weight, + 'tsp' => $amount / 48 / $recipe->volume, + 'tbsp' => $amount / 16 / $recipe->volume, + 'cup' => $amount / $recipe->volume, + default => throw new \DomainException("Unsupported recipe unit: {$fromUnit}"), + }; + return $multiplier * $recipe->{"{$nutrient}Total"}(); } /** diff --git a/app/View/Components/Inputs/Select.php b/app/View/Components/Inputs/Select.php index 0f26a23..c687727 100644 --- a/app/View/Components/Inputs/Select.php +++ b/app/View/Components/Inputs/Select.php @@ -8,20 +8,20 @@ use Illuminate\View\Component; class Select extends Component { + public ?bool $hasError; public Collection|array $options; public ?string $selectedValue; /** * Select constructor. - * - * @param \Illuminate\Support\Collection|array $options - * @param ?string $selectedValue */ public function __construct( Collection|array $options, + ?bool $hasError = false, ?string $selectedValue = '', ) { $this->options = $options; + $this->hasError = $hasError; $this->selectedValue = $selectedValue; } @@ -29,6 +29,7 @@ class Select extends Component { return view('components.inputs.select') ->with('options', $this->options) + ->with('hasError', $this->hasError) ->with('selectedValue', $this->selectedValue); } diff --git a/database/Factories/RecipeFactory.php b/database/Factories/RecipeFactory.php index 95dea44..bcef573 100644 --- a/database/Factories/RecipeFactory.php +++ b/database/Factories/RecipeFactory.php @@ -23,6 +23,7 @@ class RecipeFactory extends Factory public function definition(): array { $description = htmlspecialchars($this->faker->realText(500)); + $volumes = [1/4, 1/3, 1/2, 2/3, 3/4, 1, 1 + 1/2, 1 + 3/4, 2, 2 + 1/2, 3, 3 + 1/2, 4, 5]; return [ 'name' => Words::randomWords(Arr::random(['npan', 'npn', 'anpn'])), 'description' => "

{$description}

", @@ -31,7 +32,8 @@ class RecipeFactory extends Factory 'time_cook' => $this->faker->numberBetween(0, 90), 'source' => $this->faker->optional()->url, 'servings' => $this->faker->numberBetween(1, 10), - 'weight' => $this->faker->randomFloat(1, 60, 2000), + 'weight' => $this->faker->optional()->randomFloat(1, 60, 2000), + 'volume' => $this->faker->optional()->randomElement($volumes), 'tags' => Words::randomWords(Arr::random(['a', 'aa', 'aaa']), TRUE), ]; } diff --git a/database/migrations/2020_12_21_215932_create_recipes_table.php b/database/migrations/2020_12_21_215932_create_recipes_table.php index 7e7884f..43b827c 100644 --- a/database/migrations/2020_12_21_215932_create_recipes_table.php +++ b/database/migrations/2020_12_21_215932_create_recipes_table.php @@ -24,6 +24,7 @@ class CreateRecipesTable extends Migration $table->string('source')->nullable(); $table->unsignedInteger('servings'); $table->unsignedFloat('weight')->nullable(); + //$table->decimal('volume', 10, 8)->unsigned()->nullable(); $table->timestamps(); }); } diff --git a/database/migrations/2021_04_17_200123_add_volume_to_recipes.php b/database/migrations/2021_04_17_200123_add_volume_to_recipes.php new file mode 100644 index 0000000..8bbeb32 --- /dev/null +++ b/database/migrations/2021_04_17_200123_add_volume_to_recipes.php @@ -0,0 +1,32 @@ +decimal('volume', 10, 8)->unsigned()->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('recipes', function (Blueprint $table) { + $table->dropColumn('volume'); + }); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index f603791..b9d525a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: image: phpmyadmin restart: always ports: - - 8080:80 + - 8081:80 environment: PMA_HOST: db MYSQL_ROOT_PASSWORD: '${DB_PASSWORD:-kcal}' diff --git a/public/css/app.css b/public/css/app.css index 3e07f75..714aa26 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -1,3 +1,3 @@ /*! tailwindcss v2.0.4 | MIT License | https://tailwindcss.com*/ -/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */:root{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder, textarea::-moz-placeholder{color:#9ca3af}input:-ms-input-placeholder, textarea:-ms-input-placeholder{color:#9ca3af}input::placeholder,textarea::placeholder{color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[type=date],[type=number],[type=password],[type=search],[type=text],[type=time],[type=url],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem}[type=date]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,select:focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent);border-color:#2563eb}input::-moz-placeholder, textarea::-moz-placeholder{color:#6b7280;opacity:1}input:-ms-input-placeholder, textarea:-ms-input-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;color-adjust:exact}[type=checkbox]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0}[type=checkbox]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent)}[type=checkbox]:checked{background-size:100% 100%;background-position:50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E")}[type=checkbox]:checked,[type=checkbox]:checked:focus,[type=checkbox]:checked:hover{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto -webkit-focus-ring-color}.prose{color:#374151;max-width:65ch}.prose [class~=lead]{color:#4b5563;font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose a{color:#111827;text-decoration:underline;font-weight:500}.prose strong{color:#111827;font-weight:600}.prose ol[type=a]{--list-counter-style:lower-alpha}.prose ol[type=i]{--list-counter-style:lower-roman}.prose ol[type="1"]{--list-counter-style:decimal}.prose ol>li{position:relative;padding-left:1.75em}.prose ol>li:before{content:counter(list-item,var(--list-counter-style,decimal)) ".";position:absolute;font-weight:400;color:#6b7280;left:0}.prose ul>li{position:relative;padding-left:1.75em}.prose ul>li:before{content:"";position:absolute;background-color:#d1d5db;border-radius:50%;width:.375em;height:.375em;top:.6875em;left:.25em}.prose hr{border-color:#e5e7eb;border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose blockquote{font-weight:500;font-style:italic;color:#111827;border-left-width:.25rem;border-left-color:#e5e7eb;quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose blockquote p:first-of-type:before{content:open-quote}.prose blockquote p:last-of-type:after{content:close-quote}.prose h1{color:#111827;font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose h2{color:#111827;font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose h3{font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose h3,.prose h4{color:#111827;font-weight:600}.prose h4{margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose figure figcaption{color:#6b7280;font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose code{color:#111827;font-weight:600;font-size:.875em}.prose code:after,.prose code:before{content:"`"}.prose a code{color:#111827}.prose pre{color:#e5e7eb;background-color:#1f2937;overflow-x:auto;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose pre code{background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:400;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose pre code:after,.prose pre code:before{content:none}.prose table{width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose thead{color:#111827;font-weight:600;border-bottom-width:1px;border-bottom-color:#d1d5db}.prose thead th{vertical-align:bottom;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose tbody tr{border-bottom-width:1px;border-bottom-color:#e5e7eb}.prose tbody tr:last-child{border-bottom-width:0}.prose tbody td{vertical-align:top;padding:.5714286em}.prose{font-size:1rem;line-height:1.75}.prose p{margin-top:1.25em;margin-bottom:1.25em}.prose figure,.prose img,.prose video{margin-top:2em;margin-bottom:2em}.prose figure>*{margin-top:0;margin-bottom:0}.prose h2 code{font-size:.875em}.prose h3 code{font-size:.9em}.prose ol,.prose ul{margin-top:1.25em;margin-bottom:1.25em}.prose li{margin-top:.5em;margin-bottom:.5em}.prose>ul>li p{margin-top:.75em;margin-bottom:.75em}.prose>ul>li>:first-child{margin-top:1.25em}.prose>ul>li>:last-child{margin-bottom:1.25em}.prose>ol>li>:first-child{margin-top:1.25em}.prose>ol>li>:last-child{margin-bottom:1.25em}.prose ol ol,.prose ol ul,.prose ul ol,.prose ul ul{margin-top:.75em;margin-bottom:.75em}.prose h2+*,.prose h3+*,.prose h4+*,.prose hr+*{margin-top:0}.prose thead th:first-child{padding-left:0}.prose thead th:last-child{padding-right:0}.prose tbody td:first-child{padding-left:0}.prose tbody td:last-child{padding-right:0}.prose>:first-child{margin-top:0}.prose>:last-child{margin-bottom:0}.prose-lg{font-size:1.125rem;line-height:1.7777778}.prose-lg p{margin-top:1.3333333em;margin-bottom:1.3333333em}.prose-lg [class~=lead]{font-size:1.2222222em;line-height:1.4545455;margin-top:1.0909091em;margin-bottom:1.0909091em}.prose-lg blockquote{margin-top:1.6666667em;margin-bottom:1.6666667em;padding-left:1em}.prose-lg h1{font-size:2.6666667em;margin-top:0;margin-bottom:.8333333em;line-height:1}.prose-lg h2{font-size:1.6666667em;margin-top:1.8666667em;margin-bottom:1.0666667em;line-height:1.3333333}.prose-lg h3{font-size:1.3333333em;margin-top:1.6666667em;margin-bottom:.6666667em;line-height:1.5}.prose-lg h4{margin-top:1.7777778em;margin-bottom:.4444444em;line-height:1.5555556}.prose-lg figure,.prose-lg img,.prose-lg video{margin-top:1.7777778em;margin-bottom:1.7777778em}.prose-lg figure>*{margin-top:0;margin-bottom:0}.prose-lg figure figcaption{font-size:.8888889em;line-height:1.5;margin-top:1em}.prose-lg code{font-size:.8888889em}.prose-lg h2 code{font-size:.8666667em}.prose-lg h3 code{font-size:.875em}.prose-lg pre{font-size:.8888889em;line-height:1.75;margin-top:2em;margin-bottom:2em;border-radius:.375rem;padding:1em 1.5em}.prose-lg ol,.prose-lg ul{margin-top:1.3333333em;margin-bottom:1.3333333em}.prose-lg li{margin-top:.6666667em;margin-bottom:.6666667em}.prose-lg ol>li{padding-left:1.6666667em}.prose-lg ol>li:before{left:0}.prose-lg ul>li{padding-left:1.6666667em}.prose-lg ul>li:before{width:.3333333em;height:.3333333em;top:.72222em;left:.2222222em}.prose-lg>ul>li p{margin-top:.8888889em;margin-bottom:.8888889em}.prose-lg>ul>li>:first-child{margin-top:1.3333333em}.prose-lg>ul>li>:last-child{margin-bottom:1.3333333em}.prose-lg>ol>li>:first-child{margin-top:1.3333333em}.prose-lg>ol>li>:last-child{margin-bottom:1.3333333em}.prose-lg ol ol,.prose-lg ol ul,.prose-lg ul ol,.prose-lg ul ul{margin-top:.8888889em;margin-bottom:.8888889em}.prose-lg hr{margin-top:3.1111111em;margin-bottom:3.1111111em}.prose-lg h2+*,.prose-lg h3+*,.prose-lg h4+*,.prose-lg hr+*{margin-top:0}.prose-lg table{font-size:.8888889em;line-height:1.5}.prose-lg thead th{padding-right:.75em;padding-bottom:.75em;padding-left:.75em}.prose-lg thead th:first-child{padding-left:0}.prose-lg thead th:last-child{padding-right:0}.prose-lg tbody td{padding:.75em}.prose-lg tbody td:first-child{padding-left:0}.prose-lg tbody td:last-child{padding-right:0}.prose-lg>:first-child{margin-top:0}.prose-lg>:last-child{margin-bottom:0}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.5rem*var(--tw-space-y-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.5rem*var(--tw-space-x-reverse));margin-left:calc(0.5rem*(1 - var(--tw-space-x-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(2rem*var(--tw-space-x-reverse));margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)))}.bg-clip-border{background-clip:border-box}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.bg-white{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75,85,99,var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgba(254,202,202,var(--tw-bg-opacity))}.bg-red-800{--tw-bg-opacity:1;background-color:rgba(153,27,27,var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgba(167,243,208,var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgba(16,185,129,var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgba(6,95,70,var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgba(30,64,175,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgba(209,213,219,var(--tw-bg-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185,28,28,var(--tw-bg-opacity))}.hover\:bg-yellow-300:hover{--tw-bg-opacity:1;background-color:rgba(252,211,77,var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgba(4,120,87,var(--tw-bg-opacity))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgba(29,78,216,var(--tw-bg-opacity))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgba(79,70,229,var(--tw-bg-opacity))}.focus\:bg-gray-100:focus{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gradient-to-r{background-image:linear-gradient(90deg,var(--tw-gradient-stops))}.from-red-400{--tw-gradient-from:#f87171;--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to,rgba(248,113,113,0))}.to-blue-500{--tw-gradient-to:#3b82f6}.bg-center{background-position:50%}.bg-no-repeat{background-repeat:no-repeat}.bg-cover{background-size:cover}.border-transparent{border-color:transparent}.border-black{--tw-border-opacity:1;border-color:rgba(0,0,0,var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243,244,246,var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(156,163,175,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-blue-100{--tw-border-opacity:1;border-color:rgba(219,234,254,var(--tw-border-opacity))}.border-blue-200{--tw-border-opacity:1;border-color:rgba(191,219,254,var(--tw-border-opacity))}.border-indigo-400{--tw-border-opacity:1;border-color:rgba(129,140,248,var(--tw-border-opacity))}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.hover\:border-red-300:hover{--tw-border-opacity:1;border-color:rgba(252,165,165,var(--tw-border-opacity))}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.focus\:border-gray-900:focus{--tw-border-opacity:1;border-color:rgba(17,24,39,var(--tw-border-opacity))}.focus\:border-red-900:focus{--tw-border-opacity:1;border-color:rgba(127,29,29,var(--tw-border-opacity))}.focus\:border-green-900:focus{--tw-border-opacity:1;border-color:rgba(6,78,59,var(--tw-border-opacity))}.focus\:border-blue-900:focus{--tw-border-opacity:1;border-color:rgba(30,58,138,var(--tw-border-opacity))}.focus\:border-indigo-300:focus{--tw-border-opacity:1;border-color:rgba(165,180,252,var(--tw-border-opacity))}.focus\:border-indigo-700:focus{--tw-border-opacity:1;border-color:rgba(67,56,202,var(--tw-border-opacity))}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.border-2{border-width:2px}.border{border-width:1px}.border-b-0{border-bottom-width:0}.border-b-2{border-bottom-width:2px}.border-t-4{border-top-width:4px}.border-b-4{border-bottom-width:4px}.border-t-8{border-top-width:8px}.border-b-8{border-bottom-width:8px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-move{cursor:move}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.inline-grid{display:inline-grid}.hidden{display:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.self-center{align-self:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-none{flex:none}.float-right{float:right}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.font-normal{font-weight:400}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-black{font-weight:900}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-32{height:8rem}.h-full{height:100%}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.leading-5{line-height:1.25rem}.leading-7{line-height:1.75rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.leading-snug{line-height:1.375}.leading-relaxed{line-height:1.625}.leading-loose{line-height:2}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.m-1{margin:.25rem}.m-auto{margin:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-1{margin-top:.25rem}.mr-1{margin-right:.25rem}.mb-1{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mt-3{margin-top:.75rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-xs{max-width:20rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.max-w-3xl{max-width:48rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.min-h-screen{min-height:100vh}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-25{opacity:.25}.opacity-100{opacity:1}.disabled\:opacity-25:disabled{opacity:.25}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.overflow-hidden{overflow:hidden}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-6{padding:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pl-2{padding-left:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-12{padding-top:3rem}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.top-0{top:0}.right-0{right:0}.left-0{left:0}*{--tw-shadow:0 0 transparent}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,0.05)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px 0 rgba(0,0,0,0.06)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -1px rgba(0,0,0,0.06)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05)}.shadow-none{--tw-shadow:0 0 transparent;box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}*{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring:focus,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent)}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgba(0,0,0,var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgba(209,213,219,var(--tw-ring-opacity))}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgba(252,165,165,var(--tw-ring-opacity))}.ring-green-300{--tw-ring-opacity:1;--tw-ring-color:rgba(110,231,183,var(--tw-ring-opacity))}.ring-green-600{--tw-ring-opacity:1;--tw-ring-color:rgba(5,150,105,var(--tw-ring-opacity))}.ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgba(147,197,253,var(--tw-ring-opacity))}.focus\:ring-indigo-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(199,210,254,var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.focus\:ring-opacity-50:focus{--tw-ring-opacity:0.5}.fill-current{fill:currentColor}.text-center{text-align:center}.text-right{text-align:right}.text-transparent{color:transparent}.text-white{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgba(209,213,219,var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgba(31,41,55,var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17,24,39,var(--tw-text-opacity))}.text-red-100{--tw-text-opacity:1;color:rgba(254,226,226,var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239,68,68,var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgba(153,27,27,var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245,158,11,var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16,185,129,var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgba(17,24,39,var(--tw-text-opacity))}.hover\:text-red-700:hover{--tw-text-opacity:1;color:rgba(185,28,28,var(--tw-text-opacity))}.hover\:text-green-700:hover{--tw-text-opacity:1;color:rgba(4,120,87,var(--tw-text-opacity))}.focus\:text-gray-700:focus{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.italic{font-style:italic}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-48{width:12rem}.w-auto{width:auto}.w-full{width:100%}.z-40{z-index:40}.z-50{z-index:50}.gap-4{gap:1rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-1{row-gap:.25rem}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.origin-top{transform-origin:top}.origin-top-right{transform-origin:top right}.origin-top-left{transform-origin:top left}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-75{transition-duration:75ms}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}[x-cloak]{display:none!important}@media (min-width:640px){.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-center{justify-content:center}.sm\:h-48{height:12rem}.sm\:mt-0{margin-top:0}.sm\:ml-6{margin-left:1.5rem}.sm\:max-w-xs{max-width:20rem}.sm\:max-w-md{max-width:28rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:w-auto{width:auto}.sm\:w-3\/5{width:60%}.sm\:w-5\/12{width:41.666667%}}@media (min-width:768px){.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:h-64{height:16rem}.md\:text-9xl{font-size:8rem;line-height:1}.md\:mt-0{margin-top:0}.md\:w-72{width:18rem}.md\:w-auto{width:auto}.md\:w-2\/3{width:66.666667%}.md\:w-1\/4{width:25%}.md\:w-3\/4{width:75%}.md\:w-5\/6{width:83.333333%}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:h-96{height:24rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:w-1\/2{width:50%}.lg\:w-3\/4{width:75%}.lg\:w-4\/12{width:33.333333%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} +/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */:root{-moz-tab-size:4;-o-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=submit],button{-webkit-appearance:button}legend{padding:0}progress{vertical-align:baseline}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset,ol,ul{margin:0;padding:0}ol,ul{list-style:none}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{font-family:inherit;line-height:inherit}*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::-moz-placeholder, textarea::-moz-placeholder{color:#9ca3af}input:-ms-input-placeholder, textarea:-ms-input-placeholder{color:#9ca3af}input::placeholder,textarea::placeholder{color:#9ca3af}button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[type=date],[type=number],[type=password],[type=search],[type=text],[type=time],[type=url],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem}[type=date]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,select:focus,textarea:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent);border-color:#2563eb}input::-moz-placeholder, textarea::-moz-placeholder{color:#6b7280;opacity:1}input:-ms-input-placeholder, textarea:-ms-input-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;color-adjust:exact}[type=checkbox]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#2563eb;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0}[type=checkbox]:focus{outline:2px solid transparent;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent)}[type=checkbox]:checked{background-size:100% 100%;background-position:50%;background-repeat:no-repeat;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E")}[type=checkbox]:checked,[type=checkbox]:checked:focus,[type=checkbox]:checked:hover{border-color:transparent;background-color:currentColor}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");border-color:transparent;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:transparent;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto -webkit-focus-ring-color}.prose{color:#374151;max-width:65ch}.prose [class~=lead]{color:#4b5563;font-size:1.25em;line-height:1.6;margin-top:1.2em;margin-bottom:1.2em}.prose a{color:#111827;text-decoration:underline;font-weight:500}.prose strong{color:#111827;font-weight:600}.prose ol[type=a]{--list-counter-style:lower-alpha}.prose ol[type=i]{--list-counter-style:lower-roman}.prose ol[type="1"]{--list-counter-style:decimal}.prose ol>li{position:relative;padding-left:1.75em}.prose ol>li:before{content:counter(list-item,var(--list-counter-style,decimal)) ".";position:absolute;font-weight:400;color:#6b7280;left:0}.prose ul>li{position:relative;padding-left:1.75em}.prose ul>li:before{content:"";position:absolute;background-color:#d1d5db;border-radius:50%;width:.375em;height:.375em;top:.6875em;left:.25em}.prose hr{border-color:#e5e7eb;border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose blockquote{font-weight:500;font-style:italic;color:#111827;border-left-width:.25rem;border-left-color:#e5e7eb;quotes:"\201C""\201D""\2018""\2019";margin-top:1.6em;margin-bottom:1.6em;padding-left:1em}.prose blockquote p:first-of-type:before{content:open-quote}.prose blockquote p:last-of-type:after{content:close-quote}.prose h1{color:#111827;font-weight:800;font-size:2.25em;margin-top:0;margin-bottom:.8888889em;line-height:1.1111111}.prose h2{color:#111827;font-weight:700;font-size:1.5em;margin-top:2em;margin-bottom:1em;line-height:1.3333333}.prose h3{font-size:1.25em;margin-top:1.6em;margin-bottom:.6em;line-height:1.6}.prose h3,.prose h4{color:#111827;font-weight:600}.prose h4{margin-top:1.5em;margin-bottom:.5em;line-height:1.5}.prose figure figcaption{color:#6b7280;font-size:.875em;line-height:1.4285714;margin-top:.8571429em}.prose code{color:#111827;font-weight:600;font-size:.875em}.prose code:after,.prose code:before{content:"`"}.prose a code{color:#111827}.prose pre{color:#e5e7eb;background-color:#1f2937;overflow-x:auto;font-size:.875em;line-height:1.7142857;margin-top:1.7142857em;margin-bottom:1.7142857em;border-radius:.375rem;padding:.8571429em 1.1428571em}.prose pre code{background-color:transparent;border-width:0;border-radius:0;padding:0;font-weight:400;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}.prose pre code:after,.prose pre code:before{content:none}.prose table{width:100%;table-layout:auto;text-align:left;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.7142857}.prose thead{color:#111827;font-weight:600;border-bottom-width:1px;border-bottom-color:#d1d5db}.prose thead th{vertical-align:bottom;padding-right:.5714286em;padding-bottom:.5714286em;padding-left:.5714286em}.prose tbody tr{border-bottom-width:1px;border-bottom-color:#e5e7eb}.prose tbody tr:last-child{border-bottom-width:0}.prose tbody td{vertical-align:top;padding:.5714286em}.prose{font-size:1rem;line-height:1.75}.prose p{margin-top:1.25em;margin-bottom:1.25em}.prose figure,.prose img,.prose video{margin-top:2em;margin-bottom:2em}.prose figure>*{margin-top:0;margin-bottom:0}.prose h2 code{font-size:.875em}.prose h3 code{font-size:.9em}.prose ol,.prose ul{margin-top:1.25em;margin-bottom:1.25em}.prose li{margin-top:.5em;margin-bottom:.5em}.prose>ul>li p{margin-top:.75em;margin-bottom:.75em}.prose>ul>li>:first-child{margin-top:1.25em}.prose>ul>li>:last-child{margin-bottom:1.25em}.prose>ol>li>:first-child{margin-top:1.25em}.prose>ol>li>:last-child{margin-bottom:1.25em}.prose ol ol,.prose ol ul,.prose ul ol,.prose ul ul{margin-top:.75em;margin-bottom:.75em}.prose h2+*,.prose h3+*,.prose h4+*,.prose hr+*{margin-top:0}.prose thead th:first-child{padding-left:0}.prose thead th:last-child{padding-right:0}.prose tbody td:first-child{padding-left:0}.prose tbody td:last-child{padding-right:0}.prose>:first-child{margin-top:0}.prose>:last-child{margin-bottom:0}.prose-lg{font-size:1.125rem;line-height:1.7777778}.prose-lg p{margin-top:1.3333333em;margin-bottom:1.3333333em}.prose-lg [class~=lead]{font-size:1.2222222em;line-height:1.4545455;margin-top:1.0909091em;margin-bottom:1.0909091em}.prose-lg blockquote{margin-top:1.6666667em;margin-bottom:1.6666667em;padding-left:1em}.prose-lg h1{font-size:2.6666667em;margin-top:0;margin-bottom:.8333333em;line-height:1}.prose-lg h2{font-size:1.6666667em;margin-top:1.8666667em;margin-bottom:1.0666667em;line-height:1.3333333}.prose-lg h3{font-size:1.3333333em;margin-top:1.6666667em;margin-bottom:.6666667em;line-height:1.5}.prose-lg h4{margin-top:1.7777778em;margin-bottom:.4444444em;line-height:1.5555556}.prose-lg figure,.prose-lg img,.prose-lg video{margin-top:1.7777778em;margin-bottom:1.7777778em}.prose-lg figure>*{margin-top:0;margin-bottom:0}.prose-lg figure figcaption{font-size:.8888889em;line-height:1.5;margin-top:1em}.prose-lg code{font-size:.8888889em}.prose-lg h2 code{font-size:.8666667em}.prose-lg h3 code{font-size:.875em}.prose-lg pre{font-size:.8888889em;line-height:1.75;margin-top:2em;margin-bottom:2em;border-radius:.375rem;padding:1em 1.5em}.prose-lg ol,.prose-lg ul{margin-top:1.3333333em;margin-bottom:1.3333333em}.prose-lg li{margin-top:.6666667em;margin-bottom:.6666667em}.prose-lg ol>li{padding-left:1.6666667em}.prose-lg ol>li:before{left:0}.prose-lg ul>li{padding-left:1.6666667em}.prose-lg ul>li:before{width:.3333333em;height:.3333333em;top:.72222em;left:.2222222em}.prose-lg>ul>li p{margin-top:.8888889em;margin-bottom:.8888889em}.prose-lg>ul>li>:first-child{margin-top:1.3333333em}.prose-lg>ul>li>:last-child{margin-bottom:1.3333333em}.prose-lg>ol>li>:first-child{margin-top:1.3333333em}.prose-lg>ol>li>:last-child{margin-bottom:1.3333333em}.prose-lg ol ol,.prose-lg ol ul,.prose-lg ul ol,.prose-lg ul ul{margin-top:.8888889em;margin-bottom:.8888889em}.prose-lg hr{margin-top:3.1111111em;margin-bottom:3.1111111em}.prose-lg h2+*,.prose-lg h3+*,.prose-lg h4+*,.prose-lg hr+*{margin-top:0}.prose-lg table{font-size:.8888889em;line-height:1.5}.prose-lg thead th{padding-right:.75em;padding-bottom:.75em;padding-left:.75em}.prose-lg thead th:first-child{padding-left:0}.prose-lg thead th:last-child{padding-right:0}.prose-lg tbody td{padding:.75em}.prose-lg tbody td:first-child{padding-left:0}.prose-lg tbody td:last-child{padding-right:0}.prose-lg>:first-child{margin-top:0}.prose-lg>:last-child{margin-bottom:0}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.5rem*var(--tw-space-y-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0.5rem*var(--tw-space-x-reverse));margin-left:calc(0.5rem*(1 - var(--tw-space-x-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(2rem*var(--tw-space-x-reverse));margin-left:calc(2rem*(1 - var(--tw-space-x-reverse)))}.bg-clip-border{background-clip:border-box}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.bg-white{--tw-bg-opacity:1;background-color:rgba(255,255,255,var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgba(229,231,235,var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgba(75,85,99,var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgba(31,41,55,var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgba(254,202,202,var(--tw-bg-opacity))}.bg-red-800{--tw-bg-opacity:1;background-color:rgba(153,27,27,var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgba(167,243,208,var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgba(16,185,129,var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgba(6,95,70,var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgba(30,64,175,var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.hover\:bg-gray-300:hover{--tw-bg-opacity:1;background-color:rgba(209,213,219,var(--tw-bg-opacity))}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgba(55,65,81,var(--tw-bg-opacity))}.hover\:bg-red-700:hover{--tw-bg-opacity:1;background-color:rgba(185,28,28,var(--tw-bg-opacity))}.hover\:bg-yellow-300:hover{--tw-bg-opacity:1;background-color:rgba(252,211,77,var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgba(4,120,87,var(--tw-bg-opacity))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgba(29,78,216,var(--tw-bg-opacity))}.hover\:bg-indigo-600:hover{--tw-bg-opacity:1;background-color:rgba(79,70,229,var(--tw-bg-opacity))}.focus\:bg-gray-100:focus{--tw-bg-opacity:1;background-color:rgba(243,244,246,var(--tw-bg-opacity))}.bg-gradient-to-r{background-image:linear-gradient(90deg,var(--tw-gradient-stops))}.from-red-400{--tw-gradient-from:#f87171;--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to,rgba(248,113,113,0))}.to-blue-500{--tw-gradient-to:#3b82f6}.bg-center{background-position:50%}.bg-no-repeat{background-repeat:no-repeat}.bg-cover{background-size:cover}.border-transparent{border-color:transparent}.border-black{--tw-border-opacity:1;border-color:rgba(0,0,0,var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgba(243,244,246,var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgba(229,231,235,var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgba(156,163,175,var(--tw-border-opacity))}.border-gray-500{--tw-border-opacity:1;border-color:rgba(107,114,128,var(--tw-border-opacity))}.border-red-600{--tw-border-opacity:1;border-color:rgba(220,38,38,var(--tw-border-opacity))}.border-blue-100{--tw-border-opacity:1;border-color:rgba(219,234,254,var(--tw-border-opacity))}.border-blue-200{--tw-border-opacity:1;border-color:rgba(191,219,254,var(--tw-border-opacity))}.border-indigo-400{--tw-border-opacity:1;border-color:rgba(129,140,248,var(--tw-border-opacity))}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.hover\:border-red-300:hover{--tw-border-opacity:1;border-color:rgba(252,165,165,var(--tw-border-opacity))}.focus\:border-gray-300:focus{--tw-border-opacity:1;border-color:rgba(209,213,219,var(--tw-border-opacity))}.focus\:border-gray-900:focus{--tw-border-opacity:1;border-color:rgba(17,24,39,var(--tw-border-opacity))}.focus\:border-red-900:focus{--tw-border-opacity:1;border-color:rgba(127,29,29,var(--tw-border-opacity))}.focus\:border-green-900:focus{--tw-border-opacity:1;border-color:rgba(6,78,59,var(--tw-border-opacity))}.focus\:border-blue-900:focus{--tw-border-opacity:1;border-color:rgba(30,58,138,var(--tw-border-opacity))}.focus\:border-indigo-300:focus{--tw-border-opacity:1;border-color:rgba(165,180,252,var(--tw-border-opacity))}.focus\:border-indigo-700:focus{--tw-border-opacity:1;border-color:rgba(67,56,202,var(--tw-border-opacity))}.rounded{border-radius:.25rem}.rounded-md{border-radius:.375rem}.rounded-full{border-radius:9999px}.border-0{border-width:0}.border-2{border-width:2px}.border{border-width:1px}.border-b-0{border-bottom-width:0}.border-b-2{border-bottom-width:2px}.border-t-4{border-top-width:4px}.border-b-4{border-bottom-width:4px}.border-t-8{border-top-width:8px}.border-b-8{border-bottom-width:8px}.border-t{border-top-width:1px}.border-r{border-right-width:1px}.border-b{border-bottom-width:1px}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-move{cursor:move}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.inline-grid{display:inline-grid}.hidden{display:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.self-center{align-self:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-around{justify-content:space-around}.flex-1{flex:1 1 0%}.flex-auto{flex:1 1 auto}.flex-none{flex:none}.float-right{float:right}.font-sans{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.font-normal{font-weight:400}.font-medium{font-weight:500}.font-semibold{font-weight:600}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-black{font-weight:900}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-10{height:2.5rem}.h-16{height:4rem}.h-20{height:5rem}.h-24{height:6rem}.h-32{height:8rem}.h-full{height:100%}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-5xl{font-size:3rem;line-height:1}.text-6xl{font-size:3.75rem;line-height:1}.leading-5{line-height:1.25rem}.leading-7{line-height:1.75rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.leading-snug{line-height:1.375}.leading-relaxed{line-height:1.625}.leading-loose{line-height:2}.list-inside{list-style-position:inside}.list-disc{list-style-type:disc}.m-1{margin:.25rem}.m-auto{margin:auto}.my-2{margin-top:.5rem;margin-bottom:.5rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mt-1{margin-top:.25rem}.mr-1{margin-right:.25rem}.mb-1{margin-bottom:.25rem}.ml-1{margin-left:.25rem}.mt-2{margin-top:.5rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-2{margin-left:.5rem}.mt-3{margin-top:.75rem}.ml-3{margin-left:.75rem}.mt-4{margin-top:1rem}.mb-4{margin-bottom:1rem}.ml-4{margin-left:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.mb-8{margin-bottom:2rem}.ml-12{margin-left:3rem}.-mt-px{margin-top:-1px}.max-w-xs{max-width:20rem}.max-w-md{max-width:28rem}.max-w-xl{max-width:36rem}.max-w-3xl{max-width:48rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.min-h-screen{min-height:100vh}.opacity-0{opacity:0}.opacity-5{opacity:.05}.opacity-25{opacity:.25}.opacity-100{opacity:1}.disabled\:opacity-25:disabled{opacity:.25}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.overflow-hidden{overflow:hidden}.p-0{padding:0}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-6{padding:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.pt-1{padding-top:.25rem}.pt-2{padding-top:.5rem}.pl-2{padding-left:.5rem}.pb-3{padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pt-6{padding-top:1.5rem}.pt-8{padding-top:2rem}.pt-12{padding-top:3rem}.pointer-events-none{pointer-events:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.top-0{top:0}.right-0{right:0}.left-0{left:0}*{--tw-shadow:0 0 transparent}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,0.05)}.shadow,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,0.1),0 1px 2px 0 rgba(0,0,0,0.06)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,0.1),0 2px 4px -1px rgba(0,0,0,0.06)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,0.1),0 4px 6px -2px rgba(0,0,0,0.05)}.shadow-none{--tw-shadow:0 0 transparent;box-shadow:var(--tw-ring-offset-shadow,0 0 transparent),var(--tw-ring-shadow,0 0 transparent),var(--tw-shadow)}*{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,0.5);--tw-ring-offset-shadow:0 0 transparent;--tw-ring-shadow:0 0 transparent}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring:focus,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 transparent)}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgba(0,0,0,var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgba(209,213,219,var(--tw-ring-opacity))}.ring-red-300{--tw-ring-opacity:1;--tw-ring-color:rgba(252,165,165,var(--tw-ring-opacity))}.ring-green-300{--tw-ring-opacity:1;--tw-ring-color:rgba(110,231,183,var(--tw-ring-opacity))}.ring-green-600{--tw-ring-opacity:1;--tw-ring-color:rgba(5,150,105,var(--tw-ring-opacity))}.ring-blue-300{--tw-ring-opacity:1;--tw-ring-color:rgba(147,197,253,var(--tw-ring-opacity))}.focus\:ring-indigo-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgba(199,210,254,var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.focus\:ring-opacity-50:focus{--tw-ring-opacity:0.5}.fill-current{fill:currentColor}.text-center{text-align:center}.text-right{text-align:right}.text-transparent{color:transparent}.text-white{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.text-gray-200{--tw-text-opacity:1;color:rgba(229,231,235,var(--tw-text-opacity))}.text-gray-300{--tw-text-opacity:1;color:rgba(209,213,219,var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgba(156,163,175,var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgba(107,114,128,var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgba(31,41,55,var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgba(17,24,39,var(--tw-text-opacity))}.text-red-100{--tw-text-opacity:1;color:rgba(254,226,226,var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgba(239,68,68,var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgba(220,38,38,var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgba(153,27,27,var(--tw-text-opacity))}.text-yellow-500{--tw-text-opacity:1;color:rgba(245,158,11,var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgba(16,185,129,var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgba(5,150,105,var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity:1;color:rgba(79,70,229,var(--tw-text-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgba(255,255,255,var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgba(75,85,99,var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgba(17,24,39,var(--tw-text-opacity))}.hover\:text-red-700:hover{--tw-text-opacity:1;color:rgba(185,28,28,var(--tw-text-opacity))}.hover\:text-green-700:hover{--tw-text-opacity:1;color:rgba(4,120,87,var(--tw-text-opacity))}.focus\:text-gray-700:focus{--tw-text-opacity:1;color:rgba(55,65,81,var(--tw-text-opacity))}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.italic{font-style:italic}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.underline{text-decoration:underline}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.align-top{vertical-align:top}.align-middle{vertical-align:middle}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-8{width:2rem}.w-10{width:2.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-48{width:12rem}.w-auto{width:auto}.w-full{width:100%}.z-40{z-index:40}.z-50{z-index:50}.gap-4{gap:1rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-y-1{row-gap:.25rem}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.transform{--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;transform:translateX(var(--tw-translate-x)) translateY(var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.origin-top{transform-origin:top}.origin-top-right{transform-origin:top right}.origin-top-left{transform-origin:top left}.scale-95{--tw-scale-x:.95;--tw-scale-y:.95}.scale-100{--tw-scale-x:1;--tw-scale-y:1}.transition{transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-75{transition-duration:75ms}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}@-webkit-keyframes spin{to{transform:rotate(1turn)}}@keyframes spin{to{transform:rotate(1turn)}}@-webkit-keyframes ping{75%,to{transform:scale(2);opacity:0}}@keyframes ping{75%,to{transform:scale(2);opacity:0}}@-webkit-keyframes pulse{50%{opacity:.5}}@keyframes pulse{50%{opacity:.5}}@-webkit-keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}@keyframes bounce{0%,to{transform:translateY(-25%);-webkit-animation-timing-function:cubic-bezier(.8,0,1,1);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;-webkit-animation-timing-function:cubic-bezier(0,0,.2,1);animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}[x-cloak]{display:none!important}@media (min-width:640px){.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:.5rem}.sm\:block{display:block}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-start{justify-content:flex-start}.sm\:justify-center{justify-content:center}.sm\:h-48{height:12rem}.sm\:mt-0{margin-top:0}.sm\:ml-6{margin-left:1.5rem}.sm\:max-w-xs{max-width:20rem}.sm\:max-w-md{max-width:28rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:pt-0{padding-top:0}.sm\:w-auto{width:auto}.sm\:w-3\/5{width:60%}.sm\:w-5\/12{width:41.666667%}}@media (min-width:768px){.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:space-x-0>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0px*var(--tw-space-x-reverse));margin-left:calc(0px*(1 - var(--tw-space-x-reverse)))}.md\:space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0.5rem*var(--tw-space-y-reverse))}.md\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.md\:hidden{display:none}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:h-64{height:16rem}.md\:text-9xl{font-size:8rem;line-height:1}.md\:mt-0{margin-top:0}.md\:w-72{width:18rem}.md\:w-auto{width:auto}.md\:w-2\/3{width:66.666667%}.md\:w-1\/4{width:25%}.md\:w-3\/4{width:75%}.md\:w-5\/6{width:83.333333%}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1024px){.lg\:h-96{height:24rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}.lg\:w-1\/2{width:50%}.lg\:w-3\/4{width:75%}.lg\:w-4\/12{width:33.333333%}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}} diff --git a/resources/views/components/ingredient-picker.blade.php b/resources/views/components/ingredient-picker.blade.php index 92cb9c4..3ff2792 100644 --- a/resources/views/components/ingredient-picker.blade.php +++ b/resources/views/components/ingredient-picker.blade.php @@ -1,3 +1,5 @@ +@props(['hasError' => false]) +
@@ -11,7 +13,7 @@ x-ref="ingredients_type"/> false]) +@props(['disabled' => false, 'hasError' => false]) -merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50']) !!}> +@php + $classes = [ + 'rounded-md', + 'shadow-sm', + 'border-gray-300', + 'focus:border-indigo-300', + 'focus:ring', + 'focus:ring-indigo-200', + 'focus:ring-opacity-50' + ]; + if ($hasError) { + $classes[] = 'border-red-600'; + } +@endphp + +merge(['class' => implode(' ', $classes)]) !!}> diff --git a/resources/views/components/inputs/select.blade.php b/resources/views/components/inputs/select.blade.php index 5b9e250..917cad9 100644 --- a/resources/views/components/inputs/select.blade.php +++ b/resources/views/components/inputs/select.blade.php @@ -1,4 +1,23 @@ -merge(['class' => implode(' ', $classes)]) !!}> {{ $slot }} diff --git a/resources/views/foods/edit.blade.php b/resources/views/foods/edit.blade.php index b9e5ef5..2f8bd06 100644 --- a/resources/views/foods/edit.blade.php +++ b/resources/views/foods/edit.blade.php @@ -5,140 +5,143 @@

{{ $title }}

- @if ($food->exists)@method('put')@endif - @csrf -
-
- -
- + @if ($food->exists)@method('put')@endif + @csrf +
+
+ +
+ - -
+ +
- -
- + +
+ - -
+ +
- -
- + +
+ - -
-
+ +
+
-
- -
- +
+ +
+ - -
+ +
- -
- + +
+ - - - -
+ + + +
- -
- + +
+ - -
+ +
- -
- + +
+ - -
-
+ +
+
-
- @foreach (\App\Support\Nutrients::all()->sortBy('weight') as $nutrient) - -
- +
+ @foreach (\App\Support\Nutrients::all()->sortBy('weight') as $nutrient) + +
+ - -
- @endforeach -
- - - - - -
- - - -
- - -
- - - -
+
+ @endforeach +
-
- - {{ ($food->exists ? 'Save' : 'Add') }} - -
- + + + + +
+ + + +
+ + +
+ + + +
+
+ +
+ + {{ ($food->exists ? 'Save' : 'Add') }} + +
+ diff --git a/resources/views/foods/show.blade.php b/resources/views/foods/show.blade.php index 4e8b2fa..80557d6 100644 --- a/resources/views/foods/show.blade.php +++ b/resources/views/foods/show.blade.php @@ -7,7 +7,7 @@
-
+
@if($food->brand) @@ -56,7 +56,7 @@
-
-
+
Edit Food diff --git a/resources/views/goals/edit.blade.php b/resources/views/goals/edit.blade.php index 9303d8b..85ee4ba 100644 --- a/resources/views/goals/edit.blade.php +++ b/resources/views/goals/edit.blade.php @@ -15,7 +15,8 @@ + :value="old('from', $goal->from?->toDateString())" + :hasError="$errors->has('from')" />
@@ -24,7 +25,8 @@ + :value="old('to', $goal->to?->toDateString())" + :hasError="$errors->has('to')" />
@@ -33,7 +35,8 @@ + :selectedValue="old('frequency', $goal->frequency)" + :hasError="$errors->has('frequency')">
@@ -44,6 +47,7 @@ class="block w-full" :options="$nameOptions" :selectedValue="old('name', $goal->name)" + :hasError="$errors->has('name')" required>
@@ -56,6 +60,7 @@ step="any" class="block w-full" :value="old('goal', $goal->goal)" + :hasError="$errors->has('goal')" required /> diff --git a/resources/views/journal-entries/create-from-nutrients.blade.php b/resources/views/journal-entries/create-from-nutrients.blade.php index 6769e18..41b7c4b 100644 --- a/resources/views/journal-entries/create-from-nutrients.blade.php +++ b/resources/views/journal-entries/create-from-nutrients.blade.php @@ -19,6 +19,7 @@ type="date" class="block w-full" :value="old('date', $default_date->toDateString())" + :hasError="$errors->has('date')" required /> @@ -30,6 +31,7 @@ class="block w-full" :options="$meals" :selectedValue="old('meal')" + :hasError="$errors->has('meal')" required> @@ -43,6 +45,7 @@ type="text" class="block w-full" :value="old('summary')" + :hasError="$errors->has('summary')" required /> @@ -58,7 +61,8 @@ type="number" step="any" class="block w-full" - :value="old($nutrient['value'])"/> + :value="old($nutrient['value'])" + :hasError="$errors->has($nutrient['value'])"/> @endforeach diff --git a/resources/views/journal-entries/partials/entry-item-input.blade.php b/resources/views/journal-entries/partials/entry-item-input.blade.php index ba3a167..27a429f 100644 --- a/resources/views/journal-entries/partials/entry-item-input.blade.php +++ b/resources/views/journal-entries/partials/entry-item-input.blade.php @@ -1,3 +1,4 @@ +@php($key = $key ?? null)
@@ -15,6 +16,7 @@ type="date" class="block w-full" :value="$date ?? $default_date->toDateString()" + :hasError="$errors->has('ingredients.date.' . $key)" required />
@@ -25,8 +27,9 @@ class="block w-full" :options="$meals" :selectedValue="$meal ?? null" + :hasError="$errors->has('ingredients.meal.' . $key)" required> - + @if(is_null($key))@endif
@@ -39,6 +42,7 @@ class="block w-full" placeholder="Amount" :value="$amount ?? null" + :hasError="$errors->has('ingredients.amount.' . $key)" required /> @@ -48,8 +52,9 @@ - + :selectedValue="$unit ?? null" + :hasError="$errors->has('ingredients.unit.' . $key)"> + @if(is_null($key))@endif diff --git a/resources/views/recipes/edit.blade.php b/resources/views/recipes/edit.blade.php index b078b0e..ed45adc 100644 --- a/resources/views/recipes/edit.blade.php +++ b/resources/views/recipes/edit.blade.php @@ -33,7 +33,7 @@
- +
+ +
+ + + +
+
- +
- + -

+

+ + +

Serving weight

diff --git a/resources/views/recipes/partials/ingredient-input.blade.php b/resources/views/recipes/partials/ingredient-input.blade.php index 6392219..3e61994 100644 --- a/resources/views/recipes/partials/ingredient-input.blade.php +++ b/resources/views/recipes/partials/ingredient-input.blade.php @@ -1,5 +1,13 @@ +@php($key = $key ?? null) +@error("ingredients.amount.{$key}") + @php($amount_error = 'border-red-600') +@enderror +@error("ingredients.unit.{$key}") + @php($unit_error = 'border-red-600') +@enderror +
- +
@@ -11,16 +19,17 @@
+ :default-name="$ingredient_name ?? null" + :has-error="(isset($amount) || isset($unit)) && empty($ingredient_id)"/>
diff --git a/resources/views/recipes/show.blade.php b/resources/views/recipes/show.blade.php index 5624a89..f40a8dd 100644 --- a/resources/views/recipes/show.blade.php +++ b/resources/views/recipes/show.blade.php @@ -8,7 +8,7 @@ {{ $recipe->name }} -
+
@if($recipe->time_total > 0)
@@ -81,7 +81,7 @@
-
+
@if(!$recipe->tags->isEmpty())

Tags

@@ -92,12 +92,28 @@
@endif + @if($recipe->source) +
+

Source

+ @if(filter_var($recipe->source, FILTER_VALIDATE_URL)) + {{ $recipe->source }} + @else + {{ $recipe->source }} + @endif +
+ @endif
-