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

This commit is contained in:
Christopher C. Wells 2022-02-12 06:43:52 -08:00
commit 0b1e4c0429
35 changed files with 7917 additions and 7644 deletions

View File

@ -1,5 +1,5 @@
APP_NAME=kcal
APP_ENV=local
APP_ENV=testing
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
@ -7,7 +7,12 @@ APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_LEVEL=debug
DB_CONNECTION=sqlite
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kcal
DB_USERNAME=root
DB_PASSWORD=root
SCOUT_DRIVER=elastic
ELASTIC_HOST=localhost:9200

View File

@ -10,6 +10,10 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: mirromutth/mysql-action@v1.1
with:
mysql database: kcal
mysql root password: root
- uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
@ -41,7 +45,7 @@ jobs:
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
run: php artisan test --parallel --recreate-databases --coverage-clover build/logs/clover.xml
- name: Upload coverage results to Coveralls
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ use App\Models\Recipe;
use App\Search\Ingredient;
use ElasticScoutDriverPlus\Builders\MultiMatchQueryBuilder;
use ElasticScoutDriverPlus\Builders\TermsQueryBuilder;
use ElasticScoutDriverPlus\Support\Query;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -42,11 +43,10 @@ class IngredientPickerController extends Controller
* Search using an ElasticSearch service.
*/
private function searchWithElasticSearch(string $term): Collection {
return Food::boolSearch()
->join(Recipe::class)
$query = Query::bool()
// Attempt to match exact phrase first.
->should('match_phrase', ['name' => $term])
->should(Query::matchPhrase()->field('name')->query($term))
// Attempt multi-match search on all relevant fields with search-as-you-type on name.
->should((new MultiMatchQueryBuilder())
@ -57,10 +57,12 @@ class IngredientPickerController extends Controller
->fuzziness('AUTO'))
// Attempt to match on any tags in the term.
->should((new TermsQueryBuilder())
->terms('tags', explode(' ', $term)))
->should((new TermsQueryBuilder())->field('tags')->values(explode(' ', $term)))
// Get resulting models.
->minimumShouldMatch(1);
return Food::searchQuery($query)
->join(Recipe::class)
->execute()
->models();
}

View File

@ -2,8 +2,8 @@
namespace App\Http\Middleware;
use Fideloper\Proxy\TrustProxies as Middleware;
use Illuminate\Http\Request;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
class TrustProxies extends Middleware
{
@ -15,5 +15,10 @@ class TrustProxies extends Middleware
/**
* {@inheritdoc}
*/
protected $headers = Request::HEADER_X_FORWARDED_ALL;
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View File

@ -7,10 +7,9 @@ use App\Models\Traits\Journalable;
use App\Models\Traits\Sluggable;
use App\Models\Traits\Taggable;
use App\Support\Number;
use ElasticScoutDriverPlus\QueryDsl;
use ElasticScoutDriverPlus\Searchable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
/**
* App\Models\Food
@ -80,7 +79,6 @@ final class Food extends Model
use HasFactory;
use Ingredient;
use Journalable;
use QueryDsl;
use Searchable;
use Sluggable;
use Taggable;

View File

@ -9,12 +9,11 @@ use App\Models\Traits\Sluggable;
use App\Models\Traits\Taggable;
use App\Support\Number;
use App\Support\Nutrients;
use ElasticScoutDriverPlus\QueryDsl;
use ElasticScoutDriverPlus\Searchable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
use Laravel\Scout\Searchable;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -91,7 +90,6 @@ final class Recipe extends Model implements HasMedia
use Ingredient;
use InteractsWithMedia;
use Journalable;
use QueryDsl;
use Searchable;
use Sluggable;
use Taggable;

View File

@ -0,0 +1,20 @@
<?php
namespace App\Policies;
use App\Models\Goal;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class GoalPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can access (show, edit, delete) the goal.
*/
public function access(User $user, Goal $goal): bool {
return $user->id === $goal->user_id;
}
}

View File

@ -2,7 +2,9 @@
namespace App\Providers;
use App\Models\Goal;
use App\Models\User;
use App\Policies\GoalPolicy;
use App\Policies\UserPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@ -13,6 +15,7 @@ class AuthServiceProvider extends ServiceProvider
* @inheritdoc
*/
protected $policies = [
Goal::class => GoalPolicy::class,
User::class => UserPolicy::class,
];

View File

@ -9,39 +9,39 @@
"ext-gd": "*",
"ext-json": "*",
"ext-mbstring": "*",
"algolia/algoliasearch-client-php": "^2.7",
"algolia/scout-extended": "^1.15",
"babenkoivan/elastic-migrations": "^1.4",
"babenkoivan/elastic-scout-driver": "^1.3",
"babenkoivan/elastic-scout-driver-plus": "^2.0",
"cloudcreativity/laravel-json-api": "^3.2",
"cviebrock/eloquent-sluggable": "^8.0",
"algolia/algoliasearch-client-php": "^3.2",
"algolia/scout-extended": "^2.0",
"babenkoivan/elastic-migrations": "^2.0",
"babenkoivan/elastic-scout-driver": "^2.0",
"babenkoivan/elastic-scout-driver-plus": "^3.3",
"cloudcreativity/laravel-json-api": "^4.0",
"cviebrock/eloquent-sluggable": "^9.0",
"doctrine/dbal": "^3.0",
"fakerphp/faker": "^1.14",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.12",
"laravel/scout": "^8.6",
"laravel/tinker": "^2.5",
"league/flysystem-aws-s3-v3": "~1.0",
"laravel/framework": "^9.0",
"laravel/scout": "^9.0",
"laravel/tinker": "^2.7",
"league/flysystem-aws-s3-v3": "^3.0",
"phospr/fraction": "^1.2",
"spatie/laravel-csp": "^2.6",
"spatie/laravel-medialibrary": "^9.0.0",
"spatie/laravel-tags": "^3.0"
"spatie/laravel-medialibrary": "^10.0",
"spatie/laravel-tags": "^4.0"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^2.9",
"brianium/paratest": "^6.2",
"cloudcreativity/json-api-testing": "^3.2",
"facade/ignition": "^2.5",
"cloudcreativity/json-api-testing": "^4.0",
"fakerphp/faker": "^1.9.1",
"laravel/breeze": "^1.0",
"laravel/sail": "^1.10",
"mockery/mockery": "^1.4.2",
"nunomaduro/collision": "^5.0",
"nunomaduro/larastan": "^0.6.13",
"nunomaduro/collision": "^6.1",
"nunomaduro/larastan": "^2.0",
"php-coveralls/php-coveralls": "^2.4",
"phpunit/phpunit": "^9.3.3"
"phpunit/phpunit": "^9.3.3",
"spatie/laravel-ignition": "^1.0"
},
"config": {
"optimize-autoloader": true,

4152
composer.lock generated

File diff suppressed because it is too large Load Diff

7014
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,18 +10,18 @@
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --config=node_modules/laravel-mix/setup/webpack.config.js"
},
"devDependencies": {
"@tailwindcss/forms": "^0.3.3",
"@tailwindcss/typography": "^0.4.1",
"alpinejs": "^3.4.1",
"autoprefixer": "^10.3.6",
"axios": "^0.21.4",
"@tailwindcss/forms": "^0.4.0",
"@tailwindcss/typography": "^0.5.1",
"alpinejs": "^3.9.0",
"autoprefixer": "^10.4.2",
"axios": "^0.25.0",
"cross-env": "^7.0",
"laravel-mix": "^6.0.31",
"laravel-mix": "^6.0.41",
"lodash": "^4.17.21",
"postcss-import": "^14.0.2",
"quill": "^1.3.7",
"resolve-url-loader": "^4.0.0",
"tailwindcss": "^2.2.16",
"resolve-url-loader": "^5.0.0",
"tailwindcss": "^3.0.22",
"vue-template-compiler": "^2.6.14"
},
"dependencies": {

View File

@ -19,6 +19,7 @@
</coverage>
<php>
<server name="APP_ENV" value="testing"/>
<server name="DB_USERNAME" value="root"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
<server name="MAIL_MAILER" value="array"/>
@ -26,9 +27,5 @@
<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

File diff suppressed because one or more lines are too long

2
public/js/app.js vendored

File diff suppressed because one or more lines are too long

2
public/js/quill.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-emerald-600']) }}>
{{ $status }}
</div>
@endif

View File

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

View File

@ -28,11 +28,11 @@ switch ($width) {
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="transform opacity-0 scale-95"
x-transition:enter-end="transform opacity-100 scale-100"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">

View File

@ -26,7 +26,7 @@
<div class="absolute border-2 border-gray-500 border-b-0 bg-white"
x-bind="ingredient">
<template x-for="result in results" :key="result.id">
<div class="p-1 border-b-2 border-gray-500 hover:bg-yellow-300 cursor-pointer" x-bind:data-id="result.id">
<div class="p-1 border-b-2 border-gray-500 hover:bg-amber-300 cursor-pointer" x-bind:data-id="result.id">
<div class="pointer-events-none">
<div>
<span class="font-bold" x-text="result.name"></span><span class="text-gray-600" x-text="', ' + result.detail" x-show="result.detail"></span>

View File

@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => "inline-flex items-center border border-transparent rounded-md font-semibold text-xs text-green-500 tracking-widest hover:text-green-700 active:text-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-300 disabled:opacity-25 transition ease-in-out duration-150"]) }}>
<button {{ $attributes->merge(['type' => 'submit', 'class' => "inline-flex items-center border border-transparent rounded-md font-semibold text-xs text-emerald-500 tracking-widest hover:text-emerald-700 active:text-emerald-900 focus:outline-none focus:border-emerald-900 focus:ring ring-emerald-300 disabled:opacity-25 transition ease-in-out duration-150"]) }}>
{{ $slot }}
</button>

View File

@ -1,4 +1,4 @@
<div x-data data-tags="{{ $defaultTags ?? '[]' }}">
<div x-data data-tags="{!! $defaultTags ?? '[]' !!}">
<div x-data="tagSelect()" @click.outside="clearSearch()" @keydown.escape="clearSearch()">
<div class="relative" @keydown.enter.prevent="addTag(searchTerm)">
<x-inputs.input type="hidden"

View File

@ -25,7 +25,7 @@
@endif
@if(!$food->ingredientAmountRelationships->isEmpty())
<div class="flex space-x-2 items-center text-lg">
<div class="text-yellow-500">
<div class="text-amber-500">
<svg class="h-8 w-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd" />
</svg>

View File

@ -125,7 +125,7 @@
:selectedValue="$currentGoal?->id ?? null">
</x-inputs.select>
<div class="flex items-center justify-start mt-4">
<x-inputs.button class="bg-green-800 hover:bg-green-700">Change Goal</x-inputs.button>
<x-inputs.button class="bg-emerald-800 hover:bg-emerald-700">Change Goal</x-inputs.button>
<x-button-link.red class="ml-3" x-on:click="showGoalChangeForm = !showGoalChangeForm">
Cancel
</x-button-link.red>

View File

@ -41,7 +41,7 @@
<!-- Page Content -->
<main>
@if(session()->has('message'))
<div class="bg-green-200 p-2 mb-2">
<div class="bg-emerald-200 p-2 mb-2">
{{ session()->get('message') }}
</div>
@endif

View File

@ -13,7 +13,7 @@
<div class="flex flex-col space-y-2 mt-2 text-lg">
@if(!$recipe->ingredientAmountRelationships->isEmpty())
<div class="flex space-x-2 items-center text-lg">
<div class="text-yellow-500">
<div class="text-amber-500">
<svg class="h-8 w-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 1.944A11.954 11.954 0 012.166 5C2.056 5.649 2 6.319 2 7c0 5.225 3.34 9.67 8 11.317C14.66 16.67 18 12.225 18 7c0-.682-.057-1.35-.166-2.001A11.954 11.954 0 0110 1.944zM11 14a1 1 0 11-2 0 1 1 0 012 0zm0-7a1 1 0 10-2 0v3a1 1 0 102 0V7z" clip-rule="evenodd" />
</svg>

View File

@ -133,7 +133,7 @@
</div>
</div>
<x-inputs.button type="button"
class="bg-green-800 hover:bg-green-700 active:bg-green-900 focus:border-green-900 ring-green-300"
class="bg-emerald-800 hover:bg-emerald-700 active:bg-emerald-900 focus:border-emerald-900 ring-emerald-300"
x-on:click="addNodeFromTemplate($refs.ingredients, 'ingredient');">
Add Ingredient
</x-inputs.button>
@ -158,7 +158,7 @@
</div>
</div>
<x-inputs.button type="button"
class="bg-green-800 hover:bg-green-700 active:bg-green-900 focus:border-green-900 ring-green-300"
class="bg-emerald-800 hover:bg-emerald-700 active:bg-emerald-900 focus:border-emerald-900 ring-emerald-300"
x-on:click="addNodeFromTemplate($refs.steps, 'step');">
Add Step
</x-inputs.button>

View File

@ -43,8 +43,18 @@
@if($item::class === \App\Models\IngredientAmount::class)
<li>
<span>
{{ \App\Support\Number::rationalStringFromFloat($item->amount) }}
@if($item->unitFormatted){{ $item->unitFormatted }}@endif
{{-- Prevent food with serving size > 1 from incorrectly using formatted
serving unit with number of servings. E.g., for a recipe calling for 1
serving of a food with 4 tbsp. to a serving size show "1 serving" instead
of "1 tbsp." (incorrect). --}}
@if($item->unit === 'serving' && $item->ingredient->serving_size > 1 && ($item->ingredient->serving_unit || $item->ingredient->serving_unit_name))
{{ \App\Support\Number::rationalStringFromFloat($item->amount * $item->ingredient->serving_size) }} {{ $item->unitFormatted }}
<span class="text-gray-500">({{ \App\Support\Number::rationalStringFromFloat($item->amount) }} {{ \Illuminate\Support\Str::plural('serving', $item->amount ) }})</span>
@else
{{ \App\Support\Number::rationalStringFromFloat($item->amount) }}
@if($item->unitFormatted){{ $item->unitFormatted }}@endif
@endif
@if($item->ingredient->type === \App\Models\Recipe::class)
<a class="text-gray-500 hover:text-gray-700 hover:border-gray-300"
href="{{ route('recipes.show', $item->ingredient) }}">

View File

@ -26,8 +26,9 @@ Route::middleware(['auth'])->group(function () {
Route::get('/foods/{food}/delete', [FoodController::class, 'delete'])->name('foods.delete');
// Goals.
Route::resource('goals', GoalController::class);
Route::get('/goals/{goal}/delete', [GoalController::class, 'delete'])->name('goals.delete');
Route::resource('goals', GoalController::class)->only(['index', 'create', 'store']);
Route::resource('goals', GoalController::class)->except(['index', 'create', 'store'])->middleware(['can:access,goal']);
Route::get('/goals/{goal}/delete', [GoalController::class, 'delete'])->middleware(['can:access,goal'])->name('goals.delete');
// Ingredient picker.
Route::get('/ingredient-picker/search', [IngredientPickerController::class, 'search'])->name('ingredient-picker.search');
@ -51,10 +52,8 @@ Route::middleware(['auth'])->group(function () {
// Users.
Route::get('/profile/{user}', [ProfileController::class, 'show'])->name('profiles.show');
});
Route::middleware(['auth', 'can:editProfile,user'])->group(function () {
// Profiles (non-admin Users variant).
Route::get('/profile/{user}/edit', [ProfileController::class, 'edit'])->name('profiles.edit');
Route::put('/profile/{user}', [ProfileController::class, 'update'])->name('profiles.update');
Route::get('/profile/{user}/edit', [ProfileController::class, 'edit'])->middleware(['can:editProfile,user'])->name('profiles.edit');
Route::put('/profile/{user}', [ProfileController::class, 'update'])->middleware(['can:editProfile,user'])->name('profiles.update');
});

View File

@ -15,18 +15,36 @@ use Illuminate\Support\Facades\Artisan;
*/
if (!App::isProduction()) {
/**
* Clear all caches.
*/
Artisan::command('dev:cache-clear', function () {
/** @phpstan-ignore-next-line */
assert($this instanceof ClosureCommand);
$commands = [
'cache:clear',
'config:clear',
'route:clear',
'view:clear',
];
foreach ($commands as $command) {
Artisan::call($command);
$this->info(trim(Artisan::output()));
}
$this->info('All caches cleared!');
})->purpose('Clear all caches.');
/**
* Wipe, migrate, and seed the database.
*/
Artisan::command('dev:reset', function () {
/** @phpstan-ignore-next-line */
assert($this instanceof ClosureCommand);
Artisan::call('db:wipe');
$this->info(Artisan::output());
Artisan::call('migrate');
$this->info(Artisan::output());
Artisan::call('db:seed');
$this->info(Artisan::output());
$commands = ['db:wipe', 'migrate', 'db:seed'];
foreach ($commands as $command) {
Artisan::call($command);
$this->info(trim(Artisan::output()));
}
$this->info('Database reset complete!');
})->purpose('Wipe, migrate, and seed the database.');
}

9
tailwind.config.js vendored
View File

@ -1,9 +1,8 @@
const defaultTheme = require('tailwindcss/defaultTheme');
module.exports = {
mode: 'jit',
purge: [
content: [
'./storage/framework/views/*.php',
'./resources/views/**/*.blade.php',
],
@ -16,12 +15,6 @@ module.exports = {
},
},
variants: {
extend: {
opacity: ['disabled'],
},
},
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/forms')

View File

@ -2,6 +2,7 @@
namespace Tests\Feature\Http\Controllers;
use Algolia\AlgoliaSearch\Exceptions\UnreachableException;
use App\Http\Controllers\IngredientPickerController;
use App\Models\Food;
use App\Models\Recipe;
@ -42,8 +43,8 @@ class IngredientPickerControllerTest extends LoggedInTestCase
*/
public function testCanSearchWithAlgolia(): void
{
$this->expectException(ConnectException::class);
$this->expectExceptionMessageMatches("/Could not resolve host: \-dsn\.algolia\.net/");
$this->expectException(UnreachableException::class);
$this->expectExceptionMessage("Impossible to connect, please check your Algolia Application Id.");
Config::set('scout.driver', 'algolia');
$response = $this->get($this->buildUrl(['term' => 'butter']));