Merge remote-tracking branch 'origin/main' into demo

This commit is contained in:
Christopher C. Wells 2021-04-10 21:13:53 -07:00
commit 741de17e07
52 changed files with 541 additions and 367 deletions

View File

@ -1,10 +1,11 @@
# Local env file assumes Sail is in use. See docker-compose.yml.
APP_NAME=kcal
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://127.0.0.1
APP_URL=http://kcal.test
APP_PORT=8080
APP_SERVICE=kcal.test
APP_SERVICE=app
APP_TIMEZONE=UTC
LOG_CHANNEL=stack
@ -17,17 +18,8 @@ DB_DATABASE=kcal
DB_USERNAME=kcal
DB_PASSWORD=kcal
#REDIS_URL=
REDIS_HOST=redis
#REDIS_PASSWORD=
REDIS_PORT=6379
#REDIS_DB=
#SCOUT_DRIVER=null
#SCOUT_DRIVER=algolia
#ALGOLIA_APP_ID=
#ALGOLIA_SECRET=
SCOUT_DRIVER=elastic
ELASTIC_HOST=elasticsearch:9200

View File

@ -35,7 +35,7 @@ jobs:
run: composer install --no-progress --no-interaction
- name: Generate app key
run: |
php -r "file_exists('.env') || copy('.env.ci.example', '.env');"
php -r "file_exists('.env') || copy('.env.ci', '.env');"
php artisan key:generate
- name: Run tests
run: vendor/bin/paratest --coverage-clover build/logs/clover.xml

View File

@ -3,7 +3,7 @@
/**
* A helper file for Laravel, to provide autocomplete information to your IDE
* Generated for Laravel 8.35.1.
* Generated for Laravel 8.36.2.
*
* This file should not be included in your code, only analyzed by your IDE!
*
@ -2773,7 +2773,7 @@
/**
* Dispatch a command to its appropriate handler in the current process.
*
* Queuable jobs will be dispatched to the "sync" queue.
* Queueable jobs will be dispatched to the "sync" queue.
*
* @param mixed $command
* @param mixed $handler
@ -15217,6 +15217,16 @@
*
* @static
*/
public static function censorRequestBodyFields($fieldNames)
{
/** @var \Facade\FlareClient\Flare $instance */
return $instance->censorRequestBodyFields($fieldNames);
}
/**
*
*
* @static
*/
public static function createReport($throwable)
{
/** @var \Facade\FlareClient\Flare $instance */

View File

@ -20,6 +20,8 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Pluralizer;
use Illuminate\Support\Str;
class JournalEntryController extends Controller
{
@ -156,72 +158,56 @@ class JournalEntryController extends Controller
/** @var \App\Models\JournalEntry[] $entries */
$entries = [];
$entry_key = 0;
$group_entries = isset($input['group_entries']) && (bool) $input['group_entries'];
// TODO: Improve efficiency. Potential for lots of queries here...
foreach ($ingredients as $ingredient) {
// Set entry key (combined date and meal or individual entries).
if (isset($input['group_entries']) && (bool) $input['group_entries']) {
if ($group_entries) {
$entry_key = "{$ingredient['date']}{$ingredient['meal']}";
}
else {
$entry_key++;
}
// Prepare entry values.
// Get an existing entry (when grouping) or create a new one.
$entries[$entry_key] = $entries[$entry_key] ?? JournalEntry::make([
'date' => $ingredient['date'],
'meal' => $ingredient['meal'],
])->user()->associate(Auth::user());
$entry = &$entries[$entry_key];
// Calculate amounts based on ingredient type.
$item = NULL;
$amount = Number::floatFromString($ingredient['amount']);
if ($ingredient['type'] == Food::class) {
$item = Food::whereId($ingredient['id'])->first();
$nutrient_multiplier = Nutrients::calculateFoodNutrientMultiplier(
$item,
Number::floatFromString($ingredient['amount']),
$ingredient['unit']
);
$nutrient_multiplier = Nutrients::calculateFoodNutrientMultiplier($item, $amount, $ingredient['unit']);
foreach (Nutrients::all()->pluck('value') as $nutrient) {
$entries[$entry_key]->{$nutrient} += $item->{$nutrient} * $nutrient_multiplier;
$entry->{$nutrient} += $item->{$nutrient} * $nutrient_multiplier;
}
$entries[$entry_key]->foods->add($item);
$entry->foods->add($item);
}
elseif ($ingredient['type'] == Recipe::class) {
$item = Recipe::whereId($ingredient['id'])->first();
foreach (Nutrients::all()->pluck('value') as $nutrient) {
$entries[$entry_key]->{$nutrient} += Nutrients::calculateRecipeNutrientAmount(
$item,
$nutrient,
Number::floatFromString($ingredient['amount']),
$ingredient['unit']
);
$entry->{$nutrient} += Nutrients::calculateRecipeNutrientAmount($item, $nutrient, $amount, $ingredient['unit']);
}
$entries[$entry_key]->recipes->add($item);
}
else {
return back()->withInput()->withErrors("Invalid ingredient type {$ingredient['type']}.");
$entry->recipes->add($item);
}
// Set entry summary.
$unit = $ingredient['unit'];
if ($item instanceof Food) {
if ($unit === 'serving') {
if (empty($item->serving_unit) && empty($item->serving_unit_name)) {
$unit = null;
}
elseif (!empty($item->serving_unit_name)) {
$unit = $item->serving_unit_formatted;
}
}
// Add to summary.
if (!empty($entry->summary)) {
$entry->summary .= '; ';
}
$entries[$entry_key]->summary .= (!empty($entries[$entry_key]->summary) ? ', ' : null);
$entries[$entry_key]->summary .= "{$ingredient['amount']} {$unit} {$item->name}";
$entry->summary .= $this->createIngredientSummary($ingredient, $item, $amount);
}
foreach ($entries as $entry) {
$entry->save();
$entry->user->save();
$entry->foods()->saveMany($entry->foods);
$entry->recipes()->saveMany($entry->recipes);
// Save all new entries.
foreach ($entries as $new_entry) {
$new_entry->save();
$new_entry->user->save();
$new_entry->foods()->saveMany($new_entry->foods);
$new_entry->recipes()->saveMany($new_entry->recipes);
}
$count = count($entries);
@ -236,6 +222,65 @@ class JournalEntryController extends Controller
return redirect()->route('journal-entries.index', $parameters);
}
/**
* Attempt to create a coherent summary for an entry ingredient.
*/
private function createIngredientSummary(array $ingredient, Food|Recipe $item, float $amount): string {
$name = $item->name;
$unit = $ingredient['unit'];
// Determine unit with special handling for custom Food units.
if ($item instanceof Food) {
if ($unit === 'serving') {
$no_serving_unit = empty($item->serving_unit) && empty($item->serving_unit_name);
// If there is no serving unit or the serving unit name is
// exactly the same as the item name don't use a serving
// unit and pluralize the _item_ name.
if ($no_serving_unit || $item->serving_unit_name === $name) {
$unit = null;
$name = Pluralizer::plural($name, $amount);
}
// If the serving unit name is already _part_ of the item
// name, just keep the defined unit (e.g. name: "tortilla
// chips" and serving name "chips").
elseif (Str::contains($name, $item->serving_unit_name)) {
$unit = 'serving';
}
// If a serving unit name is set, use the formatted serving
// unit name as a base.
elseif (!empty($item->serving_unit_name)) {
$unit = $item->serving_unit_formatted;
}
}
}
// Pluralize unit with supplied plurals or Pluralizer.
if (Nutrients::units()->has($unit)) {
$value = 'label';
if ($amount > 1) {
$value = 'plural';
}
$unit = Nutrients::units()->get($unit)[$value];
}
else {
$unit = Pluralizer::plural($unit, $amount);
}
// Add amount, unit, and name to summary.
$amount = Number::rationalStringFromFloat($amount);
$summary = "{$amount} {$unit} {$name}";
// Add detail if available.
if (isset($item->detail) && !empty($item->detail)) {
$summary .= ", {$item->detail}";
}
return $summary;
}
/**
* Store an entry from nutrients.
*/

View File

@ -170,7 +170,7 @@ final class Food extends Model
public function getServingSizeFormattedAttribute(): ?string {
$result = null;
if (!empty($this->serving_size)) {
$result = Number::fractionStringFromFloat($this->serving_size);
$result = Number::rationalStringFromFloat($this->serving_size);
}
return $result;
}

View File

@ -92,7 +92,7 @@ final class IngredientAmount extends Model
* Get the amount as a formatted string (e.g. 0.5 = 1/2).
*/
public function getAmountFormattedAttribute(): string {
return Number::fractionStringFromFloat($this->amount);
return Number::rationalStringFromFloat($this->amount);
}
/**
@ -103,7 +103,7 @@ final class IngredientAmount extends Model
public function getNutrientsSummaryAttribute(): string {
$summary = [];
foreach (Nutrients::all() as $nutrient) {
$amount = round($this->{$nutrient['value']}(), 2);
$amount = Nutrients::round($this->{$nutrient['value']}(), $nutrient['value']);
$summary[] = "{$nutrient['label']}: {$amount}{$nutrient['unit']}";
}
return implode(', ', $summary);

View File

@ -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\Nutrients;
use ElasticScoutDriverPlus\QueryDsl;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -165,7 +166,7 @@ final class Recipe extends Model implements HasMedia
if (empty($this->weight)) {
return null;
}
return round($this->weight / $this->servings, 2);
return round($this->weight / $this->servings);
}
/**
@ -209,13 +210,6 @@ final class Recipe extends Model implements HasMedia
/**
* Add nutrient calculations handling to overloading.
*
* @param string $method
* @param array $parameters
*
* @return mixed
*
* @noinspection PhpMissingParamTypeInspection
*/
public function __call($method, $parameters): mixed {
if (in_array($method, $this->nutrientTotalMethods)) {
@ -223,15 +217,7 @@ final class Recipe extends Model implements HasMedia
}
elseif (in_array($method, $this->nutrientPerServingMethods)) {
$sum = $this->sumNutrient(substr($method, 0, -10)) / $this->servings;
// Per-serving calculations are rounded, though actual food label
// rounding standards are more complex.
if ($sum > 1) {
return round($sum);
}
else {
return round($sum, 2);
}
return Nutrients::round($sum, substr($method, 0, -10));
}
else {
return parent::__call($method, $parameters);

View File

@ -7,10 +7,10 @@ use Phospr\Fraction;
class Number
{
/**
* Get a float value from a decimal or fraction string.
* Get a float value from a decimal or rational string.
*
* @param string $value
* Value in decimal or string format.
* Decimal or rational string.
* @return float
* Float representation of the value.
*
@ -27,21 +27,38 @@ class Number
}
/**
* Get a string faction representation of a float.
* Get a string rational representation of a float.
*
* @todo Handle repeating values like 1/3, 2/3, etc. better.
*
* @see https://rosettacode.org/wiki/Convert_decimal_number_to_rational#PHP
* Special handling is used for common cases 1/3 and 2/3 to ensure the
* expected rationals. Other less common rationals (e.g. n/7 or n/9) will
* not be well handled here.
*
* @param float $value
* Value to convert to string fraction.
* Value to convert to rational string.
* @return string
* String fraction.
* Rational string.
*
* @todo Learn maths.
*/
public static function fractionStringFromFloat(float $value): string {
$fraction = (string) Fraction::fromFloat($value);
$fraction = str_replace(['33/100', '33333333/100000000'], '1/3', $fraction);
$fraction = str_replace(['67/100', '66666667/100000000'], '2/3', $fraction);
return $fraction;
public static function rationalStringFromFloat(float $value): string {
$decimal = Fraction::fromFloat(($value - floor($value)));
if ($decimal->isSameValueAs(Fraction::fromFloat(1/3))) {
$string = '1/3';
$whole = floor($value);
if ($whole > 0) {
$string = "{$whole} {$string}";
}
}
elseif ($decimal->isSameValueAs(Fraction::fromFloat(2/3))) {
$string = '2/3';
$whole = floor($value);
if ($whole > 0) {
$string = "{$whole} {$string}";
}
}
else {
$string = (string) Fraction::fromFloat($value);
}
return $string;
}
}

View File

@ -16,6 +16,7 @@ class Nutrients
* Each entry has two keys:
* - value: Machine name for the unit.
* - label: Human-readable name for the unit.
* - plural: Human-readable plural form of the unit name.
* - type: Unit type -- matching types can be converted.
*
* @return \Illuminate\Support\Collection
@ -25,31 +26,37 @@ class Nutrients
'cup' => [
'value' => 'cup',
'label' => 'cup',
'plural' => 'cups',
'type' => 'volume',
],
'gram' => [
'value' => 'gram',
'label' => 'gram',
'plural' => 'grams',
'type' => 'weight',
],
'oz' => [
'value' => 'oz',
'label' => 'oz',
'plural' => 'oz',
'type' => 'weight',
],
'serving' => [
'value' => 'serving',
'label' => 'serving',
'plural' => 'servings',
'type' => 'division',
],
'tbsp' => [
'value' => 'tbsp',
'label' => 'tbsp.',
'plural' => 'tbsp.',
'type' => 'volume',
],
'tsp' => [
'value' => 'tsp',
'label' => 'tsp.',
'plural' => 'tsp.',
'type' => 'volume',
],
]);
@ -188,4 +195,57 @@ class Nutrients
throw new \DomainException("Unsupported recipe unit: {$fromUnit}");
}
}
/**
* Round a nutrient amount according to FDA guidelines.
*
* Note: this stays mostly true to the guidelines except that carbohydrates
* and protein are meant to state "less than 1 gram" when the amount is less
* than 1 gram. Instead, this method treats anything less than 1 gram as
* zero.
*
* @url https://labelcalc.com/food-labeling/a-guide-to-using-fda-rounding-rules-for-your-food-label/
*
* @throws \InvalidArgumentException
*/
public static function round(float $amount, string $nutrient): float {
return match ($nutrient) {
/*
* Calories:
* - Less than 5 goes to zero.
* - Between 5 and 50 rounds to nearest number divisible by 5.
* - Greater than 50 rounds to nearest number divisible by 10.
*/
'calories' => ($amount < 5 ? 0 : ($amount <= 50 ? round($amount / 5 ) * 5 : round($amount / 10 ) * 10)),
/*
* Carbohydrates and protein:
* - Less than 1 goes to zero.
* - Greater than 1 rounds to nearest whole.
*/
'carbohydrates', 'protein' => ($amount < 1 ? 0 : round($amount)),
/*
* Cholesterol and fat:
* - Less than 0.5 goes to zero.
* - Between 0.5 and 5 rounds to nearest half.
* - Greater than 5 rounds to nearest whole.
*/
'cholesterol', 'fat' => ($amount < 0.5 ? 0 : ($amount <= 5 ? round($amount / 5, 1 ) * 5 : round($amount))),
/*
* Sodium:
* - Less than 5 goes to zero.
* - Between 5 and 140 rounds to nearest number divisible by 5.
* - Greater than 140 rounds to nearest number divisible by 10.
*/
'sodium' => ($amount < 5 ? 0 : ($amount <= 140 ? round($amount / 5 ) * 5 : round($amount / 10 ) * 10)),
/*
* Anything else excepts!
*/
default => throw new \InvalidArgumentException("Unrecognized nutrient {$nutrient}.")
};
}
}

168
composer.lock generated
View File

@ -8,16 +8,16 @@
"packages": [
{
"name": "algolia/algoliasearch-client-php",
"version": "2.7.3",
"version": "2.8.0",
"source": {
"type": "git",
"url": "https://github.com/algolia/algoliasearch-client-php.git",
"reference": "142a382e4649db0cb64d9eb8893872f1a4ba8dd3"
"reference": "d9781147ae433f5bdbfd902497d748d60e70d693"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/algolia/algoliasearch-client-php/zipball/142a382e4649db0cb64d9eb8893872f1a4ba8dd3",
"reference": "142a382e4649db0cb64d9eb8893872f1a4ba8dd3",
"url": "https://api.github.com/repos/algolia/algoliasearch-client-php/zipball/d9781147ae433f5bdbfd902497d748d60e70d693",
"reference": "d9781147ae433f5bdbfd902497d748d60e70d693",
"shasum": ""
},
"require": {
@ -76,22 +76,22 @@
],
"support": {
"issues": "https://github.com/algolia/algoliasearch-client-php/issues",
"source": "https://github.com/algolia/algoliasearch-client-php/tree/2.7.3"
"source": "https://github.com/algolia/algoliasearch-client-php/tree/2.8.0"
},
"time": "2020-12-22T11:27:03+00:00"
"time": "2021-04-07T16:50:58+00:00"
},
{
"name": "algolia/scout-extended",
"version": "v1.15.0",
"version": "v1.16.0",
"source": {
"type": "git",
"url": "https://github.com/algolia/scout-extended.git",
"reference": "f1da27101b4c88166f9d66d5110b46e1dacb8f1c"
"reference": "1154a57e8049e7d07d51571599ee5aedd106b945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/algolia/scout-extended/zipball/f1da27101b4c88166f9d66d5110b46e1dacb8f1c",
"reference": "f1da27101b4c88166f9d66d5110b46e1dacb8f1c",
"url": "https://api.github.com/repos/algolia/scout-extended/zipball/1154a57e8049e7d07d51571599ee5aedd106b945",
"reference": "1154a57e8049e7d07d51571599ee5aedd106b945",
"shasum": ""
},
"require": {
@ -157,9 +157,9 @@
],
"support": {
"issues": "https://github.com/algolia/scout-extended/issues",
"source": "https://github.com/algolia/scout-extended/tree/v1.15.0"
"source": "https://github.com/algolia/scout-extended/tree/v1.16.0"
},
"time": "2021-03-17T15:52:17+00:00"
"time": "2021-04-08T15:17:35+00:00"
},
{
"name": "asm89/stack-cors",
@ -2336,16 +2336,16 @@
},
{
"name": "laravel/framework",
"version": "v8.35.1",
"version": "v8.36.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "d118c0df39e7524131176aaf76493eae63a8a602"
"reference": "0debd8ad6b5aa1f61ccc73910adf049af4ca0444"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/d118c0df39e7524131176aaf76493eae63a8a602",
"reference": "d118c0df39e7524131176aaf76493eae63a8a602",
"url": "https://api.github.com/repos/laravel/framework/zipball/0debd8ad6b5aa1f61ccc73910adf049af4ca0444",
"reference": "0debd8ad6b5aa1f61ccc73910adf049af4ca0444",
"shasum": ""
},
"require": {
@ -2500,20 +2500,20 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2021-03-30T21:34:17+00:00"
"time": "2021-04-07T12:37:22+00:00"
},
{
"name": "laravel/scout",
"version": "v8.6.0",
"version": "v8.6.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/scout.git",
"reference": "54070f7b68fed15f25e61e68884c4110496b8aa1"
"reference": "7fb1c860a2fd904f0e084a7cc3641eb1448ba278"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/scout/zipball/54070f7b68fed15f25e61e68884c4110496b8aa1",
"reference": "54070f7b68fed15f25e61e68884c4110496b8aa1",
"url": "https://api.github.com/repos/laravel/scout/zipball/7fb1c860a2fd904f0e084a7cc3641eb1448ba278",
"reference": "7fb1c860a2fd904f0e084a7cc3641eb1448ba278",
"shasum": ""
},
"require": {
@ -2569,7 +2569,7 @@
"issues": "https://github.com/laravel/scout/issues",
"source": "https://github.com/laravel/scout"
},
"time": "2021-01-19T15:30:52+00:00"
"time": "2021-04-06T14:35:41+00:00"
},
{
"name": "laravel/tinker",
@ -3470,16 +3470,16 @@
},
{
"name": "opis/closure",
"version": "3.6.1",
"version": "3.6.2",
"source": {
"type": "git",
"url": "https://github.com/opis/closure.git",
"reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5"
"reference": "06e2ebd25f2869e54a306dda991f7db58066f7f6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opis/closure/zipball/943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5",
"reference": "943b5d70cc5ae7483f6aff6ff43d7e34592ca0f5",
"url": "https://api.github.com/repos/opis/closure/zipball/06e2ebd25f2869e54a306dda991f7db58066f7f6",
"reference": "06e2ebd25f2869e54a306dda991f7db58066f7f6",
"shasum": ""
},
"require": {
@ -3529,9 +3529,9 @@
],
"support": {
"issues": "https://github.com/opis/closure/issues",
"source": "https://github.com/opis/closure/tree/3.6.1"
"source": "https://github.com/opis/closure/tree/3.6.2"
},
"time": "2020-11-07T02:01:34+00:00"
"time": "2021-04-09T13:42:10+00:00"
},
{
"name": "phospr/fraction",
@ -4522,16 +4522,16 @@
},
{
"name": "spatie/image",
"version": "1.10.4",
"version": "1.10.5",
"source": {
"type": "git",
"url": "https://github.com/spatie/image.git",
"reference": "7ea129bc7b7521864c5a540e3b1c14ea194316d3"
"reference": "63a963d0200fb26f2564bf7201fc7272d9b22933"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/image/zipball/7ea129bc7b7521864c5a540e3b1c14ea194316d3",
"reference": "7ea129bc7b7521864c5a540e3b1c14ea194316d3",
"url": "https://api.github.com/repos/spatie/image/zipball/63a963d0200fb26f2564bf7201fc7272d9b22933",
"reference": "63a963d0200fb26f2564bf7201fc7272d9b22933",
"shasum": ""
},
"require": {
@ -4541,7 +4541,7 @@
"league/glide": "^1.6",
"php": "^7.2|^8.0",
"spatie/image-optimizer": "^1.1",
"spatie/temporary-directory": "^1.0",
"spatie/temporary-directory": "^1.0|^2.0",
"symfony/process": "^3.0|^4.0|^5.0"
},
"require-dev": {
@ -4575,7 +4575,7 @@
],
"support": {
"issues": "https://github.com/spatie/image/issues",
"source": "https://github.com/spatie/image/tree/1.10.4"
"source": "https://github.com/spatie/image/tree/1.10.5"
},
"funding": [
{
@ -4587,7 +4587,7 @@
"type": "github"
}
],
"time": "2021-03-10T16:11:40+00:00"
"time": "2021-04-07T08:42:24+00:00"
},
{
"name": "spatie/image-optimizer",
@ -4645,16 +4645,16 @@
},
{
"name": "spatie/laravel-medialibrary",
"version": "9.5.0",
"version": "9.5.3",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-medialibrary.git",
"reference": "89d1d2e5b4b53137819cc18a166f43edaeaf7e52"
"reference": "ebbc996db457adecc778db6030d22ef72b495d59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/89d1d2e5b4b53137819cc18a166f43edaeaf7e52",
"reference": "89d1d2e5b4b53137819cc18a166f43edaeaf7e52",
"url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/ebbc996db457adecc778db6030d22ef72b495d59",
"reference": "ebbc996db457adecc778db6030d22ef72b495d59",
"shasum": ""
},
"require": {
@ -4670,7 +4670,7 @@
"maennchen/zipstream-php": "^1.0|^2.0",
"php": "^7.4|^8.0",
"spatie/image": "^1.4.0",
"spatie/temporary-directory": "^1.1",
"spatie/temporary-directory": "^1.1|^2.0",
"symfony/console": "^4.4|^5.0"
},
"conflict": {
@ -4733,7 +4733,7 @@
],
"support": {
"issues": "https://github.com/spatie/laravel-medialibrary/issues",
"source": "https://github.com/spatie/laravel-medialibrary/tree/9.5.0"
"source": "https://github.com/spatie/laravel-medialibrary/tree/9.5.3"
},
"funding": [
{
@ -4745,7 +4745,7 @@
"type": "github"
}
],
"time": "2021-03-29T21:40:29+00:00"
"time": "2021-04-08T08:27:49+00:00"
},
{
"name": "spatie/laravel-tags",
@ -4899,23 +4899,23 @@
},
{
"name": "spatie/temporary-directory",
"version": "1.3.0",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/temporary-directory.git",
"reference": "f517729b3793bca58f847c5fd383ec16f03ffec6"
"reference": "06fe0f10d068fdf145c9b2235030e568c913bb61"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/temporary-directory/zipball/f517729b3793bca58f847c5fd383ec16f03ffec6",
"reference": "f517729b3793bca58f847c5fd383ec16f03ffec6",
"url": "https://api.github.com/repos/spatie/temporary-directory/zipball/06fe0f10d068fdf145c9b2235030e568c913bb61",
"reference": "06fe0f10d068fdf145c9b2235030e568c913bb61",
"shasum": ""
},
"require": {
"php": "^7.2|^8.0"
"php": "^8.0"
},
"require-dev": {
"phpunit/phpunit": "^8.0|^9.0"
"phpunit/phpunit": "^9.5"
},
"type": "library",
"autoload": {
@ -4944,9 +4944,19 @@
],
"support": {
"issues": "https://github.com/spatie/temporary-directory/issues",
"source": "https://github.com/spatie/temporary-directory/tree/1.3.0"
"source": "https://github.com/spatie/temporary-directory/tree/2.0.0"
},
"time": "2020-11-09T15:54:21+00:00"
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
},
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"time": "2021-03-30T19:46:13+00:00"
},
{
"name": "swiftmailer/swiftmailer",
@ -7552,16 +7562,16 @@
"packages-dev": [
{
"name": "barryvdh/laravel-ide-helper",
"version": "v2.9.3",
"version": "v2.10.0",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-ide-helper.git",
"reference": "2f61602e7a7f88ad29b0f71355b4bb71396e923b"
"reference": "73b1012b927633a1b4cd623c2e6b1678e6faef08"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/2f61602e7a7f88ad29b0f71355b4bb71396e923b",
"reference": "2f61602e7a7f88ad29b0f71355b4bb71396e923b",
"url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/73b1012b927633a1b4cd623c2e6b1678e6faef08",
"reference": "73b1012b927633a1b4cd623c2e6b1678e6faef08",
"shasum": ""
},
"require": {
@ -7630,7 +7640,7 @@
],
"support": {
"issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v2.9.3"
"source": "https://github.com/barryvdh/laravel-ide-helper/tree/v2.10.0"
},
"funding": [
{
@ -7638,7 +7648,7 @@
"type": "github"
}
],
"time": "2021-04-02T14:32:13+00:00"
"time": "2021-04-09T06:17:55+00:00"
},
{
"name": "barryvdh/reflection-docblock",
@ -8297,16 +8307,16 @@
},
{
"name": "facade/flare-client-php",
"version": "1.5.0",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/facade/flare-client-php.git",
"reference": "9dd6f2b56486d939c4467b3f35475d44af57cf17"
"reference": "f2b0969f2d9594704be74dbeb25b201570a98098"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/facade/flare-client-php/zipball/9dd6f2b56486d939c4467b3f35475d44af57cf17",
"reference": "9dd6f2b56486d939c4467b3f35475d44af57cf17",
"url": "https://api.github.com/repos/facade/flare-client-php/zipball/f2b0969f2d9594704be74dbeb25b201570a98098",
"reference": "f2b0969f2d9594704be74dbeb25b201570a98098",
"shasum": ""
},
"require": {
@ -8350,7 +8360,7 @@
],
"support": {
"issues": "https://github.com/facade/flare-client-php/issues",
"source": "https://github.com/facade/flare-client-php/tree/1.5.0"
"source": "https://github.com/facade/flare-client-php/tree/1.6.1"
},
"funding": [
{
@ -8358,26 +8368,26 @@
"type": "github"
}
],
"time": "2021-03-31T07:32:54+00:00"
"time": "2021-04-08T08:50:01+00:00"
},
{
"name": "facade/ignition",
"version": "2.7.0",
"version": "2.8.3",
"source": {
"type": "git",
"url": "https://github.com/facade/ignition.git",
"reference": "bdc8b0b32c888f6edc838ca641358322b3d9506d"
"reference": "a8201d51aae83addceaef9344592a3b068b5d64d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/facade/ignition/zipball/bdc8b0b32c888f6edc838ca641358322b3d9506d",
"reference": "bdc8b0b32c888f6edc838ca641358322b3d9506d",
"url": "https://api.github.com/repos/facade/ignition/zipball/a8201d51aae83addceaef9344592a3b068b5d64d",
"reference": "a8201d51aae83addceaef9344592a3b068b5d64d",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
"facade/flare-client-php": "^1.3.7",
"facade/flare-client-php": "^1.6",
"facade/ignition-contracts": "^1.0.2",
"filp/whoops": "^2.4",
"illuminate/support": "^7.0|^8.0",
@ -8435,7 +8445,7 @@
"issues": "https://github.com/facade/ignition/issues",
"source": "https://github.com/facade/ignition"
},
"time": "2021-03-30T15:55:38+00:00"
"time": "2021-04-09T20:45:59+00:00"
},
{
"name": "facade/ignition-contracts",
@ -8929,16 +8939,16 @@
},
{
"name": "nunomaduro/collision",
"version": "v5.3.0",
"version": "v5.4.0",
"source": {
"type": "git",
"url": "https://github.com/nunomaduro/collision.git",
"reference": "aca63581f380f63a492b1e3114604e411e39133a"
"reference": "41b7e9999133d5082700d31a1d0977161df8322a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/aca63581f380f63a492b1e3114604e411e39133a",
"reference": "aca63581f380f63a492b1e3114604e411e39133a",
"url": "https://api.github.com/repos/nunomaduro/collision/zipball/41b7e9999133d5082700d31a1d0977161df8322a",
"reference": "41b7e9999133d5082700d31a1d0977161df8322a",
"shasum": ""
},
"require": {
@ -9013,7 +9023,7 @@
"type": "patreon"
}
],
"time": "2021-01-25T15:34:13+00:00"
"time": "2021-04-09T13:38:32+00:00"
},
{
"name": "nunomaduro/larastan",
@ -9533,16 +9543,16 @@
},
{
"name": "phpstan/phpstan",
"version": "0.12.82",
"version": "0.12.83",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11"
"reference": "4a967cec6efb46b500dd6d768657336a3ffe699f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/3920f0fb0aff39263d3a4cb0bca120a67a1a6a11",
"reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a967cec6efb46b500dd6d768657336a3ffe699f",
"reference": "4a967cec6efb46b500dd6d768657336a3ffe699f",
"shasum": ""
},
"require": {
@ -9573,7 +9583,7 @@
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/0.12.82"
"source": "https://github.com/phpstan/phpstan/tree/0.12.83"
},
"funding": [
{
@ -9589,7 +9599,7 @@
"type": "tidelift"
}
],
"time": "2021-03-19T06:08:17+00:00"
"time": "2021-04-03T15:35:45+00:00"
},
{
"name": "phpunit/php-code-coverage",

View File

@ -33,10 +33,11 @@ class IngredientAmountFactory extends Factory
$ingredient_unit = 'serving';
}
$amounts = [1/8, 1/4, 1/3, 1/2, 2/3, 3/4, 1, 1 + 1/4, 1 + 1/3, 1 + 1/2, 1 + 2/3, 1 + 3/4, 2, 2 + 1/2, 3];
return [
'ingredient_id' => $ingredient_factory,
'ingredient_type' => $ingredient_type,
'amount' => $this->faker->randomElement([1/8, 1/4, 1/2, 3/4, 1, 1.25, 1.5, 1.75, 2, 2.5, 3]),
'amount' => $this->faker->randomElement($amounts),
'unit' => $ingredient_unit,
'detail' => $this->faker->boolean() ? Words::randomWords('a') : null,
'weight' => $this->faker->numberBetween(0, 50),

View File

@ -18,7 +18,7 @@ class CreateIngredientAmountsTable extends Migration
$table->id();
$table->unsignedInteger('ingredient_id');
$table->string('ingredient_type');
$table->unsignedFloat('amount');
$table->decimal('amount', 10, 8)->unsigned();
$table->enum('unit', Nutrients::units()->pluck('value')->toArray())->nullable();
$table->string('detail')->nullable();
$table->unsignedInteger('weight');

View File

@ -1,7 +1,7 @@
# For more information: https://laravel.com/docs/sail
version: '3'
services:
kcal.test:
app:
build:
context: ./vendor/laravel/sail/runtimes/8.0
dockerfile: Dockerfile
@ -18,10 +18,10 @@ services:
networks:
- sail
depends_on:
- mysql
- db
- redis
- elasticsearch
mysql:
db:
image: 'mysql:8.0'
ports:
- '${DB_PORT:-3306}:3306'
@ -35,6 +35,18 @@ services:
- 'mysql-data:/var/lib/mysql'
networks:
- sail
phpmyadmin:
image: phpmyadmin
restart: always
ports:
- 8080:80
environment:
PMA_HOST: db
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD:-kcal}'
networks:
- sail
depends_on:
- db
elasticsearch:
image: 'elasticsearch:7.12.0'
environment:

View File

@ -21,13 +21,14 @@
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
<server name="ELASTIC_HOST" value="localhost:9200"/>
<server name="MAIL_MAILER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="elastic"/>
<server name="SESSION_DRIVER" value="array"/>
<server name="TELESCOPE_ENABLED" value="false"/>
<!-- @todo Figure out how to do MySQL parallel testing inside Sail. -->
<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>
</php>
</phpunit>

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
<a {{ $attributes->merge(['class' => "px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white text-center uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150"]) }}>
{{ $slot }}
</a>

View File

@ -0,0 +1,3 @@
<x-button-link.base :attributes="$attributes" class="bg-green-800 hover:bg-green-700 active:bg-green-900 focus:border-green-900 ring-green-300">
{{ $slot }}
</x-button-link.base>

View File

@ -0,0 +1,3 @@
<x-button-link.base :attributes="$attributes" class="bg-red-800 hover:bg-red-700 active:bg-red-900 focus:border-red-900 ring-red-300">
{{ $slot }}
</x-button-link.base>

View File

@ -3,12 +3,9 @@
<x-slot name="header">
<div class="flex justify-between items-center">
<h1 class="font-semibold text-2xl text-gray-800 leading-tight">Foods</h1>
<a href="{{ route('foods.create') }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
New Food
</a>
<x-button-link.green href="{{ route('foods.create') }}" class="text-sm">
Add Food
</x-button-link.green>
</div>
</x-slot>
<x-search-view :route="route('api:v1:foods.index')" :tags="$tags">

View File

@ -4,82 +4,46 @@
<h1 class="font-semibold text-xl text-gray-800 leading-tight flex flex-auto items-center">
<div>
{{ $food->name }}@if($food->detail), {{ $food->detail }}@endif
@if($food->brand)
<div>{{ $food->brand }}</div>
@endif
</div>
<a class="ml-2 text-gray-500 hover:text-gray-700 hover:border-gray-300 text-sm"
href="{{ route('foods.edit', $food) }}">
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" />
</svg>
</a>
<a class="h-6 w-6 text-red-500 hover:text-red-700 hover:border-red-300 float-right text-sm"
href="{{ route('foods.delete', $food) }}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</a>
</h1>
</x-slot>
<article class="flex flex-col space-y-2 sm:flex-row sm:space-x-4 sm:space-y-0">
<section class="p-1 mb-2 border-2 border-black font-sans md:w-72">
<h1 class="text-3xl font-extrabold leading-none">Nutrition Facts</h1>
<section class="flex justify-between font-bold border-b-8 border-black">
<h1>Serving size</h1>
<div>
{{ $food->servingSizeFormatted }}
{{ $food->servingUnitFormatted ?? $food->name }}
({{ $food->serving_weight }}g)
</div>
</section>
<h2 class="font-bold text-right">Amount per serving</h2>
<section class="flex justify-between items-end font-extrabold">
<h1 class="text-3xl">Calories</h1>
<div class="text-4xl">{{ $food->calories }}</div>
</section>
<div class="border-t-4 border-black text-sm">
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Total Fat</h1>
<div>{{ $food->fat }}g</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Cholesterol</h1>
<div>{{ $food->cholesterol }}mg</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Sodium</h1>
<div>{{ $food->sodium }}mg</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Total Carbohydrate</h1>
<div>{{ $food->carbohydrates }}g</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Protein</h1>
<div>{{ $food->protein }}g</div>
</section>
</div>
</section>
<section class="flex flex-col space-y-2">
@if(!$food->tags->isEmpty())
<h1 class="font-bold text-2xl">Tags</h1>
<div class="flex flex-wrap">
@foreach ($food->tags as $tag)
<span class="m-1 bg-gray-200 rounded-full px-2 leading-loose">{{ $tag->name }}</span>
@endforeach
</div>
@endif
@if($food->description)
<h1 class="font-bold text-2xl">Description</h1>
<p class="text-gray-800">{{ $food->description }}</p>
@endif
<div class="flex flex-col justify-between pb-4 md:flex-row md:space-x-4">
<div class="flex-1">
<section class="flex flex-col space-y-2">
@if($food->brand)
<h1 class="font-bold text-2xl">Brand</h1>
<div class="flex flex-wrap">
{{ $food->brand }}
</div>
@endif
@if($food->notes)
<h1 class="font-bold text-2xl">Notes</h1>
<div class="flex flex-wrap">
{{ $food->notes }}
</div>
@endif
@if(!$food->tags->isEmpty())
<h1 class="font-bold text-2xl">Tags</h1>
<div class="flex flex-wrap">
@foreach ($food->tags as $tag)
<span class="m-1 bg-gray-200 rounded-full px-2 leading-loose">{{ $tag->name }}</span>
@endforeach
</div>
@endif
@if($food->description)
<h1 class="font-bold text-2xl">Description</h1>
<p class="text-gray-800">{{ $food->description }}</p>
@endif
@if($food->source)
<h1 class="font-bold text-2xl">Source</h1>
<p>
@if(filter_var($food->source, FILTER_VALIDATE_URL))
<a class="text-gray-500 hover:text-gray-700" href="{{ $food->source }}">{{ $food->source }}</a>
@else
{{ $food->source }}
@endif
</p>
@endif
@if(!$food->ingredientAmountRelationships->isEmpty())
<h1 class="font-bold text-2xl">Recipes</h1>
<ul class="list-disc list-inside ml-3 space-y-1">
@ -89,16 +53,61 @@
@endforeach
</ul>
@endif
@if($food->source)
<h1 class="font-bold text-2xl">Source</h1>
<p>
@if(filter_var($food->source, FILTER_VALIDATE_URL))
<a class="text-gray-500 hover:text-gray-700" href="{{ $food->source }}">{{ $food->source }}</a>
@else
{{ $food->source }}
@endif
</p>
@endif
</section>
</article>
</section>
</div>
<aside class="flex flex-col space-y-4 mt-8 sm:mt-0">
<section class="p-1 mb-2 border-2 border-black font-sans md:w-72">
<h1 class="text-3xl font-extrabold leading-none">Nutrition Facts</h1>
<section class="flex justify-between font-bold border-b-8 border-black">
<h1>Serving size</h1>
<div>
{{ $food->servingSizeFormatted }}
{{ $food->servingUnitFormatted ?? $food->name }}
({{ $food->serving_weight }}g)
</div>
</section>
<h2 class="font-bold text-right">Amount per serving</h2>
<section class="flex justify-between items-end font-extrabold">
<h1 class="text-3xl">Calories</h1>
<div class="text-4xl">{{ $food->calories }}</div>
</section>
<div class="border-t-4 border-black text-sm">
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Total Fat</h1>
<div>{{ $food->fat }}g</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Cholesterol</h1>
<div>{{ $food->cholesterol }}mg</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Sodium</h1>
<div>{{ $food->sodium }}mg</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Total Carbohydrate</h1>
<div>{{ $food->carbohydrates }}g</div>
</section>
<hr class="border-gray-500"/>
<section class="flex justify-between">
<h1 class="font-bold">Protein</h1>
<div>{{ $food->protein }}g</div>
</section>
</div>
</section>
<hr />
<section class="flex flex-col space-y-2">
<x-button-link.base href="{{ route('foods.edit', $food) }}">
Edit Food
</x-button-link.base>
<x-button-link.red href="{{ route('foods.delete', $food) }}">
Delete Food
</x-button-link.red>
</section>
</aside>
</div>
</x-app-layout>

View File

@ -13,7 +13,16 @@
</svg>
</a>
</div>
<div class="text-base text-gray-500">{{ $date->format('D, j M Y') }}</div>
<div class="text-base text-gray-500">
<form x-data method="GET" action="{{ route('goals.index') }}">
<x-inputs.input name="date"
type="date"
class="border-0 shadow-none p-0 text-center"
:value="$date->toDateString()"
x-on:change="$el.submit();"
required />
</form>
</div>
<div>
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
href="{{ route(Route::current()->getName(), ['date' => $date->copy()->addDay(1)->toDateString()]) }}">
@ -24,12 +33,9 @@
</div>
</div>
</h1>
<a href="{{ route('goals.create') }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
<x-button-link.green href="{{ route('goals.create') }}" class="text-sm">
Add Goal
</a>
</x-button-link.green>
</div>
</x-slot>
<div class="space-y-4">

View File

@ -3,9 +3,9 @@
<x-slot name="header">
<div class="flex justify-between items-center">
<h1 class="font-semibold text-xl text-gray-800 leading-tight">Add Entry</h1>
<a href="{{ route('journal-entries.create', ['date' => $default_date->format('Y-m-d')]) }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
Add by Recipes/Food
</a>
<x-button-link.green href="{{ route('journal-entries.create', ['date' => $default_date->format('Y-m-d')]) }}" class="text-sm">
Add by Recipes/Foods
</x-button-link.green>
</div>
</x-slot>
<form method="POST" action="{{ route('journal-entries.store.from-nutrients') }}">

View File

@ -3,9 +3,9 @@
<x-slot name="header">
<div class="flex justify-between items-center">
<h1 class="font-semibold text-xl text-gray-800 leading-tight">Add Entries</h1>
<a href="{{ route('journal-entries.create.from-nutrients', ['date' => $default_date->format('Y-m-d')]) }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
<x-button-link.green href="{{ route('journal-entries.create.from-nutrients', ['date' => $default_date->format('Y-m-d')]) }}" class="text-sm">
Add by Nutrients
</a>
</x-button-link.green>
</div>
</x-slot>
<form method="POST" action="{{ route('journal-entries.store') }}">
@ -17,14 +17,14 @@
<div class="journal-entry-template hidden">
@include('journal-entries.partials.entry-item-input', ['default_date' => $default_date])
</div>
<x-inputs.icon-green class="add-entry-item" x-on:click="addEntryNode($el);">
<x-inputs.icon-green type="button" class="add-entry-item" x-on:click="addEntryNode($el);">
<svg class="h-10 w-10 pointer-events-none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-11a1 1 0 10-2 0v2H7a1 1 0 100 2h2v2a1 1 0 102 0v-2h2a1 1 0 100-2h-2V7z" clip-rule="evenodd" />
</svg>
</x-inputs.icon-green>
<div class="flex items-center justify-end mt-4 space-x-4">
<fieldset class="flex space-x-2">
<x-inputs.input name="group_entries" type="checkbox" class="h-5 w-5" value="1" checked />
<x-inputs.input name="group_entries" type="checkbox" class="h-5 w-5" value="1" />
<x-inputs.label for="groupEntries" value="Group entries by day and meal" />
</fieldset>
<x-inputs.button x-on:click="removeTemplate($el);">Add entries</x-inputs.button>

View File

@ -13,7 +13,16 @@
</svg>
</a>
</div>
<div class="text-base text-gray-500">{{ $date->format('D, j M Y') }}</div>
<div class="text-base text-gray-500">
<form x-data method="GET" action="{{ route('journal-entries.index') }}">
<x-inputs.input name="date"
type="date"
class="border-0 shadow-none p-0 text-center"
:value="$date->toDateString()"
x-on:change="$el.submit();"
required />
</form>
</div>
<div>
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
href="{{ route(Route::current()->getName(), ['date' => $date->copy()->addDay(1)->toDateString()]) }}">
@ -24,12 +33,9 @@
</div>
</div>
</h1>
<a href="{{ route('journal-entries.create', ['date' => $date->format('Y-m-d')]) }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
New Entry
</a>
<x-button-link.green href="{{ route('journal-entries.create', ['date' => $date->format('Y-m-d')]) }}" class="text-sm">
Add Entry
</x-button-link.green>
</div>
</x-slot>
<div class="flex align-top flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0">
@ -104,7 +110,7 @@
</div>
<span class="text-sm text-gray-500">
@foreach(\App\Support\Nutrients::all()->sortBy('weight') as $nutrient)
{{ round($entries->where('meal', $meal)->sum($nutrient['value']), 2) }}{{ $nutrient['unit'] }}
{{ \App\Support\Nutrients::round($entries->where('meal', $meal)->sum($nutrient['value']), $nutrient['value']) }}{{ $nutrient['unit'] }}
{{ $nutrient['value'] }}@if(!$loop->last), @endif
@endforeach
</span>
@ -124,7 +130,7 @@
<div>
<span class="font-bold">nutrients:</span>
@foreach(\App\Support\Nutrients::all()->sortBy('weight') as $nutrient)
{{ round($entry->{$nutrient['value']}, 2) }}{{ $nutrient['unit'] }}
{{ \App\Support\Nutrients::round($entry->{$nutrient['value']}, $nutrient['value']) }}{{ $nutrient['unit'] }}
{{ $nutrient['value'] }}@if(!$loop->last), @endif
@endforeach
</div>

View File

@ -38,7 +38,8 @@
size="5"
class="block w-full"
placeholder="Amount"
:value="$amount ?? null" />
:value="$amount ?? null"
required />
</div>
<!-- Unit -->

View File

@ -3,12 +3,9 @@
<x-slot name="header">
<div class="flex justify-between items-center">
<h1 class="font-semibold text-2xl text-gray-800 leading-tight">Recipes</h1>
<a href="{{ route('recipes.create') }}" class="inline-flex items-center rounded-md font-semibold text-white p-2 bg-green-500 tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-600 disabled:opacity-25 transition ease-in-out duration-150">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
New Recipe
</a>
<x-button-link.green href="{{ route('recipes.create') }}" class="text-sm">
Add Recipe
</x-button-link.green>
</div>
</x-slot>
<x-search-view :route="route('api:v1:recipes.index')" :tags="$tags">

View File

@ -6,25 +6,12 @@
<x-slot name="header">
<h1 class="font-semibold text-xl text-gray-800 leading-tight flex flex-auto">
{{ $recipe->name }}
<a class="ml-2 text-gray-500 hover:text-gray-700 hover:border-gray-300 text-sm"
href="{{ route('recipes.edit', $recipe) }}">
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" />
</svg>
</a>
<a class="h-6 w-6 text-red-500 hover:text-red-700 hover:border-red-300 float-right text-sm"
href="{{ route('recipes.delete', $recipe) }}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
</a>
</h1>
</x-slot>
<div class="flex flex-col justify-between pb-4 md:flex-row md:space-x-4">
<div class="flex-1" x-data="{showNutrientsSummary: false}">
@if($recipe->time_total > 0)
<section class="flex justify-between mb-2 p-2 bg-gray-100 rounded">
<section class="flex justify-between mb-2 p-2 bg-gray-100 rounded max-w-3xl">
<div>
<h1 class="mb-1 font-bold">Prep time</h1>
<p class="text-gray-800 text-sm">{{ $recipe->time_prep }} minutes</p>
@ -56,7 +43,7 @@
@if($item::class === \App\Models\IngredientAmount::class)
<li>
<span>
{{ \App\Support\Number::fractionStringFromFloat($item->amount) }}
{{ \App\Support\Number::rationalStringFromFloat($item->amount) }}
@if($item->unitFormatted){{ $item->unitFormatted }}@endif
@if($item->ingredient->type === \App\Models\Recipe::class)
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
@ -94,8 +81,20 @@
</ol>
</div>
</section>
<footer>
@if(!$recipe->tags->isEmpty())
<section>
<h1 class="mb-2 font-bold text-2xl">Tags</h1>
<div class="flex flex-wrap">
@foreach($recipe->tags as $tag)
<span class="m-1 bg-gray-200 rounded-full px-2 leading-loose cursor-default">{{ $tag->name }}</span>
@endforeach
</div>
</section>
@endif
</footer>
</div>
<aside class="flex flex-col space-y-4 md:w-1/2 lg:w-1/3">
<aside class="flex flex-col space-y-4 mt-8 sm:mt-0">
<div class="p-1 border-2 border-black font-sans w-72">
<div class="text-3xl font-extrabold leading-none">Nutrition Facts</div>
<div class="leading-snug">{{ $recipe->servings }} {{ \Illuminate\Support\Str::plural('serving', $recipe->servings ) }}</div>
@ -149,16 +148,15 @@
@endif
</section>
@endif
@if(!$recipe->tags->isEmpty())
<section>
<h1 class="mb-2 font-bold text-2xl">Tags</h1>
<div class="flex flex-wrap">
@foreach($recipe->tags as $tag)
<span class="m-1 bg-gray-200 rounded-full px-2 leading-loose cursor-default">{{ $tag->name }}</span>
@endforeach
</div>
</section>
@endif
<hr />
<section class="flex flex-col space-y-2">
<x-button-link.base href="{{ route('recipes.edit', $recipe) }}">
Edit Recipe
</x-button-link.base>
<x-button-link.red href="{{ route('recipes.delete', $recipe) }}">
Delete Recipe
</x-button-link.red>
</section>
</aside>
</div>
</x-app-layout>

View File

@ -3,12 +3,10 @@
namespace Tests\Feature\Console;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserAddTest extends TestCase
{
use RefreshDatabase;
public function testCanAddUserInteractively(): void
{

View File

@ -5,14 +5,11 @@ namespace Tests\Feature\Http\Controllers\Auth;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Lockout;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Validation\ValidationException;
use Tests\TestCase;
class AuthenticationTest extends TestCase
{
use RefreshDatabase;
public function testLoginScreenCanRendered(): void
{

View File

@ -5,11 +5,9 @@ namespace Tests\Feature\Http\Controllers;
use App\Http\Controllers\FoodController;
use App\Models\Food;
use Database\Factories\FoodFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
class FoodControllerTest extends HttpControllerTestCase
{
use RefreshDatabase;
/**
* @inheritdoc

View File

@ -5,11 +5,9 @@ namespace Tests\Feature\Http\Controllers;
use App\Http\Controllers\GoalController;
use App\Models\Goal;
use Database\Factories\GoalFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
class GoalControllerTest extends HttpControllerTestCase
{
use RefreshDatabase;
/**
* @inheritdoc

View File

@ -6,7 +6,6 @@ use App\Http\Controllers\IngredientPickerController;
use App\Models\Food;
use App\Models\Recipe;
use GuzzleHttp\Exception\ConnectException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Tests\LoggedInTestCase;
@ -15,7 +14,6 @@ use Tests\LoggedInTestCase;
*/
class IngredientPickerControllerTest extends LoggedInTestCase
{
use RefreshDatabase;
private function buildUrl(array $parameters = []): string
{

View File

@ -6,12 +6,11 @@ use App\Http\Controllers\JournalEntryController;
use App\Models\IngredientAmount;
use App\Models\JournalEntry;
use Database\Factories\JournalEntryFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
class JournalEntryControllerTest extends HttpControllerTestCase
{
use RefreshDatabase, WithFaker;
use WithFaker;
/**
* @inheritdoc

View File

@ -9,14 +9,12 @@ use App\Models\RecipeSeparator;
use App\Models\RecipeStep;
use Database\Factories\RecipeFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class RecipeControllerTest extends HttpControllerTestCase
{
use RefreshDatabase, WithFaker;
use WithFaker;
/**
* @inheritdoc

View File

@ -3,15 +3,13 @@
namespace Tests\Feature\JsonApi;
use App\Models\Food;
use App\Models\Recipe;
use Database\Factories\FoodFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\JsonApi\Traits\HasTags;
class FoodApiTest extends JsonApiTestCase
{
use RefreshDatabase, HasTags;
use HasTags;
/**
* @inheritdoc

View File

@ -5,12 +5,12 @@ namespace Tests\Feature\JsonApi;
use App\Models\Goal;
use Database\Factories\GoalFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\JsonApi\Traits\BelongsToUser;
class GoalApiTest extends JsonApiTestCase
{
use RefreshDatabase, BelongsToUser;
use BelongsToUser;
/**
* @inheritdoc

View File

@ -7,11 +7,9 @@ use App\Models\IngredientAmount;
use App\Models\JournalEntry;
use App\Models\Recipe;
use Database\Factories\IngredientAmountFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
class IngredientAmountApiTest extends JsonApiTestCase
{
use RefreshDatabase;
/**
* @inheritdoc

View File

@ -5,12 +5,11 @@ namespace Tests\Feature\JsonApi;
use App\Models\JournalEntry;
use Database\Factories\JournalEntryFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\JsonApi\Traits\BelongsToUser;
class JournalEntryApiTest extends JsonApiTestCase
{
use RefreshDatabase, BelongsToUser;
use BelongsToUser;
/**
* @inheritdoc

View File

@ -5,11 +5,9 @@ namespace Tests\Feature\JsonApi;
use App\Models\Recipe;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Foundation\Testing\RefreshDatabase;
class MediumApiTest extends JsonApiTestCase
{
use RefreshDatabase;
/**
* @inheritdoc

View File

@ -4,12 +4,11 @@ namespace Tests\Feature\JsonApi;
use App\Models\Recipe;
use Database\Factories\RecipeFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\JsonApi\Traits\HasTags;
class RecipeApiTest extends JsonApiTestCase
{
use RefreshDatabase, HasTags;
use HasTags;
/**
* @inheritdoc

View File

@ -6,12 +6,11 @@ use App\Models\Recipe;
use App\Models\RecipeSeparator;
use Database\Factories\RecipeSeparatorFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\JsonApi\Traits\BelongsToRecipe;
class RecipeSeparatorApiTest extends JsonApiTestCase
{
use RefreshDatabase, BelongsToRecipe;
use BelongsToRecipe;
/**
* @inheritdoc

View File

@ -6,12 +6,11 @@ use App\Models\Recipe;
use App\Models\RecipeStep;
use Database\Factories\RecipeStepFactory;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\Feature\JsonApi\Traits\BelongsToRecipe;
class RecipeStepApiTest extends JsonApiTestCase
{
use RefreshDatabase, BelongsToRecipe;
use BelongsToRecipe;
/**
* @inheritdoc

View File

@ -4,11 +4,9 @@ namespace Tests\Feature\JsonApi;
use App\Models\Tag;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Foundation\Testing\RefreshDatabase;
class TagApiTest extends JsonApiTestCase
{
use RefreshDatabase;
/**
* @inheritdoc

View File

@ -4,11 +4,9 @@ namespace Tests\Feature\JsonApi;
use App\Models\User;
use Database\Factories\UserFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserApiTest extends JsonApiTestCase
{
use RefreshDatabase;
/**
* @inheritdoc

View File

@ -4,12 +4,10 @@ namespace Tests\Feature\Support;
use App\Models\Food;
use App\Support\Nutrients;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class NutrientsTest extends TestCase
{
use RefreshDatabase;
/**
* Test invalid Food nutrient multiplier calculation.

View File

@ -2,9 +2,11 @@
namespace Tests;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
use RefreshDatabase;
}

View File

@ -1,6 +1,5 @@
<?php
namespace Tests\Unit\Rules;
use App\Rules\ArrayNotEmpty;

View File

@ -1,6 +1,5 @@
<?php
namespace Tests\Unit\Rules;
use App\Rules\InArray;

View File

@ -1,6 +1,5 @@
<?php
namespace Tests\Unit\Rules;
use App\Rules\StringIsDecimalOrFraction;

View File

@ -24,15 +24,15 @@ class NumberTest extends TestCase
}
/**
* Test (fraction) string to float conversion.
* Test (rational) string to float conversion.
*
* @dataProvider fractionStringFloatsProvider
*
* @see \App\Support\Number::fractionStringFromFloat()
* @see \App\Support\Number::rationalStringFromFloat()
*/
public function testFractionStringFromFloat(string $expectedString, float $float): void
{
$result = Number::fractionStringFromFloat($float);
$result = Number::rationalStringFromFloat($float);
$this->assertIsString($result);
$this->assertEquals($expectedString, $result);
}
@ -62,9 +62,9 @@ class NumberTest extends TestCase
*/
public function fractionStringFloatsProvider(): array {
return [
['0', 0.0], ['1/8', 1/8], ['1/4', 1/4], ['1/2', 1/2],
['3/4', 3/4], ['1', 1.0], ['1 1/4', 1.25],
['1 1/2', 1.5], ['2 1/2', 2.5], ['2 3/4', 2.75],
['0', 0.0], ['1/8', 1/8], ['1/4', 1/4], ['1/3', 1/3], ['1/2', 1/2],
['2/3', 2/3], ['3/4', 3/4], ['1', 1.0], ['1 1/4', 1.25], ['1 1/3', 1 + 1/3],
['1 1/2', 1.5], ['1 2/3', 1 + 2/3], ['2 1/2', 2.5], ['2 3/4', 2.75],
];
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace Tests\Unit\Support;
use App\Support\Nutrients;
use PHPUnit\Framework\TestCase;
class NutrientsTest extends TestCase
{
/**
* Test nutrient rounding.
*
* @dataProvider nutrientAmountsProvider
*
* @see \App\Support\Nutrients::round()
*/
public function testNutrientRounding(float $amount, string $nutrient, float $expectedFloat): void
{
$result = Nutrients::round($amount, $nutrient);
$this->assertIsFloat($result);
$this->assertEquals($expectedFloat, $result);
}
public function testNutrientRoundingExcepts(): void
{
$this->expectException(\InvalidArgumentException::class);
Nutrients::round(1, 'pancake');
}
/**
* Data providers.
*/
/**
* Provide nutrient amounts for and expected rounded results.
*/
public function nutrientAmountsProvider(): array {
return [
[0, 'calories', 0], [1, 'calories', 0], [2, 'calories', 0], [3, 'calories', 0], [4, 'calories', 0], [5, 'calories', 5], [21, 'calories', 20], [23, 'calories', 25], [45, 'calories', 45], [50, 'calories', 50],
[0, 'carbohydrates', 0], [0.1, 'carbohydrates', 0], [0.2, 'carbohydrates', 0], [0.3, 'carbohydrates', 0], [0.4, 'carbohydrates', 0], [0.5, 'carbohydrates', 0], [0.9, 'carbohydrates', 0], [1, 'carbohydrates', 1], [2.25, 'carbohydrates', 2], [2.75, 'carbohydrates', 3],
[0, 'protein', 0], [0.1, 'protein', 0], [0.2, 'protein', 0], [0.3, 'protein', 0], [0.4, 'protein', 0], [0.5, 'protein', 0], [0.9, 'protein', 0], [1, 'protein', 1], [2.25, 'protein', 2], [2.75, 'protein', 3],
[0, 'cholesterol', 0], [0.1, 'cholesterol', 0], [0.2, 'cholesterol', 0], [0.3, 'cholesterol', 0], [0.4, 'cholesterol', 0], [0.5, 'cholesterol', 0.5], [0.9, 'cholesterol', 1], [1, 'cholesterol', 1], [1.2, 'cholesterol', 1], [1.4, 'cholesterol', 1.5], [5, 'cholesterol', 5], [6, 'cholesterol', 6], [6.25, 'cholesterol', 6], [6.75, 'cholesterol', 7],
[0, 'fat', 0], [0.1, 'fat', 0], [0.2, 'fat', 0], [0.3, 'fat', 0], [0.4, 'fat', 0], [0.5, 'fat', 0.5], [0.9, 'fat', 1], [1, 'fat', 1], [1.2, 'fat', 1], [1.4, 'fat', 1.5], [5, 'fat', 5], [6, 'fat', 6], [6.25, 'fat', 6], [6.75, 'fat', 7],
[0, 'sodium', 0], [1, 'sodium', 0], [2, 'sodium', 0], [3, 'sodium', 0], [4, 'sodium', 0], [5, 'sodium', 5], [6, 'sodium', 5], [7, 'sodium', 5], [8, 'sodium', 10], [9, 'sodium', 10], [10, 'sodium', 10], [139, 'sodium', 140], [140, 'sodium', 140], [146, 'sodium', 150], [151, 'sodium', 150], [159, 'sodium', 160],
];
}
}